Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Encryption

This page explains how Maskura’s envelope encryption works, from the key you hand the gateway to the bytes that leave your writer. It is the long-form companion to the post-quantum feature page and ADR 0011.

The model

Encryption is envelope encryption with a fresh data key per field:

  1. The gateway generates a random 256-bit data encryption key (DEK) and encrypts the field with AES-256-GCM.
  2. The DEK is then key-encapsulated to your public key, so only the holder of the matching private key can recover it.
  3. Both the ciphertext and the encapsulated DEK are written to storage.

Maskura does see the plaintext field transiently while the selected transform runs inside the per-session Wasm sandbox. It does not persist that plaintext as part of the encrypted output and never holds your private key. Decryption happens entirely on the client.

This is not end-to-end confidentiality against the gateway operator: the gateway must be trusted while it processes plaintext. Neither client-held keys nor Wasm isolation proves that an operator-controlled runtime did not copy that plaintext or that it ran the selected transform. Maskura does not currently provide execution attestation or computation proofs. See the operator trust boundary.

The primitives

RolePrimitiveWhy
Data cipherAES-256-GCMAuthenticated encryption; 256-bit keys resist Grover (~2^128)
Classical KEMX25519 (ECDH)Protects against an undiscovered weakness in a young PQ scheme
Post-quantum KEMML-KEM-768 (FIPS 203)Protects against a cryptographically relevant quantum computer
CombinerHKDF-SHA256Folds the two shared secrets into one DEK

The construction is a hybrid KEM: the DEK is secure unless both X25519 and ML-KEM-768 are broken. This is the consensus position of the IETF hybrid key-exchange draft, NSA CNSA 2.0, and BSI.

The data path contains no signatures, so ML-DSA and SLH-DSA are out of scope — KEM-only.

Encapsulation (encrypt)

For each detected field the gateway:

m            = 32 random bytes from a CSPRNG
(mlkem_ss, mlkem_ct) = ML-KEM-768.Encaps(ek, m)        # ek = your ML-KEM public key
x25519_sk    = 32 random bytes
x25519_epk   = X25519(x25519_sk, basepoint)
x25519_ss    = X25519(x25519_sk, your_x25519_pk)
dek          = HKDF-SHA256(ikm = x25519_ss ‖ mlkem_ss,
                           salt = "",
                           info = "maskura/hybrid/envelope-dek/v1",
                           L = 32)
enc_dek      = x25519_epk ‖ mlkem_ct                  # 1120 bytes
field        = AES-256-GCM(dek, iv, plaintext)

The wrapped key material (enc_dek) carries everything you need to decrypt: your X25519 ephemeral public key and the ML-KEM-768 ciphertext. The DEK itself is never stored — it is derived from the two shared secrets on both sides.

Decapsulation (decrypt, client-side)

(x25519_epk, mlkem_ct) = split(enc_dek)
x25519_ss = X25519(your_x25519_sk, x25519_epk)
mlkem_ss  = ML-KEM-768.Decaps(your_mlkem_dk, mlkem_ct)
dek       = HKDF-SHA256(same construction as above)
plaintext = AES-256-GCM.Decrypt(dek, iv, ct ‖ tag)

The envelope

Each encrypted field becomes a JSON object with five fields:

{"alg":"X25519+ML-KEM-768/AES-256-GCM","iv":"<b64>","enc_dek":"<b64>","ct":"<b64>","tag":"<b64>"}
  • alg identifies the construction. Existing RSA-OAEP/AES-256-GCM envelopes remain decryptable (dual-alg read); new writes are hybrid-only.
  • iv is the 12-byte AES-GCM nonce.
  • enc_dek is the 1120-byte hybrid encapsulation (base64).
  • ct / tag are the AES-256-GCM ciphertext and authentication tag.

Key format

Keys are a single base64 blob of fixed-length concatenation, wrapped in PEM:

-----BEGIN MASKURA HYBRID PUBLIC KEY-----
base64( x25519_pk ‖ mlkem_ek )        # 32 + 1184 = 1216 bytes
-----END MASKURA HYBRID PUBLIC KEY-----

-----BEGIN MASKURA HYBRID PRIVATE KEY-----
base64( x25519_sk ‖ mlkem_seed )      # 32 + 64 = 96 bytes
-----END MASKURA HYBRID PRIVATE KEY-----
ComponentSize
X25519 public key / shared secret32 B
ML-KEM-768 encapsulation key (public)1184 B
ML-KEM-768 ciphertext1088 B
ML-KEM-768 shared secret32 B
ML-KEM-768 seed (private key serialization)64 B
Hybrid public key1216 B
Hybrid private key96 B
enc_dek1120 B

The gateway’s public-key-pem config field is unchanged: it is still a string. The private key never touches the gateway.

Client tooling status

Current gateway releases accept only MASKURA HYBRID PUBLIC KEY PEM blocks for new encrypted writes. MaskuraClient.generate_keypair() in Python and MaskuraClient.generateKeypair() in TypeScript now create that exact hybrid format. Their decrypt helpers decapsulate current hybrid envelopes locally and also retain dual-algorithm read compatibility for old RSA envelopes. Explicit generate_legacy_rsa_keypair() and generateLegacyRsaKeypair() helpers exist only for pre-hybrid gateways and compatibility fixtures.

The CLI provides the same safe path: maskura key create --generate-encryption-key --private-key-out <path> attaches the public key to the newly created API key and writes the private key locally with create-new semantics (mode 0600 on Unix). maskura get ... --decrypt <path> decrypts hybrid envelopes before writing the object to stdout. The private key is never sent to Maskura.

Python hybrid support requires cryptography >= 47. TypeScript uses the Noble X25519, HKDF-SHA256, and ML-KEM implementations while retaining CommonJS output. SDK and CLI tests exercise the gateway wire sizes and round-trip behavior. The Python and TypeScript implementations have also been verified against the deterministic envelope produced by the Rust gateway reference implementation.

Security properties

  • IND-CCA2. Both KEMs are CCA-secure; the HKDF combiner produces an indistinguishable DEK unless both are broken.
  • Per-field keys. A fresh DEK per field means a compromise of one DEK does not expose any other field.
  • Authenticated encryption. AES-GCM binds the ciphertext to its tag; any tampering fails decryption.
  • Fail closed. If key encapsulation or encryption fails, the field is redacted rather than emitted as plaintext.
  • Single-alg write. New writes are hybrid-only; legacy RSA keys are rejected for new writes and remain only for decrypting previously written objects.

Cost

The hybrid wrap trades a slightly larger key encapsulation for materially cheaper CPU than the RSA-OAEP wrap it replaces:

  • ~34M Wasm fuel per encrypted field (vs ~52M for RSA-2048 OAEP).
  • enc_dek grows from 256 B (RSA-2048) to 1120 B per field.

Measured numbers and methodology are in Benchmarks.