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; a fresh ephemeral gives each message an independent sender contribution. This is ephemeral-static ECIES against the recipient’s static key, so it does not provide forward secrecy (see the note under Encrypted Intents).
  2. Sender performs ECDH using the ephemeral private key and the recipient’s static X25519 public key, taken from the keys.encryption entry named by currentEncryptionKeyId on the recipient’s Agent Card.
  3. Derives a symmetric key via HKDF-SHA256:
    • IKM: The raw ECDH shared secret (32 bytes).
    • Salt: "ink/0.1" (UTF-8 encoded, 7 bytes).
    • Info: "ink/0.1/encrypt" (UTF-8 encoded, 15 bytes).
    • Output length: 32 bytes.
  4. Encrypts the plaintext envelope with AES-256-GCM using a random 12-byte nonce.
  5. Wraps the result in an InkEncryptedEnvelope 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": "<replay-protection nonce, 16 to 256 characters of [A-Za-z0-9_-]>"
}

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 intent message. An intent envelope has no type field; it carries id, correlationId, createdAt, intent and a nested payload under the strict per-intent schema:

{
"protocol": "ink/0.1",
"id": "01JQ8Z5N3K7M9V4P2C6R8T0W1Y",
"correlationId": "01JQ8Z5N3K7M9V4P2C6R8T0W1Z",
"createdAt": "2026-03-18T12:00:00.000Z",
"from": "did:plc:sender",
"to": "did:plc:recipient",
"intent": "schedule_meeting",
"payload": {
"proposedTimes": ["2026-03-20T15:00:00.000Z", "2026-03-20T16:00:00.000Z"],
"topic": "Discuss partnership opportunity",
"format": "video",
"urgency": "normal"
},
"signature": "<base64url body signature>",
"nonce": "<base64url>",
"timestamp": "2026-03-18T12:00:00.000Z"
}

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.

The same binding is a producer obligation. A conformant sealer MUST NOT emit an envelope its own decrypt rule would reject, so it refuses to encrypt a plaintext whose from is not the outer envelope sender, or whose to is absent, empty or different from the recipient identity the caller asserted. encryptInkPayload and its Go counterpart EncryptInkPayload both enforce this at seal time. An implementation that checks the binding only on receive can still mint envelopes nobody will open.

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, recipient, timestamp or message type. The library constructs AAD as follows:

  1. Build an object with exactly these members: protocol ("ink/0.1"), type, from, recipientKey, ephemeralKey, nonce, timestamp, messageNonce. recipientKey is the base64url of the recipient’s static X25519 public key; ephemeralKey and nonce are the base64url outer-envelope values. Every other value must equal its outer-envelope value byte-for-byte. Binding recipientKey ties the ciphertext to one recipient: the decrypter recomputes it from its own private key, so a ciphertext encrypted for a different recipient fails the tag.
  2. JCS-canonicalize the object (sorts keys lexicographically), so the on-wire member order does not matter.
  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