In this document, a secret refers to any unencrypted data (such as a raw PGP private key or CryptPad seed) that could allow an administrator or attacker to decrypt user data. An encrypted secret requires the user's master password (or a key derived from it) to be decrypted.
In Scope for AurionMail:
- End-to-end encryption/decryption of emails using PGP within Bulwark Webmail.
- Generating and securely passing encryption secrets to CryptPad.
- Enabling user-side derivation of master encryption keys from a single master password.
Out of Scope:
- Internal CryptPad secret lifecycle after handover. Once the secret is passed to CryptPad, data protection relies on CryptPad's native Zero-Knowledge architecture.
- Zero-Knowledge Password: The login password never leaves the user's device in plaintext.
- Zero-Knowledge Secrets: Unencrypted secrets never leave the client device.
- Single Password UX: Users maintain only one master password.
- In-Memory Operations: Secrets are kept strictly in RAM on the client side and are never written to disk unencrypted.
- Single Prompt: Users enter their passphrase at most once per active session.
To transfer secrets securely between origins—such as Parent (P, e.g., SSO) and Child (C, e.g., Bulwark or CryptPad), without violating our security constraints:
- P holds an unencrypted secret in RAM.
- P generates a cryptographically secure random
seedand initialization vector (iv). - P uses these parameters to generate a non-extractable
CryptoKey(AES-GCM). - P encrypts the secret using the
CryptoKey. - P transmits the encrypted secret to the server and receives a temporary
secret_id. - P writes the
iv,seed, andsecret_idinto C'sIndexedDBstorage via a secure cross-origin mechanism. - C reconstructs the
CryptoKeyusing the localivandseed, then fetches the encrypted secret from the server using thesecret_id. - C decrypts the secret in RAM using the reconstructed
CryptoKey.
The server does not store secrets in persistent storage. Encrypted payloads are held in volatile memory with a 5-minute maximum TTL. If a child origin does not claim the secret within 5 minutes, it is purged automatically.
To write parameters into child storage (Step 6), communication is handled via embedded iframe elements using postMessage. Every incoming message explicitly validates event.origin against an allowed list before execution.
- To compromise a secret, an attacker must simultaneously gain access to the user's local storage/disk and the server's volatile RAM within the 5-minute window.
- Abandoning the flow midway results in server-side secret deletion, rendering localized parameters useless.
When an account is provisioned in LDAP, the user sets their password via the SSO portal.
The authentication payload sent over the wire is an Argon2id hash derived client-side from the user's password using the salt format auth_salt_${username}.
Upon account unlock or key import, a single Argon2id computation processes the master passphrase along with a 16-byte random salt to yield a Master HKDF Key.
Using HKDF (SHA-256), domain-isolated sub-keys are derived instantly without requiring repeated, computationally expensive Argon2id executions:
- PGP Wrapping Key (
info: "pgp-wrapping-key"): AnAES-GCM 256-bitkey used to encrypt the OpenPGP private key at rest (in IndexedDB or server sync storage). - Local Index & Secret AES Key (
info: "aes-key"): AnAES-GCM 256-bitkey used for local mail preview encryption, search indexing, and deterministic secret generation.
To eliminate Known-Plaintext Attack (KPA) vectors and avoid persisting any CryptPad seed to disk:
The CryptPad secret is derived dynamically in memory by encrypting a fixed static string (plugin-cryptpad) using the user's active session key (aesKey), followed by a SHA-256 hashing pass. Because aesKey is unique to each user session context, the derived 256-bit secret is deterministic for the user while remaining unpredictable to external observers. No intermediate seeds or secret tokens are written to disk or sent to the backend database.