Skip to main content

SimpleGo Cryptography Documentation

Complete cryptographic specification for the SimpleGo Double Ratchet implementation.


Overview

SimpleGo implements end-to-end encryption using the Double Ratchet algorithm with X3DH key agreement.

Cryptographic Components

ComponentAlgorithmLibraryPurpose
Key AgreementX3DHCustomInitial shared secret
DH RatchetX448 (Curve448)wolfSSLForward secrecy
Key DerivationHKDF-SHA512mbedTLSKey expansion
Symmetric EncryptionAES-256-GCMmbedTLSMessage encryption
Per-Queue EncryptionX25519libsodiumQueue-level encryption
SignaturesEd25519libsodiumAuthentication

X3DH Key Agreement

Extended Triple Diffie-Hellman establishes the initial shared secret.

Keys Involved

KeyNameOwnerTypeSize
spk1Semi-permanent KeyPeerX448 Public56 bytes
rk1Ratchet KeyPeerX448 Public56 bytes
sk1Ephemeral SecretUsX448 Private56 bytes
pk1Ephemeral PublicUsX448 Public56 bytes
rpk1Our Ratchet KeyUsX448 Key Pair56+56 bytes

DH Calculations

The sender performs three Diffie-Hellman operations:

  • dh1 = X448_DH(sk1, spk1) - Our ephemeral x Peer semi-permanent
  • dh2 = X448_DH(sk1, rk1) - Our ephemeral x Peer ratchet
  • dh3 = X448_DH(rpk1_priv, spk1) - Our ratchet x Peer semi-permanent
  • ikm = dh1 || dh2 || dh3 (168 bytes total)

HKDF Derivation

Input:

  • salt = 64 zero bytes
  • ikm = dh1 || dh2 || dh3 (168 bytes)
  • info = SimpleXX3DH (11 bytes)

Output: 96 bytes

  • [0:32] = header_key
  • [32:64] = next_header_key
  • [64:96] = root_key

Double Ratchet

The Double Ratchet provides forward secrecy through continuous key evolution.

Root KDF

Derives new keys when the DH ratchet advances.

Input:

  • salt = root_key (32 bytes)
  • ikm = dh_output (56 bytes)
  • info = SimpleXRootRatchet (19 bytes)

Output: 96 bytes

  • [0:32] = new_root_key
  • [32:64] = chain_key
  • [64:96] = next_header_key

Chain KDF

Derives per-message keys and IVs.

Input:

  • salt = empty (0 bytes)
  • ikm = chain_key (32 bytes)
  • info = SimpleXChainRatchet (20 bytes)

Output: 96 bytes

  • [0:32] = message_key
  • [32:64] = new_chain_key
  • [64:80] = header_iv (FIRST!)
  • [80:96] = message_iv (SECOND!)

Important: The IV order is critical. header_iv comes before message_iv.


KDF Parameters Summary

KDFSaltIKMInfoOutput
X3DH64 x 0x00dh1+dh2+dh3 (168B)SimpleXX3DH (11B)96 bytes
Rootroot_key (32B)dh_output (56B)SimpleXRootRatchet (19B)96 bytes
Chainempty (0B)chain_key (32B)SimpleXChainRatchet (20B)96 bytes

AES-256-GCM Encryption

SimpleX uses AES-256-GCM with 16-byte IVs.

Parameters

ParameterSize
Key32 bytes
IV16 bytes
Auth Tag16 bytes
AADVariable

Header AAD (rcAD) - 112 bytes

rcAD = our_ratchet_key_raw (56 bytes) || peer_ratchet_key_raw (56 bytes)

Important: Use RAW keys (56 bytes each), not SPKI-encoded keys (68 bytes).

Payload AAD - 235 bytes

payload_aad = rcAD (112 bytes) || emHeader (123 bytes)

Important: Size is 235 bytes (112 + 123), not 236.


SPKI Key Format

X448 SPKI (68 bytes)

Header (12 bytes): 30 42 30 05 06 03 2b 65 6f 03 39 00

  • Raw key (56 bytes)

X25519 SPKI (44 bytes)

Header (12 bytes): 30 2a 30 05 06 03 2b 65 6e 03 21 00

  • Raw key (32 bytes)

wolfSSL Byte-Order Issue

wolfSSL exports X448 keys in reversed byte order.

Problem:

  • wolfSSL output: [byte_55][byte_54]...[byte_0]
  • SimpleX expects: [byte_0][byte_1]...[byte_55]

Solution: Reverse all bytes after key generation and DH operations.


Verification Results

All cryptographic operations verified against Python reference implementations.

ComponentStatus
X448 Key GenerationVerified
X448 Diffie-HellmanVerified
HKDF-SHA512 (X3DH)Verified
HKDF-SHA512 (Root KDF)Verified
HKDF-SHA512 (Chain KDF)Verified
AES-256-GCMVerified
Wire FormatVerified

Common Issues and Solutions

Issue #1: wolfSSL Byte Order

Symptom: DH shared secret does not match Solution: Reverse all key bytes

Issue #2: IV Order in Chain KDF

Symptom: Decryption fails Solution: header_iv is bytes [64:80], message_iv is bytes [80:96]

Issue #3: SPKI vs Raw Keys in AAD

Symptom: Auth tag mismatch Solution: Use raw keys (56 bytes) for AAD, not SPKI (68 bytes)

Issue #4: Wrong IV Size

Symptom: Encryption produces garbage Solution: SimpleX uses 16-byte IVs, not 12-byte

Issue #5: Payload AAD Size

Symptom: Auth tag verification fails Solution: payload_aad = 112 + 123 = 235 bytes


References


License

AGPL-3.0 - See LICENSE