End-to-End Encryption in a Password Manager: Implementation Deep Dive
How I implemented E2E encryption, TOTP, and zero-trust mode in Passman — the cryptographic choices and why they matter.
The Core Principle: Zero Trust
In Passman, the server never sees your plaintext passwords. Ever. All encryption and decryption happens in the browser. The server stores only ciphertext — even if the database is compromised, your passwords are safe.
Key Derivation
The master password never leaves your device. We derive an encryption key from it using Argon2id:
import argon2
def derive_key(master_password: str, salt: bytes) -> bytes:
return argon2.low_level.hash_secret_raw(
secret=master_password.encode(),
salt=salt,
time_cost=3,
memory_cost=65536, # 64MB
parallelism=4,
hash_len=32,
type=argon2.low_level.Type.ID,
)
Argon2id is memory-hard, making brute-force attacks expensive even with GPUs.
Vault Encryption
Each vault entry is encrypted with AES-256-GCM using the derived key:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def encrypt_entry(key: bytes, plaintext: str) -> dict:
aesgcm = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
return {
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(ciphertext).decode(),
}
GCM mode gives us both confidentiality and integrity — if anyone tampers with the ciphertext, decryption fails.
TOTP Implementation
For MFA, we use TOTP (RFC 6238). The secret is stored encrypted in the vault:
import pyotp
def generate_totp_secret() -> str:
return pyotp.random_base32()
def verify_totp(secret: str, token: str) -> bool:
totp = pyotp.TOTP(secret)
return totp.verify(token, valid_window=1) # ±30s tolerance
The 40 Recovery Keys
If you lose your master password and MFA device, you're locked out. Recovery keys solve this. On account creation, we generate 40 random keys:
def generate_recovery_keys() -> list[str]:
return [
"-".join([secrets.token_hex(4).upper() for _ in range(4)])
for _ in range(40)
]
Each key is hashed with bcrypt before storage. To recover, you provide one key — it's verified, then invalidated. You get 40 chances.
Zero-Trust Mode
In zero-trust mode, the session token is never stored in localStorage or cookies — only in memory. Closing the tab logs you out. This prevents XSS attacks from stealing your session.
What I'd Do Differently
Use WebCrypto API for all crypto operations in the browser instead of sending anything to the server. The current architecture is secure, but a pure client-side crypto approach would be even cleaner.