Encryption
For sensitive payloads (e.g., scheduling details, personal context), INK supports ECIES encryption.
Encryption Flow
Encryption Procedure
- Sender generates an ephemeral X25519 key pair for this message. The sender’s long-term
encryptionKeyMultibaseis NOT used for ECDH, ephemeral keys provide forward secrecy. - Sender performs ECDH using the ephemeral private key and the recipient’s
encryptionKeyMultibase(from theiragentLink). - Derives a symmetric key via HKDF-SHA256:
- IKM: The raw ECDH shared secret (32 bytes).
- Salt:
"ink/0.1"(UTF-8 encoded, 6 bytes). - Info:
"ink/0.1/encrypt"(UTF-8 encoded). - Output length: 32 bytes.
- Encrypts the plaintext envelope with AES-256-GCM using a random 12-byte nonce.
- Wraps the result in a
InkEncryptedPayloadouter envelope.
Outer Envelope
{ "protocol": "ink/0.1", "type": "network.tulpa.encrypted", "from": "did:plc:sender", "ephemeralKey": "<base64url-encoded ephemeral X25519 public key>", "nonce": "<base64url-encoded 12-byte AES-GCM nonce>", "ciphertext": "<base64url-encoded AES-GCM ciphertext + 16-byte auth tag>", "timestamp": "2026-03-18T12:00:00Z", "messageNonce": "<base64url-encoded 128-bit replay-protection nonce>"}The outer envelope is not encrypted. from, ephemeralKey, timestamp and messageNonce are plaintext. This is necessary so the recipient can identify the sender and apply replay protection before decryption.
Plaintext Envelope
The ciphertext, when decrypted, yields a JSON object identical to an unencrypted INK message:
{ "protocol": "ink/0.1", "type": "network.tulpa.intent", "from": "did:plc:sender", "to": "did:plc:recipient", "intent": "schedule_meeting", "purpose": "Discuss partnership opportunity", "urgency": "normal", "nonce": "<base64url>", "timestamp": "2026-03-18T12:00:00Z"}The recipient MUST verify that from and to in the plaintext envelope match the outer envelope’s from and the recipient’s own DID. A mismatch indicates tampering and MUST be rejected.
Decryption Procedure
- Parse the outer envelope. Verify the
Authorizationheader signature. Reject before decryption if invalid. - Check
timestampandmessageNonceagainst replay protection rules. Reject before decryption if replayed. - Perform ECDH using the recipient’s own X25519 private key and the
ephemeralKey. - Derive the symmetric key via HKDF-SHA256.
- Decrypt
ciphertextusing AES-256-GCM. - Parse the plaintext envelope. Verify
frommatches outerfromandtomatches recipient’s DID. - Process the inner message normally.
Envelope Anatomy: Plaintext vs Encrypted
A side-by-side comparison of what is visible on the wire in each mode.
Key differences:
- Encrypted envelopes expose only:
from,ephemeralKey,nonce,timestamp,messageNonce - The
tofield, intent type, purpose and all payload data are inside the ciphertext - Both modes carry Ed25519 signatures and replay protection
- The outer
nonceis the AES-GCM IV (12 bytes, base64url-encoded);messageNonceis the replay-protection nonce. Both fields live on the outer envelope.
AAD Construction
AES-GCM Additional Authenticated Data binds the ciphertext to every security-relevant outer-envelope field. Without this binding, an attacker could replay the same ciphertext under a different sender, timestamp or message type. The library constructs AAD as follows:
- Build an object with exactly these fields, in this order:
protocol,type,from,ephemeralKey,nonce,timestamp,messageNonce. All values must equal the outer-envelope values byte-for-byte. - JCS-canonicalize the object (sorts keys lexicographically).
- Prepend the domain separator
ink/0.1:envelope\n. - UTF-8-encode the result. The resulting bytes are passed as
additionalDatatoAES-GCM.encrypt/AES-GCM.decrypt.
Implementations MUST construct AAD identically on both sides. Any mismatch causes the GCM tag to fail and decryption returns an error — there is no version negotiation for AAD.
Encryption Requirements by Intent Type
| Intent Type | Sender | Receiver | Rationale |
|---|---|---|---|
schedule_meeting | MUST encrypt | MUST reject plaintext | Contains availability windows, calendar data |
context_share | MUST encrypt | MUST reject plaintext | Contains personal/professional context |
multi_party_sync | MUST encrypt | MUST reject plaintext | Contains scheduling coordination data for multi-party enclaves |
intro_request | MAY encrypt | MUST accept both | Low sensitivity |
opportunity | MAY encrypt | MUST accept both | Low sensitivity |
follow_up | SHOULD encrypt | MUST accept both | May reference prior conversations |
ask | MAY encrypt | MUST accept both | General-purpose |