| name | file-encryption |
| description | AES-256-GCM file encryption and PBKDF2 key derivation as used in health-records-vault. Use when you need to understand or implement the encryption model, derive a key from a password, encrypt or decrypt a file manually, restore an encrypted backup, or verify the cryptographic integrity of a .enc file. Triggers include "AES-256-GCM", "PBKDF2", "decrypt .enc file", "restore backup", "encryption key derivation", "IV", "salt", or any task about the cryptographic internals of the vault. |
file-encryption
AES-256-GCM encryption model used by health-records-vault. Covers key derivation, encryption, decryption, and manual restoration from a backup archive.
Encryption model overview
master password + salt
|
v
PBKDF2(password, salt, 100000 iterations, SHA-256, 32 bytes)
|
v
256-bit AES key (K)
|
v
IV = randomBytes(12) # unique per file
|
v
AES-256-GCM.encrypt(K, IV, plaintext) -> ciphertext + authTag (16 bytes)
|
v
stored on disk: ciphertext || authTag (.enc file)
stored in DB: IV (hex), salt (hex)
Key points:
- Every file has a unique 12-byte IV and a unique 32-byte salt
- The auth tag provides integrity verification (tampering detection)
- The master password is never stored anywhere
- The derived key exists only in server RAM for the duration of the session
PBKDF2 parameters
| Parameter | Value |
|---|
| Hash | SHA-256 |
| Iterations | 100,000 |
| Key length | 32 bytes (256 bits) |
| Salt length | 32 bytes (256 bits, random per file) |
Encrypting a file (Node.js)
import crypto from 'node:crypto';
import fs from 'node:fs';
function encryptFile(
plaintextPath: string,
outputPath: string,
key: Buffer
): { iv: string; salt: string } {
const iv = crypto.randomBytes(12);
const salt = crypto.randomBytes(32);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const plaintext = fs.readFileSync(plaintextPath);
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const authTag = cipher.getAuthTag();
fs.writeFileSync(outputPath, Buffer.concat([ciphertext, authTag]));
return {
iv: iv.toString('hex'),
salt: salt.toString('hex'),
};
}
Decrypting a file (Node.js)
function decryptFile(
encPath: string,
key: Buffer,
ivHex: string,
saltHex: string
): Buffer {
const iv = Buffer.from(ivHex, 'hex');
const raw = fs.readFileSync(encPath);
const authTag = raw.slice(raw.length - 16);
const ciphertext = raw.slice(0, raw.length - 16);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}
If the auth tag does not match, decipher.final() throws Error: Unsupported state or unable to authenticate data. This indicates tampering or a wrong key.
Deriving a key from password
function deriveKey(password: string, saltHex: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
crypto.pbkdf2(
password,
Buffer.from(saltHex, 'hex'),
100_000,
32,
'sha256',
(err, key) => (err ? reject(err) : resolve(key))
);
});
}
Note: each file stores its own salt in the DB. To decrypt file X, use X's salt to re-derive the key.
Manually restoring from a backup archive
Given a backup .zip archive from /api/backup:
- Extract the archive
- Open
vault-metadata.json to get the list of records with their IV and salt values
- For each record, derive the key using the stored salt and your master password
- Decrypt the .enc file using the derived key and stored IV
node restore.mjs \
--archive vault-backup-2026-03-20.zip \
--password "your-master-password" \
--output ./restored/
Verifying file integrity
A successful AES-256-GCM decryption proves:
- The file was encrypted with a key derived from your password
- The ciphertext has not been tampered with since encryption
- The IV matches the one stored in the database
If decryption throws an authentication error, possible causes are:
- Wrong master password (wrong key derived)
- .enc file has been corrupted or modified
- IV stored in the database does not match the IV used during encryption
Security considerations
- Never log the derived key or decrypted file contents
- Delete the plaintext temp file immediately after encryption (multer temp file)
- Use
crypto.timingSafeEqual for any token comparison to prevent timing attacks
- The session key must not be serialized to a persistent session store (use memory store only)
- In production: set
NODE_ENV=production to enable secure: true on the session cookie
File format of .enc files
+------ ciphertext (variable length) ------+---- auth tag (16 bytes) ----+
| AES-256-GCM encrypted content | GCM authentication tag |
+-------------------------------------------+-----------------------------+
The IV and salt are stored separately in the SQLite database. They are not embedded in the .enc file itself.