Key Derivation (X25519)
Sapphire.deriveSymmetricKey implements X25519 Diffie-Hellman key exchange. Given a public key and a private key from opposite ends of the exchange, it returns a shared 32-byte symmetric key. Combine this with Symmetric Encryption to build encrypt-to-user or encrypt-to-contract patterns.
Signature
function deriveSymmetricKey(bytes32 publicKey, bytes32 privateKey)
internal view returns (bytes32);
publicKey— the other party's X25519 public key.privateKey— your X25519 private key.
Returns a 32-byte symmetric key that both parties can compute independently:
- Alice computes
derive(Bob.publicKey, Alice.privateKey) - Bob computes
derive(Alice.publicKey, Bob.privateKey) - Both get the same result
Basic usage
import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";
contract EncryptToUser {
function encryptFor(
bytes32 userPublicKey,
bytes32 myPrivateKey,
bytes memory plaintext
) external view returns (bytes32 nonce, bytes memory ciphertext) {
bytes32 sharedKey = Sapphire.deriveSymmetricKey(userPublicKey, myPrivateKey);
nonce = bytes32(Sapphire.randomBytes(32, ""));
ciphertext = Sapphire.encrypt(sharedKey, nonce, plaintext, "");
}
}
Only the intended recipient can compute sharedKey (they have myPublicKey and their own private key). Nobody else, including the enclave, retains that key.
Full encrypt-to-user example
contract PrivateMessages {
// Users register their X25519 public key
mapping(address => bytes32) public userPublicKeys;
mapping(address => bytes[]) private inboxes;
function registerKey(bytes32 pk) external {
userPublicKeys[msg.sender] = pk;
}
function sendMessage(address to, bytes32 senderPrivateKey, bytes calldata message) external {
bytes32 sharedKey = Sapphire.deriveSymmetricKey(
userPublicKeys[to],
senderPrivateKey
);
bytes32 nonce = bytes32(Sapphire.randomBytes(32, ""));
bytes memory ct = Sapphire.encrypt(sharedKey, nonce, message, "PrivateMessages");
// Store envelope: nonce || ciphertext, plus sender's public key so recipient can decrypt
bytes32 senderPk = derivePublicFromPrivate(senderPrivateKey); // custom helper
inboxes[to].push(abi.encodePacked(senderPk, nonce, ct));
}
}
Off-chain, the recipient (using their own X25519 private key) computes the same shared secret and decrypts.
When to use X25519 vs storing a symmetric key
Use X25519 (deriveSymmetricKey) when:
- You need to encrypt for a specific recipient who possesses their own private key.
- You want to send messages between users where the contract only relays ciphertext.
- Off-chain services need to decrypt.
Use a stored symmetric key when:
- The contract is the sole holder and user of the key.
- Data will be re-decrypted many times from the same key.
- No off-chain recipients are involved.
Ephemeral vs persistent keys
Ephemeral X25519 keys — generated per session, thrown away. Provides forward secrecy: leaking today's session key does not compromise past or future sessions. This is how OXN's own encrypted transaction envelope works.
Persistent X25519 keys — stored in contract storage or in the user's wallet. Simpler, but a leaked key exposes all historical ciphertexts encrypted to it. Use persistent keys when the user identity is meant to be stable.
Common patterns
Handshake pattern:
function beginSession(bytes32 clientEphemeralPk) external view returns (bytes32 shared) {
return Sapphire.deriveSymmetricKey(
clientEphemeralPk,
contractPrivateKey
);
}
Encrypt-then-emit (event):
event Message(address indexed to, bytes32 nonce, bytes ciphertext);
function sendMessage(...) external {
bytes32 shared = Sapphire.deriveSymmetricKey(userPublicKeys[to], msg.sender);
// ...
emit Message(to, nonce, ciphertext);
}
Remember: on OXN, event contents are encrypted per-caller-session. The message goes into the block, but the topic and data are opaque to observers.
Next steps
- Symmetric Encryption — the encryption side
- Signatures — for signed authentication