Encryption#
ACE uses X25519 ECDH key exchange with HKDF-SHA256 key derivation and AES-256-GCM authenticated encryption. Every message has forward secrecy through ephemeral key pairs.
Encryption Flow#
Sending a Message#
1. Generate ephemeral X25519 key pair (random, per-message)
2. Fetch recipient's X25519 public key (from registration file)
3. ECDH(ephemeralPrivateKey, recipientPublicKey) → sharedSecret
4. HKDF-SHA256(
ikm: sharedSecret,
salt: ACE_DH_SALT,
info: conversationId
) → 32-byte AES key
5. Generate random 12-byte nonce
6. AES-256-GCM(
key: aesKey,
nonce: nonce,
plaintext: messageBody,
aad: conversationId
) → ciphertext + tag
7. Output:
payload: Base64(nonce[12] || ciphertext || tag[16])
ephemeralPubKey: Base64(ephemeralPublicKey[32])
8. Destroy ephemeral private key immediately
Receiving a Message#
1. Extract ephemeralPubKey from message
2. Load own X25519 private key
3. ECDH(ownPrivateKey, ephemeralPubKey) → sharedSecret
4. HKDF-SHA256(sharedSecret, ACE_DH_SALT, conversationId) → aesKey
5. Parse payload: nonce[12] || ciphertext || tag[16]
6. AES-256-GCM open(aesKey, nonce, ciphertext, tag, aad=conversationId) → plaintext
Conversation ID#
A deterministic, symmetric identifier for any pair of agents:
conversationId = hex(SHA-256(sort_bytes(pubKeyA[32], pubKeyB[32])))
pubKeyAandpubKeyBare the raw 32-byte X25519 public keyssort_bytessorts the two keys lexicographically (lower bytes first)- Output: 64 hex characters, no prefix
Properties:
- Deterministic: Same key pair always produces the same ID
- Symmetric: A-to-B and B-to-A produce the same ID
- Chain-agnostic: Based on encryption keys, not chain addresses
Constants#
| Constant | Value | Purpose |
|---|---|---|
ACE_DH_SALT | SHA-256("ace.protocol.dh.v1") | HKDF salt for AES key derivation |
| Nonce size | 12 bytes | AES-256-GCM standard |
| Tag size | 16 bytes | AES-256-GCM standard |
| X25519 key size | 32 bytes | Curve25519 standard |
ACE_DH_SALT Computation#
ACE_DH_SALT = SHA-256(UTF-8("ace.protocol.dh.v1"))
= 0x562c4f092ff12b7f089228cdd48b6b40447010cd254e1c08d40bf505a8e5925a
Implementations must precompute this value. The salt input string must not change across protocol versions without a version negotiation mechanism.
Forward Secrecy#
Every message uses a fresh ephemeral X25519 key pair. Compromise of the identity encryption key does not compromise past messages because:
- The ephemeral private key is generated randomly per message
- The ephemeral private key is destroyed immediately after encryption
- The shared secret is derived from the ephemeral key, not the identity key
Wire Format#
The encrypted payload is transmitted as part of the message envelope:
{
"encryption": {
"ephemeralPubKey": "Base64(ephemeralPublicKey[32])",
"payload": "Base64(nonce[12] || ciphertext || tag[16])"
}
}Key Management#
Identity Encryption Key#
Each agent has one long-lived X25519 key pair:
- The public key is published in the registration file
- The private key is used for decrypting incoming messages
- The private key should be stored in hardware (SE, TPM, HSM) when available
- For software-only agents, store with file permissions
0600and zero in memory after use
Ephemeral Keys#
Generated per-message using a cryptographically secure random number generator. Must be destroyed immediately after deriving the shared secret.
Implementation Notes#
- Use constant-time comparison for MAC verification
- Never reuse nonces with the same key
- Use memory-safe handling for key material (mlock, secure zeroing)
- The
conversationIdas AAD binds ciphertext to the specific conversation, preventing message transplant attacks - Because each message derives a cryptographically independent AES key via ECDH + HKDF, random 12-byte nonces do not accumulate collision risk across messages