Technical White Paper

The KX402 Protocol

A wallet-native, end-to-end encrypted messaging protocol for the Kaspa era. Identity is a wallet address. The server stores only ciphertext.

Version 1.0
July 2026
Fluxkmail Research

Abstract

KX402 is a wallet-native encrypted messaging protocol that operates on the Kaspa blockchain. It replaces traditional email infrastructure — email addresses, passwords, and centralized mail servers — with cryptocurrency wallet addresses as identity and browser-side end-to-end encryption.

The server, which may be operated by anyone including Fluxkmail, cannot decrypt message content. Encryption keys are derived locally from cryptographic wallet signatures and never leave the user's browser in plaintext. This paper describes the architecture, key derivation, message flow, and threat model of the protocol as implemented in production.

1. Protocol Overview

A user's identity is their Kaspa wallet address. There is no registry of usernames, no password database, and no recovery server. Authentication and authorization are established through cryptographic signatures.

1.1 Identity Paths

KX402 supports two authentication paths. The primary path relies on a browser extension wallet (Kasware) that signs messages with secp256k1 Schnorr signatures. An alternative path uses a TTT Agent ID from the tttz.xyz identity network, bound to a Kaspa wallet address. Both paths produce a deterministic input from which the user's encryption keys are derived.

1.2 Design Principles

Zero-knowledge server
The backend stores ciphertext only. Operators never possess decryption keys.
Key derivation from wallet signature
Encryption keys are derived locally and never uploaded.
Ephemeral keys in memory
Private keys exist only during an active session and are destroyed on page unload.
Graceful degradation
Messages to recipients without keys fall back to plaintext — no protocol breakage.

2. Cryptographic Architecture

2.1 Algorithms

ComponentSpecification
Key ExchangeECDH on NIST P-256 (secp256r1)
Symmetric CipherAES-256-GCM (authenticated)
KDFSHA-256 (signature → 256-bit storage key)
EncodingBase64 (storage), UTF-8 (text)
IV96-bit random, unique per encryption

2.2 Key Hierarchy

The ECDH public key is stored in plaintext in the user's profile record. The ECDH private key — the secret that enables decryption — is encrypted with a key derived from the wallet signature before being stored:

Key hierarchy
Wallet Signature (Schnorr / secp256k1)
      │
      ▼
   SHA-256
      │
      ▼
Storage Key (AES-256) ──► encrypts ──► ECDH Private Key (P-256, JWK)
                                         │
                                         ▼
                              Stored encrypted on the server

3. Key Derivation

3.1 Kasware Identity Path

When a user connects via Kasware, the extension signs a domain-separated message:

Derivation message
"Fluxkmail E2E Encryption Key Derivation v1"

The returned signature is passed through SHA-256 to produce a 256-bit AES-GCM key:

Key derivation (JavaScript – Web Crypto API)
storageKey = await crypto.subtle.importKey(
  'raw',
  await crypto.subtle.digest('SHA-256', signature),
  { name: 'AES-GCM' },
  false,
  ['encrypt', 'decrypt']
);

This storage key encrypts the user's ECDH private key before it is persisted. The private key never leaves the browser in plaintext.

3.2 TTT Identity Path

For TTT users without a hardware wallet, the storage key is derived from their agent credentials:

storageKey = SHA-256("<tttId>:<password>")

3.3 First-Time Setup

On first authentication, a new ECDH keypair is generated in the browser:

First-time key generation
keyPair = crypto.subtle.generateKey(
  { name: 'ECDH', namedCurve: 'P-256' }, true,
  ['deriveKey', 'deriveBits']
);

// Export both halves
publicKeyJwk  = crypto.subtle.exportKey('jwk', keyPair.publicKey);  // plaintext
privateKeyJwk = crypto.subtle.exportKey('jwk', keyPair.privateKey);

// Encrypt the private key with the storage key
iv = crypto.getRandomValues(new Uint8Array(12));
encryptedPrivateKey = crypto.subtle.encrypt(
  { name: 'AES-GCM', iv }, storageKey,
  JSON.stringify(privateKeyJwk)
);

The server stores:

  • encryption_public_key — ECDH public key (JWK, plaintext)
  • encrypted_private_key — private key encrypted under AES-GCM (base64)
  • encryption_key_iv — initialization vector (base64)

3.4 Session Unlock

On subsequent page loads — when the in-memory private key has been lost — the user re-signs the derivation message. The signature decrypts the stored private key inside the browser's crypto.subtle. No network round-trip ever exposes the private key.

4. Message Encryption Flow

4.1 Sending

Encryption flow (symmetric via ECDH shared secret)
// 1. Resolve recipient's public key
recipientKey = UserProfile.filter({ wallet_address: recipient });
// → encryption_public_key (JWK string)

if (recipientKey exists && sender has private key):

    // 2. Derive shared secret via ECDH
    sharedSecret = crypto.subtle.deriveBits(
      { name: 'ECDH', public: recipientPublicKey },
      senderPrivateKey, 256
    );

    // 3. Encrypt the message body with AES-GCM
    iv = crypto.getRandomValues(new Uint8Array(12));
    aesKey = crypto.subtle.importKey('raw', sharedSecret,
      { name: 'AES-GCM' }, false, ['encrypt']);
    ciphertext = AES-GCM.encrypt(aesKey, iv, utf8(body));

    // 4. Store: only ciphertext, IV, and the counterparty key reach the server
    Email.create({
      body:            base64(ciphertext),
      encryption_iv:   base64(iv),
      is_encrypted:    true,
      enc_public_key:  <counterparty's public key — see table below>
    });

else:
    // Fallback — recipient has no encryption key set up yet
    Email.create({ body: plaintext, is_encrypted: false });

Two copies of each message are created — one in the sender's Sent folder and one in the recipient's Inbox. Each copy stores the counterparty's public key as enc_public_key so the reader of that specific copy can re-derive the same ECDH shared secret:

CopyReaderenc_public_keyDerivation
SentSenderRecipient's pubkeyECDH(sender_priv, recipient_pub)
InboxRecipientSender's pubkeyECDH(recipient_priv, sender_pub)

Both derivations yield an identical shared secret — a fundamental property of ECDH — so the same ciphertext decrypts for both parties.

4.2 Reading

Decryption flow
// 1. Import the counterparty's public key stored on the email
otherKey = importJWK(email.enc_public_key);

// 2. Re-derive the same shared secret
sharedSecret = ECDH.deriveBits(
  { name: 'ECDH', public: otherKey },
  readerPrivateKey, 256
);

// 3. Decrypt
aesKey = AES.import(sharedSecret);
plaintext = AES-GCM.decrypt(aesKey, email.encryption_iv, email.body);

Decryption happens entirely inside window.crypto.subtle. The server only delivers ciphertext, the IV, and the counterparty's public key.

5. Privacy Model

5.1 What is stored on the server

FieldContentServer-readable?
subjectPlaintextYes
bodyAES-GCM ciphertext (base64)No
previewPlaceholder onlyN/A
encryption_iv96-bit IV (public by design)Yes (non-secret)
enc_public_keyPublic key (JWK)Yes (non-secret)

The email subject remains plaintext to enable server-side sorting and search. Only the body is encrypted. Subject-line protection is a planned extension (Section 6.2).

5.2 What is never stored

  • The wallet signature (ephemeral — used once per session to unlock)
  • The SHA-256-derived storage key
  • The ECDH private key (only its encrypted form persists)
  • The per-message AES key (derived on the fly, never stored)

5.3 Session forward secrecy

The ECDH private key persists across sessions in its encrypted form. Strict forward secrecy (automatic key erasure) is achieved on sign-out: the in-memory private key is destroyed on page unload, and unlocking requires a fresh wallet signature.

6. Threat Model

6.1 Covered

Database breach
Attacker obtains ciphertext and public keys. Without the wallet signature, decryption is infeasible.
Server operator curiosity
The operator never possesses any private key in plaintext — they cannot read mail just because they run the server.
Network MITM
TLS protects transport; the message body is additionally encrypted at the application layer.

6.2 Not yet covered (v1)

Metadata
Sender, recipient, timestamps, and subject remain visible to the server for routing.
Recipient compromise
An attacker who controls the recipient’s wallet can unlock all mail sent to them.
Quantum
ECDH (P-256) is not post-quantum secure; a future revision may use a PQ key exchange.
Subject-line leakage
Subjects are plaintext in v1; subject encryption is planned for v1.1.

7. References

  • —
    W3C Web Cryptography API
    W3C Recommendation, January 2017
  • —
    NIST SP 800-186
    Recommendations for Discrete Logarithm-Based Cryptography (P-256)
  • —
    NIST SP 800-38D
    Galois / Counter Mode (GCM)
  • —
    RFC 7517
    JSON Web Key (JWK)
  • —
    Kaspa protocol
    kaspa.org — blockDAG protocol specification