How Vaultwarden Handles Data Encryption at Rest: Server-Side Cryptographic Implementation
Vaultwarden does not encrypt user vault data on the server—ciphertext arrives pre-encrypted from Bitwarden clients and is stored verbatim, while the server only protects authentication credentials using PBKDF2-HMAC-SHA256 for passwords and Argon2id for admin tokens via the ring and argon2 Rust crates.
Vaultwarden implements a zero-knowledge architecture where encryption at rest is primarily handled by Bitwarden clients before data ever reaches the server. In this open-source Rust implementation (dani-garcia/vaultwarden), the server stores only ciphertext for vault items, ciphers, and attachments, focusing its cryptographic operations on securing authentication secrets and generating cryptographically secure random tokens.
The Zero-Knowledge Architecture: Client-Side Encryption
Vaultwarden stores user data—including passwords, secure notes, attachments, and organizational secrets—as opaque encrypted blobs received directly from Bitwarden client applications. The server never possesses the plaintext encryption keys for this data, ensuring that even with full database access, vault contents remain inaccessible without the user's master password.
Master Key (aKey) Storage Model
The per-user master encryption key, referred to as the aKey, is stored in the users database table as a plain string (users.akey), but arrives at the server already encrypted by the client. When a user creates or changes their master password, the Bitwarden client encrypts the aKey using the new password-derived key before transmission. In src/db/models/user.rs, lines 44–48, the server simply persists this encrypted value without modification:
// src/db/models/user.rs
pub fn set_password(&mut self, new_key: String, new_hash: String, new_iterations: i32) {
self.akey = new_key; // Already encrypted by client
self.password_hash = new_hash;
self.password_iterations = new_iterations;
}
Database Storage of Encrypted Artifacts
All vault entities follow the same pattern: API keys, send keys, attachment keys, and organization keys arrive as encrypted values or randomly generated keys from the client, and Vaultwarden stores them unchanged. For example, in src/db/models/send.rs, lines 26–31, the Send model stores the encrypted key directly without server-side cryptographic transformation.
Server-Side Cryptographic Primitives
While Vaultwarden does not encrypt vault data itself, it implements several cryptographic primitives to protect authentication credentials and generate secure tokens.
PBKDF2-HMAC-SHA256 for Password Verification
Vaultwarden protects user password hashes using PBKDF2-HMAC-SHA256 with a per-user random salt and configurable iterations (defaulting to 600,000). The implementation in src/crypto.rs uses the ring crate for key derivation:
// src/crypto.rs
pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
let mut out = vec![0u8; OUTPUT_LEN];
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out);
out
}
pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool {
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok()
}
During authentication, src/db/models/user.rs (lines 56–62) invokes hash_password with the submitted password and stored salt to verify credentials without ever storing the plaintext password.
Argon2id for Administrative Token Hashing
Administrative tokens and CLI-generated hashes use Argon2id (version 0x13) with a memory-hard configuration to resist GPU and ASIC attacks. The default parameters allocate 65,540 KB of memory with 3 passes and 4 threads, implemented in src/main.rs:
// src/main.rs ── "hash" subcommand
let argon2 = Argon2::new(Argon2id, V0x13, argon2_params.build().unwrap());
let salt = SaltString::encode_b64(&crypto::get_random_bytes::<32>()).unwrap();
let password_hash = argon2.hash_password(password.as_bytes(), &salt)?;
println!("ADMIN_TOKEN='{}'", password_hash);
Cryptographically Secure Random Generation
Vaultwarden generates API keys, email tokens, and file IDs using ring::rand::SystemRandom for cryptographically secure random bytes, subsequently encoded via hex or base64. The generate_api_key function in src/crypto.rs produces 30-character alphanumeric strings (approximately 178 bits of entropy):
// src/crypto.rs
pub fn generate_api_key() -> String {
get_random_string_alphanum(30)
}
pub fn get_random_bytes<const N: usize>() -> [u8; N] {
let mut bytes = [0u8; N];
SystemRandom::new().fill(&mut bytes).expect("Failed to generate random bytes");
bytes
}
Constant-Time Comparison for Timing Attack Prevention
To prevent timing attacks during API key verification and HMAC validation, Vaultwarden uses constant-time equality checks via the subtle crate. The ct_eq function in src/crypto.rs compares secrets without leaking timing information:
// src/crypto.rs
pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {
use subtle::ConstantTimeEq;
a.as_ref().ct_eq(b.as_ref()).into()
}
This function protects sensitive comparisons in src/api/core/two_factor/email.rs (line 223) and other authentication flows.
Critical Source Files for Cryptographic Operations
| File Path | Cryptographic Responsibility |
|---|---|
src/crypto.rs |
Core PBKDF2-HMAC-SHA256 password hashing, HMAC-SHA1 signatures, secure random generation, and constant-time comparison |
src/main.rs |
Argon2id implementation for the hash CLI command and admin token generation |
src/db/models/user.rs |
Storage and retrieval of encrypted master keys (aKey) and password hash verification |
src/db/models/send.rs |
Example of encrypted blob storage for secure sends |
Cargo.toml |
Dependency specifications for ring, argon2, subtle, rand, and data-encoding |
Summary
- Vaultwarden implements zero-knowledge encryption where clients encrypt vault data before transmission, leaving the server to store only ciphertext.
- The server protects user credentials using PBKDF2-HMAC-SHA256 with 600,000 default iterations and per-user salts.
- Administrative secrets use Argon2id (v0x13) with 65,540 KB memory hardness for GPU-resistant hashing.
- Cryptographic operations rely on audited Rust crates:
ringfor PBKDF2 and random generation,argon2for admin tokens, andsubtlefor constant-time comparisons. - The master encryption key (aKey) is stored in the database only after client-side encryption with the user's password-derived key.
Frequently Asked Questions
Does Vaultwarden encrypt my passwords and vault data on the server?
No. Vaultwarden stores vault data exactly as received from Bitwarden clients—already encrypted using AES-256-CBC or AES-256-GCM. The server never sees the plaintext or the master decryption key, ensuring zero-knowledge architecture.
What encryption algorithms does Vaultwarden use for authentication?
Vaultwarden uses PBKDF2-HMAC-SHA256 for deriving and verifying user password hashes with a default of 600,000 iterations, and Argon2id (version 0x13) for hashing administrative tokens with memory-hard parameters (65,540 KB, 3 passes, 4 threads).
Is the master key stored securely in the Vaultwarden database?
The master key (aKey) is stored as a string in the users table, but it arrives from the client already encrypted with the user's master password. Vaultwarden never stores the master key in plaintext, nor does it perform server-side encryption of this value.
Which cryptographic libraries does Vaultwarden depend on?
Vaultwarden uses the ring crate for PBKDF2, HMAC, and secure random number generation, the argon2 crate for password hashing, the subtle crate for constant-time equality comparisons, and data-encoding for hex and base64 operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →