All tests run on an 8-year-old MacBook Air.
When you encrypt a PDF with a password, that password needs to become a 32-byte key.
How you do that conversion matters more than most people realize.
The problem with bcrypt
bcrypt is fine for password hashing. It's not designed for key derivation.
- Output is fixed at 60 characters — not suitable as a raw encryption key
- Memory usage is low, making GPU-based brute force cheap
- No built-in support for generating arbitrary-length keys
PBKDF2 is better but still memory-light. A GPU farm can run billions of iterations per second against it.
Why Argon2id
Argon2id won the Password Hashing Competition in 2015. It's memory-hard by design.
Memory-hard means: to brute force it, you need not just compute but RAM. A GPU with thousands of cores but limited memory per core is suddenly much less useful.
use argon2::{Argon2, Params};
pub fn derive_key(password: &str, salt: &[u8]) -> [u8; 32] {
let mut key = [0u8; 32];
let params = Params::new(
64 * 1024, // 64MB memory
3, // 3 iterations
1, // 1 thread
Some(32), // 32-byte output
).expect("invalid params");
Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params)
.hash_password_into(password.as_bytes(), salt, &mut key)
.expect("key derivation failed");
key
}
64MB of memory per derivation attempt. That makes large-scale GPU attacks expensive.
The salt
Never reuse a salt. Generate a fresh random one per encryption:
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::aead::OsRng;
pub fn generate_salt() -> [u8; 16] {
let mut salt = [0u8; 16];
OsRng.fill_bytes(&mut salt);
salt
}
Store the salt alongside the ciphertext — it's not secret, just needs to be unique.
For new implementations: Argon2id, no exceptions.
bcrypt was designed in 1999. The threat model has changed.
Hiyoko PDF Vault → https://hiyokoko.gumroad.com/l/HiyokoPDFVault
X → @hiyoyok
Top comments (0)