Skip to content

Encryption

For sensitive payloads (e.g., scheduling details, personal context), INK supports ECIES encryption.

Encryption Flow

Diagram

Encryption Procedure

  1. Sender generates an ephemeral X25519 key pair for this message. The sender’s long-term encryptionKeyMultibase is NOT used for ECDH, ephemeral keys provide forward secrecy.
  2. Sender performs ECDH using the ephemeral private key and the recipient’s encryptionKeyMultibase (from their agentLink).
  3. 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.
  4. Encrypts the plaintext envelope with AES-256-GCM using a random 12-byte nonce.
  5. Wraps the result in a InkEncryptedPayload outer 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

  1. Parse the outer envelope. Verify the Authorization header signature. Reject before decryption if invalid.
  2. Check timestamp and messageNonce against replay protection rules. Reject before decryption if replayed.
  3. Perform ECDH using the recipient’s own X25519 private key and the ephemeralKey.
  4. Derive the symmetric key via HKDF-SHA256.
  5. Decrypt ciphertext using AES-256-GCM.
  6. Parse the plaintext envelope. Verify from matches outer from and to matches recipient’s DID.
  7. 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.

Diagram

Key differences:

  • Encrypted envelopes expose only: from, ephemeralKey, nonce, timestamp, messageNonce
  • The to field, intent type, purpose and all payload data are inside the ciphertext
  • Both modes carry Ed25519 signatures and replay protection
  • The outer nonce is the AES-GCM IV (12 bytes, base64url-encoded); messageNonce is 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:

  1. 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.
  2. JCS-canonicalize the object (sorts keys lexicographically).
  3. Prepend the domain separator ink/0.1:envelope\n.
  4. UTF-8-encode the result. The resulting bytes are passed as additionalData to AES-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 TypeSenderReceiverRationale
schedule_meetingMUST encryptMUST reject plaintextContains availability windows, calendar data
context_shareMUST encryptMUST reject plaintextContains personal/professional context
multi_party_syncMUST encryptMUST reject plaintextContains scheduling coordination data for multi-party enclaves
intro_requestMAY encryptMUST accept bothLow sensitivity
opportunityMAY encryptMUST accept bothLow sensitivity
follow_upSHOULD encryptMUST accept bothMay reference prior conversations
askMAY encryptMUST accept bothGeneral-purpose