Back to Blog

The Math Behind End-to-End Encryption — How Anchora's Keys Actually Work

You don't need a math degree to use E2E encryption. But understanding the math helps you trust it. Here's what's actually happening under the hood — no PhD required.

Why Math Matters in Encryption

Every time you send a WhatsApp message, your phone performs elliptic curve multiplications, hash computations, and Diffie-Hellman key exchanges — all in under 50 milliseconds. The math is what makes it impossible for anyone (including WhatsApp itself) to read your messages.

Anchora uses the exact same algorithms. This blog explains what's actually happening under the hood — no PhD required.

1. Elliptic Curves — The Foundation of Everything

All keys in the Anchora system are points on a mathematical curve called Curve25519.

The Curve Equation

Curve25519 Equation
y² = x³ + 486662x² + x   (mod p)

where p = 2²&sup5;&sup5; - 19  (a very large prime number — that's where "25519" comes from)

This isn't a curve you'd draw in school. It exists in a "finite field" — a world where all numbers wrap around at p (like a clock wraps at 12). Every valid (x, y) pair that satisfies this equation is a "point" on the curve.

Why Curves? The Trapdoor

The magic of elliptic curves is point multiplication — you can multiply a point by a number, but you can't reverse it:

The One-Way Property
Easy (fast):     Public Key = private_key × G     (takes microseconds)
Hard (impossible): private_key = Public Key ÷ G    (would take billions of years)

G is a known "generator point" — a specific starting point on the curve that everyone agrees on. Multiplying G by your private key (a random 256-bit number) gives you a public key (another point on the curve).

This one-way property is called the Elliptic Curve Discrete Logarithm Problem (ECDLP). No one has found an efficient way to solve it. The best known attack against Curve25519 requires approximately 2¹²&sup8; operations — more than all the atoms in the observable universe.

Visual intuition: Think of it like mixing paint. Red + Blue = Purple (easy). But can you unmix purple back into red and blue? Impossible. Similarly: Private key × G = Public key (easy). Public key ÷ G = Private key? Impossible.

2. Key Generation — What Happens When You Call generateKeySet()

Ed25519 Key Pair (Identity Key)

Ed25519 Key Generation
Step 1: Generate 32 random bytes (256 bits of entropy)
        seed = crypto.randomBytes(32)

Step 2: Hash with SHA-512 to get 64 bytes
        h = SHA-512(seed)

Step 3: "Clamp" the first 32 bytes (private scalar)
        h[0]  &= 248    // Clear lowest 3 bits  → ensures divisible by 8
        h[31] &= 127    // Clear highest bit     → keeps number < 2²&sup5;&sup5;
        h[31] |= 64     // Set second-highest    → ensures consistent bit length

        WHY CLAMP?
        - Divisible by 8: prevents small-subgroup attacks
        - < 2²&sup5;&sup5;: prevents wrap-around modular reduction
        - Fixed high bit: ensures constant-time operations (prevents timing attacks)

Step 4: Compute public key
        public_key = clamped_scalar × B
        (B is the Ed25519 base point, different from X25519's)

Result:
  Private key: 32 bytes (the clamped scalar — NEVER leaves the device)
  Public key:  32 bytes (a point on the curve — stored on server as 64-char hex)

X25519 Key Pair (Pre-Keys and OPKs)

X25519 Key Generation
Step 1: Generate 32 random bytes
        private_key = crypto.randomBytes(32)

Step 2: Clamp (same rules as Ed25519)
        private_key[0]  &= 248
        private_key[31] &= 127
        private_key[31] |= 64

Step 3: Compute public key
        public_key = private_key × G
        (G is the X25519 base point: x-coordinate = 9)

Result:
  Private key: 32 bytes (clamped random — NEVER leaves the device)
  Public key:  32 bytes (x-coordinate only — stored on server as 64-char hex)

Why Two Different Curves?

Ed25519 and X25519 are actually the same underlying curve (Curve25519), but used in two different forms:

Ed25519 X25519
Form Twisted Edwards Montgomery
Used for Digital signatures Key exchange (Diffie-Hellman)
Output (x, y) coordinates x-coordinate only
Why this form Faster, constant-time signing Faster, simpler DH computation

They're mathematically equivalent — you can convert between them. But using the right form for each purpose is faster and safer.

3. Digital Signatures — How Ed25519 Signing Works

When you register keys, the Signed Pre-Key must be signed by the Identity Key. This proves "this pre-key belongs to me." Here's the exact math:

Signing

Ed25519 Signing Process
Inputs:
  message     = the signed pre-key's public key (32 bytes)
  private_key = Ed25519 private key (from seed)
  public_key  = corresponding Ed25519 public key

Step 1: Compute deterministic nonce
  r = SHA-512(private_key_second_half || message)
  r = r mod L    (L is the curve order)

  WHY DETERMINISTIC?
  - No random number generator needed during signing
  - If RNG is broken/biased, signature is STILL secure
  - Same message always produces same signature (reproducible)
  - PS3 was hacked because Sony used a FIXED nonce in ECDSA
    — Ed25519 prevents this by design

Step 2: Compute commitment point
  R = r × B     (B = base point)

Step 3: Compute challenge
  h = SHA-512(R || public_key || message)
  h = h mod L

Step 4: Compute response
  s = (r + h × private_scalar) mod L

Step 5: Signature = (R, s) — 64 bytes total
  First 32 bytes: R (compressed point)
  Last 32 bytes:  s (scalar)

Verification

Ed25519 Verification
Inputs:
  message    = the data that was signed
  signature  = (R, s) — 64 bytes
  public_key = signer's Ed25519 public key

Step 1: Recompute challenge
  h = SHA-512(R || public_key || message)
  h = h mod L

Step 2: Check the equation
  s × B  ==  R + h × public_key  ?

WHY THIS WORKS:
  s × B = (r + h × private) × B         ← substitute s
        = r × B + h × private × B       ← distribute
        = R + h × public_key            ← because R = r×B and public = private×B
        ✓ MATCHES!

Without the private key, you can't produce s that satisfies this equation.
Forging a signature = solving ECDLP = computationally impossible.

What This Looks Like in Anchora

Anchora Key Signing Example
Identity Key (Ed25519):
  Private: f1a2b3c4...  (32 bytes, stays on device)
  Public:  9c5ae952...  (32 bytes, stored on server)

Signed Pre-Key (X25519):
  Public:  f00082dc...  (32 bytes)

Signature = Ed25519.sign(signed_pre_key_public, identity_private)
  Result:  edaa2533...  (64 bytes = 128 hex chars, stored on server)

Anyone can verify:
  Ed25519.verify(signed_pre_key_public, signature, identity_public)
  → true (this pre-key really belongs to this identity)

4. Diffie-Hellman Key Exchange — The Core Magic

This is the most elegant piece of mathematics in all of cryptography. Two strangers create the same secret without ever exchanging secrets.

The Basic Idea

Diffie-Hellman Key Exchange
Public knowledge: G (generator point), p (prime modulus)

Alice (private = a):              Bob (private = b):
  A = a × G  (public key)          B = b × G  (public key)

  Publishes A →→→→→→→            Publishes B
              ←←←←←←←

Alice computes:                   Bob computes:
  shared = a × B                    shared = b × A
         = a × (b × G)                    = b × (a × G)
         = ab × G                          = ba × G
         = SAME!                           = SAME!

An eavesdropper sees A and B but CANNOT compute ab × G.
This is the Computational Diffie-Hellman (CDH) problem — no efficient solution known.

X25519 Specifically

X25519 Diffie-Hellman
shared_secret = X25519(my_private_key, their_public_key)

Internally:
  1. Decode their_public_key as x-coordinate on Curve25519
  2. Perform scalar multiplication: result = my_private × their_point
  3. Return the x-coordinate of the result (32 bytes)

Properties:
  - Always 32 bytes output
  - Constant-time (immune to timing side-channel attacks)
  - No invalid curve attacks (every 32-byte input is valid)
  - Takes ~150 microseconds on modern hardware

5. X3DH — The 4 DH Operations Explained

When Alice wants to message Bob, the SDK performs X3DH (Extended Triple Diffie-Hellman). Here's the exact math:

X3DH Key Setup
Alice has:                              Bob's bundle (from server):
  IK_A  = Ed25519 identity key pair      IK_B  = Ed25519 identity public key
  EK_A  = X25519 ephemeral key pair      SPK_B = X25519 signed pre-key public
                                         OPK_B = X25519 one-time pre-key public

NOTE: Ed25519 keys are converted to X25519 for DH operations.
(This is a standard birational map between twisted Edwards and Montgomery forms)

The 4 DH Operations

4 Diffie-Hellman Operations
DH1 = X25519(IK_A_private, SPK_B_public)
      Alice's IDENTITY × Bob's SIGNED PRE-KEY
      PURPOSE: Mutual authentication
      PROPERTY: Only works if Alice is who she claims (has IK_A private)
                and the pre-key really belongs to Bob (signed by IK_B)

DH2 = X25519(EK_A_private, IK_B_public)
      Alice's EPHEMERAL × Bob's IDENTITY
      PURPOSE: Forward secrecy for the initiator
      PROPERTY: EK_A is thrown away after this — even if Alice's identity
                key is compromised later, this DH result can't be recomputed

DH3 = X25519(EK_A_private, SPK_B_public)
      Alice's EPHEMERAL × Bob's SIGNED PRE-KEY
      PURPOSE: Asynchronous capability
      PROPERTY: Works even when Bob is offline (SPK is already on server)

DH4 = X25519(EK_A_private, OPK_B_public)
      Alice's EPHEMERAL × Bob's ONE-TIME PRE-KEY
      PURPOSE: Per-session uniqueness (forward secrecy)
      PROPERTY: OPK is consumed (used once, deleted) — even if everything
                else is compromised, each session has a unique component

Why 4 Operations?

Each DH protects against a different attack scenario:

Attacker compromises... Affected DH Impact
Alice's identity key (IK_A) DH1, DH2 DH3, DH4 still secret
Bob's signed pre-key (SPK_B) DH1, DH3 DH2, DH4 still secret
Alice's ephemeral key (EK_A) DH2, DH3, DH4 DH1 still secret (but EK_A is deleted!)
Bob's one-time pre-key (OPK_B) DH4 DH1, DH2, DH3 still secret
ALL of Alice's long-term keys DH1, DH2 DH3, DH4 (ephemeral/OPK) protect past sessions
Key insight: To decrypt a message, an attacker needs ALL 4 DH results. Compromising any subset is not enough.

Combining the Results

Combining DH Results
raw_material = DH1 || DH2 || DH3 || DH4    (128 bytes concatenated)

This raw material goes into HKDF (next section) to produce the actual encryption key.

If OPK is not available (exhausted):
  raw_material = DH1 || DH2 || DH3          (96 bytes — still secure, slightly weaker)

6. HKDF — Turning Raw DH Output into Proper Keys

The raw DH outputs aren't suitable as encryption keys directly. They might have biased bits or predictable patterns. HKDF (HMAC-based Key Derivation Function) from RFC 5869 solves this.

Two-Phase Process

HKDF Extract + Expand
Phase 1: EXTRACT — compress randomness into a fixed-size key

  salt = 32 bytes of zeros (or a random value)
  IKM  = DH1 || DH2 || DH3 || DH4    (input keying material)

  PRK = HMAC-SHA-256(salt, IKM)
      = 32 bytes of high-quality pseudorandom material

  WHAT THIS DOES:
  - Even if DH outputs have some structure, HMAC destroys it
  - Output is indistinguishable from random (proven property of HMAC)
  - Fixed 32-byte output regardless of input size


Phase 2: EXPAND — derive multiple keys from the PRK

  info = "anchora-x3dh-shared-secret"    (context string, prevents cross-protocol attacks)

  T(1) = HMAC-SHA-256(PRK, info || 0x01)           → 32 bytes (encryption key)
  T(2) = HMAC-SHA-256(PRK, T(1) || info || 0x02)   → 32 bytes (authentication key)
  T(3) = HMAC-SHA-256(PRK, T(2) || info || 0x03)   → 32 bytes (more keys if needed)

  WHAT THIS DOES:
  - Derives MULTIPLE independent keys from one shared secret
  - Each key is cryptographically independent (knowing one doesn't reveal others)
  - The "info" string ensures keys derived for different purposes are different


Final result:
  shared_secret = T(1) = 32-byte AES-256-GCM key

  This is what the SDK returns as session.sharedSecret
  Both Alice and Bob derive the EXACT SAME 32 bytes

Why Not Just Use SHA-256 Directly?

Approach Problem
SHA-256(DH1 || DH2 || DH3 || DH4) No salt support (vulnerable to pre-computation attacks), no domain separation, can only derive one key
HKDF-SHA-256(salt, DH..., info) Salt adds randomness, info string separates uses, can derive unlimited independent keys. RFC 5869 — formally proven secure

7. AES-256-GCM — Encrypting the Actual Message

Once the shared secret is derived, messages are encrypted with AES-256-GCM (Galois/Counter Mode).

How It Works

AES-256-GCM Encryption
Inputs:
  key       = 32 bytes (from HKDF — the shared secret)
  plaintext = the message to encrypt
  nonce     = 12 random bytes (MUST be unique per message with the same key)
  aad       = optional "additional authenticated data" (authenticated but not encrypted)

Encryption:

  Step 1: Generate counter blocks
    CTR_0 = nonce || 0x00000001
    CTR_1 = nonce || 0x00000002
    CTR_2 = nonce || 0x00000003
    ...

  Step 2: Encrypt (Counter mode)
    keystream_1 = AES-256(key, CTR_1)     ← 16-byte block
    keystream_2 = AES-256(key, CTR_2)
    ...
    ciphertext = plaintext XOR keystream   ← same length as plaintext

  Step 3: Authenticate (GHASH — the "G" in GCM)
    tag = GHASH(H, aad, ciphertext)        ← 16-byte authentication tag
    where H = AES-256(key, 0¹²&sup8;)          ← hash key derived from encryption key

  Result:
    output = nonce || tag || ciphertext
    (nonce is prepended so the receiver knows what nonce was used)

Why GCM?

Mode Encrypts Authenticates Limitation
AES-CBC Yes No Attacker can modify ciphertext without detection (padding oracle, bit flipping)
AES-CTR Yes No Parallelizable but still no integrity check
AES-GCM Yes Yes None — encrypts AND authenticates in ONE pass (~4 GB/s with AES-NI)

The Numbers

AES-256 Security & Performance
AES-256 security level: 256-bit key = 2²&sup5;&sup6; possible keys
  To brute-force: ~10&sup7;&sup7; operations
  If you checked 10¹&sup8; keys/second: would take 10&sup5;&sup9; years
  Age of universe: ~10¹&sup0; years

  Even quantum computers (Grover's algorithm) only reduce this to 2¹²&sup8; operations
  — still utterly infeasible.

Performance:
  Encrypt 1 KB:   < 0.1 ms
  Encrypt 1 MB:   < 1 ms
  Encrypt 100 MB: < 50 ms

8. Argon2id — Protecting Key Backups

When users back up their private keys with a password, Argon2id converts the password into an encryption key.

Why Not Just SHA-256 the Password?

SHA-256 vs Argon2id
SHA-256("mypassword") takes 0.0001 ms
  → An attacker with a GPU can try 10 BILLION passwords per second
  → All 8-character passwords cracked in < 1 hour

Argon2id("mypassword") takes ~300 ms and uses 64 MB of RAM
  → A GPU can only try ~1,000 passwords per second (needs 64 MB per attempt)
  → All 8-character passwords would take ~200,000 years

How Argon2id Works

Argon2id Process
Inputs:
  password    = user's backup password
  salt        = 16 random bytes (stored alongside the backup)
  timeCost    = 3  (number of iterations)
  memoryCost  = 65536 KB (64 MB of RAM)
  parallelism = 4  (lanes/threads)
  outputLen   = 32 bytes (AES-256 key)

Step 1: Initialize memory matrix
  Allocate 64 MB as a grid of 1 KB blocks
  Fill first row using Blake2b(password || salt || params)

Step 2: Fill memory (data-dependent + data-independent hybrid)
  For each iteration (t = 1 to timeCost):
    For each lane (l = 1 to parallelism):
      For each block position:

        Argon2id alternates between:
        - Argon2i mode (first half): index computation is data-INDEPENDENT
          → resistant to side-channel attacks (timing, cache)
        - Argon2d mode (second half): index computation is data-DEPENDENT
          → resistant to GPU/ASIC time-memory tradeoff attacks

        new_block = G(old_block, reference_block)
        where G is a compression function based on Blake2b

Step 3: Finalize
  XOR the last block from each lane
  Hash the result with Blake2b to produce the 32-byte output

Step 4: Use the output as AES-256-GCM key
  encrypted_backup = AES-GCM(argon2_output, nonce, private_keys)

Why 64 MB Memory Matters

Without memory-hardness (SHA-256, bcrypt): A GPU (RTX 4090) with 24 GB VRAM can run billions of parallel hashes. Cost to try 10 billion passwords: ~$1.

With Argon2id (64 MB per hash): 24 GB VRAM ÷ 64 MB = 375 parallel hashes MAX. Cost to try 10 billion passwords: ~$2,600,000. The 64 MB requirement makes parallel attacks 7 MILLION times more expensive.

9. SHA-256 — Hashing in the System

SHA-256 appears in several places in Anchora's key system:

Where It's Used

SHA-256 Usage in Anchora
1. Key Fingerprints
   fingerprint = identityKey.substring(0, 16)
   (First 16 chars of the hex-encoded public key — quick visual verification)

2. Backup Checksums
   checksum = SHA-256(encryptedBundle)
   (Verify the backup blob wasn't corrupted during storage/transfer)

3. Inside HKDF
   HKDF uses HMAC-SHA-256 internally for both extract and expand phases

4. Inside Ed25519
   Ed25519 uses SHA-512 (SHA-256's bigger sibling) for nonce generation and hashing

5. Anchora Core (Existing)
   record_hash = SHA-256(data) → Merkle tree → blockchain
   (Data integrity anchoring — separate from E2E encryption)

SHA-256 Properties

SHA-256 Properties
Input:  any data (0 to 2&sup6;&sup4; bits)
Output: always exactly 256 bits (32 bytes, 64 hex chars)

Properties:
  1. Deterministic:    SHA-256("hello") ALWAYS = 2cf24dba5fb0a30e...
  2. Avalanche:        SHA-256("hello") vs SHA-256("hellp") = completely different
  3. One-way:          Given the hash, can't find the input
  4. Collision-free:   Can't find two inputs with the same hash (2¹²&sup8; operations)
  5. Fast:             ~500 MB/s on modern CPUs

How it works (simplified):
  1. Pad message to multiple of 512 bits
  2. Split into 512-bit blocks
  3. For each block, run 64 rounds of:
     - Bitwise rotations, shifts, XORs
     - Modular additions
     - Non-linear functions (Ch, Maj)
     - Round constants (derived from cube roots of first 64 primes)
  4. Output = 8 × 32-bit words = 256 bits

10. Putting It All Together — The Complete Math Pipeline

When Alice sends Bob an encrypted message through Anchora, here's every mathematical operation:

Complete Math Pipeline
SETUP (one-time):
——————————————————————————————
Alice's device:
  1. crypto.randomBytes(32)              → identity seed
  2. SHA-512(seed) → clamp              → Ed25519 private key
  3. private × B                         → Ed25519 public key (identity)
  4. crypto.randomBytes(32) → clamp     → X25519 private key (signed pre-key)
  5. private × G                         → X25519 public key (signed pre-key)
  6. Ed25519.sign(SPK_public, IK_private) → signature (128 hex chars)
  7. Repeat steps 4-5 for 20 OPKs       → 20 X25519 key pairs

  Total: 23 key generations + 1 signature = ~5 ms

SERVER (register):
  8. Store public keys + signature       → MongoDB (UserKey collection)
  9. AuditLog.create(KEYS_REGISTERED)    → MongoDB (KeyAuditLog)


MESSAGE ENCRYPTION:
——————————————————————————————
Alice's device:
  10. Fetch Bob's bundle from server     → HTTP GET /v1/keys/bundle/bob
  11. crypto.randomBytes(32) → clamp     → ephemeral X25519 key pair
  12. X25519(IK_A_priv, SPK_B_pub)       → DH1 (32 bytes)
  13. X25519(EK_A_priv, IK_B_pub)        → DH2 (32 bytes)
  14. X25519(EK_A_priv, SPK_B_pub)       → DH3 (32 bytes)
  15. X25519(EK_A_priv, OPK_B_pub)       → DH4 (32 bytes)
  16. HKDF-SHA-256(DH1||DH2||DH3||DH4)  → 32-byte shared secret
  17. crypto.randomBytes(12)              → AES-GCM nonce
  18. AES-256-GCM(shared_secret, nonce, plaintext) → ciphertext + 16-byte tag

  Total: 4 DH + 1 HKDF + 1 AES-GCM = ~2 ms

  19. DELETE ephemeral private key from memory (forward secrecy!)


SERVER (shared-secret facilitation):
  20. Atomic OPK consumption             → MongoDB updateOne with $elemMatch
  21. SharedSecret.create(sessionId)      → MongoDB (SharedSecret collection)
  22. AuditLog entries                    → MongoDB (KeyAuditLog)


DECRYPTION (Bob's device):
——————————————————————————————
  23. X25519(SPK_B_priv, IK_A_pub)       → DH1' (same as DH1!)
  24. X25519(IK_B_priv, EK_A_pub)        → DH2' (same as DH2!)
  25. X25519(SPK_B_priv, EK_A_pub)       → DH3' (same as DH3!)
  26. X25519(OPK_B_priv, EK_A_pub)       → DH4' (same as DH4!)
  27. HKDF-SHA-256(DH1'||DH2'||DH3'||DH4') → SAME 32-byte shared secret
  28. AES-256-GCM-decrypt(shared_secret, nonce, ciphertext) → plaintext

  Total: 4 DH + 1 HKDF + 1 AES-GCM = ~2 ms

  29. DELETE OPK_B private key (one-time use — never used again)


BACKUP (optional):
——————————————————————————————
  30. crypto.randomBytes(16)              → Argon2 salt
  31. Argon2id(password, salt, 64MB, 3 iterations) → 32-byte backup key (~300ms)
  32. crypto.randomBytes(12)              → AES-GCM nonce
  33. AES-256-GCM(backup_key, nonce, all_private_keys) → encrypted bundle
  34. SHA-256(encrypted_bundle)           → checksum
  35. Store on server                     → HTTP POST /v1/keys/backup


TOTAL MATH OPERATIONS PER MESSAGE:
  Key generations:          1 (ephemeral)
  Scalar multiplications:   4 (DH operations per side)
  HMAC-SHA-256 calls:       3 (HKDF extract + expand)
  AES-256 block encryptions: plaintext_length / 16
  GHASH multiplications:    ciphertext_length / 16

  Wall clock time: < 5 ms total (faster than a screen refresh)

Security Guarantees — What the Math Proves

Property Guaranteed by What it means
Confidentiality AES-256-GCM + X3DH Only Alice and Bob can read the message
Integrity GCM authentication tag Any modification is detected
Authentication Ed25519 signatures + DH1 Alice knows it's really Bob (and vice versa)
Forward secrecy Ephemeral keys + OPKs Past messages stay safe even if keys are later compromised
Deniability No long-term signing of messages Bob can't prove to a third party that Alice sent a specific message
Offline capability Pre-key bundle system Alice can encrypt for Bob even when Bob is offline
Brute-force resistance 256-bit keys everywhere 2²&sup5;&sup6; possible keys — heat death of universe before cracking
Side-channel resistance Constant-time implementations Timing attacks don't leak key bits
Password protection Argon2id (64 MB, 3 iterations) GPU brute-force of backups is economically infeasible

Comparison with Other Platforms

Feature Anchora Signal WhatsApp Telegram
X3DH key agreement Yes Yes Yes No (custom MTProto)
Ed25519 signatures Yes Yes Yes No (RSA-2048)
X25519 key exchange Yes Yes Yes No (DH-2048)
AES-256-GCM Yes Yes Yes No (AES-256-IGE)
HKDF-SHA-256 Yes Yes Yes No (custom KDF)
Argon2id backups Yes Yes (PIN) No No
Blockchain anchoring Yes No No No
Open API for developers Yes (10 APIs) No No Partial
Race-safe OPK consumption Yes (tested) Presumably Unknown N/A

Anchora uses the same battle-tested algorithms as Signal — the gold standard of secure messaging — and adds blockchain data integrity on top.

Further Reading

  • Curve25519 paper (Daniel J. Bernstein) — the original 2006 paper
  • Ed25519 paper — high-speed high-security signatures
  • X3DH specification (Signal) — the key agreement protocol
  • RFC 5869 (HKDF) — HMAC-based key derivation
  • Argon2 paper — Password Hashing Competition winner
  • NIST SP 800-38D (AES-GCM) — authenticated encryption

Ready to implement E2E encryption?

Explore the Key Management API documentation and start building with Signal Protocol-level encryption today.

View Keys API Docs