The user enters a seed phrase, a benefactor key, and a
beneficiary key.
The benefactor and beneficiary keys are joined — with an
untypeable 0x1F separator between them — to
form a single combined password, which PBKDF2 stretches
into one AES-256 key. Both halves are required to decrypt,
and the order must match. To be clear about what this is:
it is one password split into two parts, not threshold
cryptography or secret-sharing — "neither key alone
decrypts" is a property of any split password. The
separator removes the old ambiguity where
"ab" + "c" and "a" + "bc" would
derive the same key.
Key Derivation:
A random salt (16 bytes) is generated using
window.crypto.getRandomValues().
The combined key and salt are used in the deriveKey()
function, which employs PBKDF2 (Password-Based Key
Derivation Function 2) to derive a 256-bit AES key.
PBKDF2 is configured with:
600,000 iterations
SHA-256 as the hashing algorithm
The resulting AES key is suitable for AES-GCM encryption
(Advanced Encryption Standard - Galois/Counter Mode)
Encryption Process:
A random Initialization Vector (IV) of 12 bytes is
generated using window.crypto.getRandomValues().
The seed phrase is encoded into a Uint8Array using a
TextEncoder.
AES-GCM encryption is performed using
window.crypto.subtle.encrypt():
Algorithm: AES-GCM
Key: Derived AES key from PBKDF2
IV: Randomly generated IV
Plaintext: Encoded seed phrase
Data Formatting (Protocol v2):
Output is a self-describing, versioned binary envelope.
A fixed 35-byte header is packed as:
version (1 byte) = 0x02
kdf_id (1 byte) = 0x01 (PBKDF2-HMAC-SHA256)
iterations (4 bytes, big-endian) — stored so the count can change without a new format
padLen (1 byte) — 0–4 random padding bytes
salt (16 bytes)
iv (12 bytes)
The header is followed by the AES-GCM ciphertext (which
includes the 16-byte auth tag). The header is also passed
as GCM associated data (AAD), so tampering with
the version or stored parameters fails the auth tag.
The whole header || ciphertext blob is
base64url-encoded (URL/filename-safe,
trailing = stripped).
Output:
A single string, prefixed for version detection:
"LE2." + base64url(header || ciphertext)
Decryptors detect the LE2. prefix to read v2;
payloads with no prefix are treated as legacy v1 and stay
decryptable forever.
Summary of Functions:
strToArrayBuffer(str): Converts a string to an
ArrayBuffer.
arrayBufferToStr(buf): Converts an ArrayBuffer to a
string.
generateSalt(): Generates a 16-byte random salt.
deriveKey(password, salt, iterations): Derives a 256-bit
AES key from a password (the combined key) and salt using
PBKDF2; the iteration count is read from the v2 header.
encryptData(plaintext, password): Encrypts the plaintext
(seed phrase) using AES-GCM with a derived key, salt,
and IV. Returns the encrypted data components.
encryptSeedPhrase(): Orchestrates the encryption
process: retrieves user inputs, validates seed phrase,
derives the key, encrypts the seed phrase, and displays
the encrypted output.
Security Considerations:
AES-GCM: This is a modern, authenticated encryption
mode, providing both confidentiality and integrity.
PBKDF2: Using PBKDF2 with a high iteration count
(600,000) strengthens the key derivation process against
brute-force attacks.
Random Salt and IV: Using random salts and IVs is
crucial for the security of the encryption scheme.
Base64 Encoding: Base64 encoding is used for
representing binary data as strings but does not provide
any encryption.
Key Management: The security of this system relies
heavily on the secrecy and strength of the benefactor
and beneficiary keys.
Honest framing of the "dual key": the two keys are simply
the two halves of one password. This is not Shamir secret
sharing, a 2-of-2 multisig, or threshold cryptography —
it provides no cryptographic guarantee beyond "you need
the whole password." Its value is operational (two people
each hold a part, neither can act alone), not a stronger
cipher. If you want on-chain, enforced inheritance,
Bitcoin multisig and timelocks are the right tools; this
is a simpler, offline, off-chain alternative.