| Crates.io | turnkey_enclave_encrypt |
| lib.rs | turnkey_enclave_encrypt |
| version | 0.5.0 |
| created_at | 2025-05-10 20:03:09.500223+00 |
| updated_at | 2025-10-17 22:19:42.462287+00 |
| description | Utilities to encrypt and decrypt data sent to and from Turnkey secure enclaves, using HPKE (RFC 9180). Used in authentication, export, and import flows. |
| homepage | https://turnkey.com |
| repository | https://github.com/tkhq/rust-sdk |
| max_upload_size | |
| id | 1668769 |
| size | 106,414 |
turnkey_enclave_encryptThis crate contains primitives to encrypt and decrypt data, sent to and from Turnkey secure enclaves (see Enclave to end-user secure channels).
Encryption and decryption are "one-shot" using the HPKE standard (RFC 9180). Neither the client or the server should ever be reused to send/receive more than one message. We want to avoid the recipient target key being used more then once in order to improve forward secrecy; see security profile section for important details and caveats.
The flows where encryption and decryption are relevant are:
use turnkey_enclave_encrypt::{AuthenticationClient, QuorumPublicKey};
let mut client = AuthenticationClient::new();
let target_public_key = client.target_public_key(); // can be used in auth activity params
let bundle = "<auth bundle>";
let decrypted = client.decrypt(bundle).expect("decryption should succeed");
If you persist client key material between initiation and decryption you may use dangerous_from_bytes to create an AuthenticationClient. Keep in mind the one-shot encryption semantics, you may not use the same client IKM to decrypt many different bundles.
use turnkey_enclave_encrypt::{AuthenticationClient, QuorumPublicKey};
let client_ikm = hex::decode("...private bytes from client...").expect("cannot decode client secret bytes");
let bundle = "<auth bundle goes here, it's a base58-encoded string>";
let decrypted = AuthenticationClient::dangerous_from_bytes(client_ikm)
.decrypt(bundle)
.expect("decryption should succeed");
To decrypt an exported private key or wallet, use decrypt_private_key or decrypt_wallet:
use turnkey_enclave_encrypt::{ExportClient, QuorumPublicKey};
let mut client = ExportClient::new(&QuorumPublicKey::production_signer());
// Decrypt a wallet (result: string)
let wallet_mnemonic_phrase = client.decrypt_wallet_mnemonic_phrase("<bundle>", "<organization id>");
// Decrypt a private key (result: bytes)
let private_key = client.decrypt_private_key("<bundle>", "<organization id>");
To encrypt private keys or wallets, use encrypt_private_key_with_bundle or encrypt_wallet_with_bundle. The resulting value is a string, ready to use as an activity param. The organization and user IDs need to match the organization and user who initiated import.
use turnkey_enclave_encrypt::{ImportClient, QuorumPublicKey};
let mut client = ImportClient::new(&QuorumPublicKey::production_signer());
// Encrypt private keys
let encrypted_key = client.encrypt_private_key_with_bundle(
"<bytes to encrypt>",
"<bundle>",
"<organization id>",
"<user id>",
).expect("encryption should succeed");
// Encrypt wallet seed phrase
let encrypted_wallet = client.encrypt_wallet_with_bundle(
"<mnemonic phrase>",
"<bundle>",
"<organization id>",
"<user id>",
).expect("encryption should succeed");
cargo test
This protocol builds on top of the HPKE standard (RFC 9180) by adding recipient pre-flight authentication so the client can verify it is sending ciphertext to a turnkey controlled enclave and the enclave can verify its sending ciphertext to the correct client. See the security profile section more details.
KEM_P256_HKDF_SHA256KDF_HKDF_SHA256AEAD_AES256GCMb"turnkey_hpke"EncappedPublicKey||ReceiverPublicKeyserverEncappedPub = ENCRYPT(plaintext, clientTargetPub) and clears clientTargetPub from memory.serverEncappedPub_sig_enclaveAuthPriv = SIGN(serverEncappedPub, enclaveAuthPriv).(ciphertext, serverEncappedPub, serverEncappedPub_sig_enclaveAuthPriv) to client.VERIFY(serverEncappedPub, serverEncappedPub_sig_enclaveAuthPriv).DECRYPT(ciphertext, serverEncappedPub, clientTargetPriv) and the client target pair is cleared from memory. If the target pair is used multiple times we increase the count of messages that an attacker with the compromised target private key can decrypt.Note there is no mechanism to prevent a faulty client from resubmitting the same target public key.
serverTargetPub_sig_enclaveAuthPriv = SIGN(serverTargetPub, enclaveAuthPriv).(serverTargetPub, serverTargetPub_sig_enclaveAuthPriv) to client.VERIFY(serverTargetPub, serverTargetPub_sig_enclaveAuthPriv).clientEncappedPub = ENCRYPT(plaintext, serverTargetPub) and clears serverTargetPub from memory.(ciphertext, clientEncappedPub) to server and the client is cleared from memory.clientEncappedPub has been verified by the Ump policy engine.DECRYPT(ciphertext, clientEncappedPub, clientTargetPriv) and server target pair is cleared from memory. If the target pair is used multiple times we increase the count of messages that an attacker with the compromised target private key can decrypt.We achieve recipient authentication for both the server and client:
The underlying HPKE spec does not provide forward secrecy on the recipient side since the target key can be long lived. To improve forward secrecy we specify that the target key should only be used once by the sender and receiver. We cannot enforce this strictly on the client-side because a client may choose to reuse their key. We could implement timestamp-based validation or rate limiting client-side but it wouldn't be a complete solution. For now we accept that a client can use an encryption bundle multiple times if it so desires. However we enforce one-time use of the key pair on the enclave side by deleting it once a successful decryption happens.
We use OpMode Base because the sender's KEM private key is not long lived and thus does not need HPKE authentication. In order for this to be exploited one side's private key data would have to be leaked or an attacker would need to spoof a message from the sender. Turnkey mitigates this attack by layering a signature from an authentication key over payloads that contain ciphertext + encappedPub. Note that in the case of client to server the authentication signature is implicitly verified by the Ump policy engine. Read more about HPKE asymmetric authentication here.