AES

In addition to ECDSA signatures, Schnorr signatures, and RSA operations, the Builder Vault also supports 128, 192, and 256-bit AES in CTR, CBC, and GCM mode.

📘

A Note on Performance and Parameters

Computing AES in a threshold wallet requires the use of so-called general purpose MPC. Builder Vault uses one of the most efficient general purpose MPC protocols, but even so, the computation of AES still requires a significant amount of bandwidth between the MPC nodes. For this reason, it it is not suited for use cases which require high throughput or with long plaintexts.

Builder Vault currently only supports AES keys shared with a security threshold of one and with two or three MPC nodes. Contact the Builder Vault support team for more information.

Key Generation

You can generate a new AES key among a set of MPC nodes as follows:

keyID, err := client.AES().GenerateKey(ctx, sessionConfig, threshold, keyLength, desiredKeyID)

As usual, the sessionConfig defines the set of MPC nodes among which the resulting AES key should be secret shared and a unique session ID. The threshold is the security threshold for the key. The keyLength specifies the byte length of the key to generate. You can choose between 16, 24, or 32. Finally, desiredKeyID can be used to give the key a specific ID. If desiredKeyID is set to the empty string, the MPC nodes will agree on a random key ID for the new key.

To start the MPC key generation, the above method must be called on the SDKs of all the MPC nodes specified in the session configuration. The MPC session will only succeed if they agree on the session configuration, as well as the threshold, keyLength, and desired key ID.

If the MPC operation succeeds, the key ID will be returned to each SDK.

📘

When to Use the Key

If an honest MPC node succeeds with the key generation MPC session, the protocol ensures that the MPC node will hold a correct key share. But the protocol does not guarantee that all honest players will succeed. In the worst case, only a few honest players will succeed, too few to actually perform a decryption or signature. Therefore you should make sure that all MPC nodes successfully completed the key generation session before using the new key.

Key Import

Existing AES keys can be imported into or exported from the Builder Vault. To import an AES key into Builder Vault you must first secret share the key. If for example you want the key to be shared among the MPC nodes with player IDs 2, 3 and 5:

players := []int{2, 3, 5}
sharing, err := tsmutils.AESSecretShare(players, aesKey)
shares, checksum := sharing.Shares, sharing.Checksum

This splits the key into a number of "xor" shares of the key. In addition, it returns a checksum. The checksum is the first three bytes of the encryption of the 16-byte block 0x01010101010101010101010101010101.

Each share must then be encrypted under the MPC nodes' public wrapping key:

wrappingKey, err := client.WrappingKey().WrappingKey(ctx)
publicKey, err := x509.ParsePKIXPublicKey(wrappingKey)
publicRSAKey, _ := publicKey.(*rsa.PublicKey)
wrappedKeyShare, err := tsmutils.Wrap(publicRSAKey, keyShare)

Finally, the shares are imported in an MPC session, where each wrapped share is input by the corresponding SDK.

threshold := 1
keyID, err := client.AES().ImportKey(ctx, sessionConfig, threshold, wrappedKeyShare, checksum, desiredKeyID)

The threshold is the security threshold that you want for the imported key sharing. All SDKs must agree on the threshold and the checksum. The SDKs may all omit the checksum, but if they do provide the checksum, it is used to validate the imported key. This is a good way to protect the integrity of the key. Without the checksum, any one of the SDK users may tamper with it's share and this will result in modifications to the imported key.

The desiredKeyIDlet's you suggest a key ID for the imported key. This can be useful, for example if you want to migrate a key sharing from one Builder Vault instance to another, without changing the key ID. If you provide an empty string as the desired key ID, the MPC nodes will choose a random key ID.

A complete example, showing how to import an AES key, is given at the end of the AES-CTR section below.

Key Export

Exporting an AES key is done by running an MPC session. The export MPC session only runs if all involved SDKs call the method. So this gives each SDK user the opportunity to enforce whatever policy it finds necessary before allowing the export. As an additional level of security, all MPC nodes involved in the MPC session must have AES key export enabled in their configuration.

The MPC session computes a new random secret sharing of the AES key and one share of the new sharing is output to each SDK. Each share is wrapped before being output to the SDK, by a wrapping key provided by the SDK. Only wrapping keys that are whitelisted in the MPC node configuration is accepted.

If you want to migrate the AES key from one Builder Vault instance to another, you can obtain the wrapping key from the target node as follows:

wrappingKey, err := targetClient.WrappingKey().WrappingKey(ctx)
publicKey, err := x509.ParsePKIXPublicKey(wrappingKey)
publicWrappingKey, ok := publicKey.(*rsa.PublicKey)

Alternatively, you can generate a key:

privateWrappingKey, err := rsa.GenerateKey(rand.Reader, 4096)
publicWrappingKey, err := x509.MarshalPKIXPublicKey(&privateWrappingKey.PublicKey)

In any case, you must ensure that the public wrapping key is whitelisted in the source MPC node configuration.

res, err := client.AES().ExportKey(ctx, exportSessionConfig, keyID, publicWrappingKey)

The returned result can now be used in an import session at another Builder Vault instance. Alternatively, if you have access to all key shares and their private unwrapping keys, you can recover the AES key:

// If you do this for all key shares
keyShare, err := tsmutils.Unwrap(privateWrappingKey, res.WrappedKeyShare)
checksum := res.Checksum

// Then you can recover the AES key as follows
exportedKey, err := tsmutils.AESRecombine([][]byte{ keyShare1, keyShare2, keyShare3}, checksum)

A complete example, showing how to export an AES key, is given at the end of the next section.

📘

Migrating Keys without Single Point of Trust

A major goal of the Builder Vault is to eliminate any single point of trust. If someone exports and unwraps all key shares and recovers the key as shown above, that party will essentially be a single point of trust. To avoid this, it is recommended to only unwrap and recover the exported AES key by importing the wrapped key shares into another Builder Vault instance, or in some other high-security environment, such as an HSM.

AES-CTR

After an AES key has been generated in or imported into the Builder Vault, the MPC nodes can generate an AES CTR key stream from a key and an initialization vector as follows:

partialResult, err := client.AES().CTRKeyStream(ctx, sessionConfig, keyID, iv, keyStreamLength)

The keyStreamLength is the length of the desired key stream. As with the key generation, this is an MPC session, and the method call must be made on the SDK for each of the MPC nodes in the MPC session, and they must agree on the parameters (key ID, IV, keyStreamLength).

Once the MPC session succeeds, the partial results from each SDK must be collected, and the final AES CTR key stream can be obtained as follows:

keyStream, err := tsm.AESFinalizeCTR([]byte{ partialResult1, partialResult2, ... })

If you want to use the key stream to encrypt or decrypt a message, you must compute the logical xor of the key stream and the message, like this:

ciphertext := make([]byte, len(plaintext))
for i := range plaintext {
    ciphertext[i] = plaintext[i] ^ keyStream[i]
}

You can find a full example in our demo repository (Go). It shows how to import an AES key, use it to generate an AES-CTR key stream, and exporting the key.

AES-CBC

Encrypting a message using AES in CBC mode is done as follows:

partialResult, err := client.AES().CBCEncrypt(ctx, sessionConfig, keyID, iv, plaintext)

As with AES-CTR, the partial results must be collected from all involved SDKs, and the ciphertext is generated like this:

ciphertext, err := tsm.AESFinalizeCBCEncrypt([]byte{ partialResult1, partialResult2, ... })

Decryption is done in a similar way:

partialResult, err := client.AES().CBCDecrypt(ctx, sessionConfig, keyID, iv, ciphertext)

and

decryptedMessage, err := tsm.AESFinalizeCBCDecrypt([]byte{ partialResult1, partialResult2, ... })
📘

Partial AES Decryption Results

In the current version of Builder Vault, each partial result contains the decrypted message in the clear and the finalization essentially just ensures that all partial results contain the same decrypted message. This is contrary to the partial signature results of ECDSA and RSA signing, where each partial signature does not reveal information about the final signature.

AES-GCM

The Builder Vault also supports AES in GCM mode. This requires a 12-byte IV (unlike AES-CTR and AES-CBC which both require 16 bytes IV), and it lets you provide additional data to be authenticated.

This is illustrated with a full example in our demo repository here: Go. In this example, an 128-bit AES key is first generated. The key is then used for AES-GCM encryption and decryption of a message.

As shown in the last part of the example, the combination of the partial results from the AES-GCM decrypt method will return a tsm.ErrMessageAuthenticationFailed if the authenticity of the ciphertext or additional data were broken.

To start the key generation, encryption and decryption MPC sessions in the example, the three SDKs are invoked concurrently using goroutines. This is because each call to the SDK blocks until all SDKs in the MPC session are called. In most real-world cases, each SDK will run in its own environment, e.g., one SDK on a mobile device, and the two other SDKs on different servers, and in that case goroutines are not needed.


What’s Next