Encryption
Encryption at rest, where the key lives, changing the passphrase and rotating the key.
In an encrypted database every page is encrypted before it reaches the disk: catalog.db, indexes.db, the partition files and the write-ahead log (wal.log). Someone who copies the files cannot read the rows, the user names, the password hashes or the schema names.
NarDB Shell creates new databases encrypted by default.
Where the key lives#
There are two layers:
| Key | What it does | Where |
|---|---|---|
| Data key | Encrypts the pages; random 32 bytes, one per database | In nardb.toml, only wrapped (encrypted with the master key) |
| Master key | Wraps the data key | In the system keychain, or derived from a passphrase |
The master key is never stored next to the database.
| Source | When |
|---|---|
keychain | One machine, desktop use. Nothing is asked on open. macOS Keychain, Windows Credential Manager, Linux Secret Service. |
passphrase | When the database moves to another machine, or the key should not be stored anywhere. Asked on every open. At least 12 characters; the master key is derived with Argon2id. |
Creating#
In the shell, the File → New ▸ Database... dialog asks for the encryption option. On the command line:
>> create_local_database -f ./data/shop.ndbc # key in the keychain (default)
>> create_local_database -f ./data/shop.ndbc -e passphrase # asks for a passphrase
>> create_local_database -f ./data/test.ndbc -e none # unencryptedIn the Go API:
import "nardb/keychain"
db, err := nardb.Open("./data/shop.ndbc", nardb.Options{
Encryption: nardb.EncryptKeychain,
Keychain: keychain.System{},
})Opening a passphrase-encrypted database without a passphrase returns nardb.ErrPassphraseRequired; a wrong one returns nardb.ErrWrongPassphrase. The shell asks for the passphrase in the first case.
Encrypting an existing database#
>> encrypt_database # key in the keychain
>> encrypt_database -e passphrase # with a passphraseOn an unencrypted database this copies all data into a new, encrypted container and swaps it in; the old files are deleted. In Go: nardb.Reencrypt(path, from, to).
Changing the passphrase or the key source#
On a database that is already encrypted, encrypt_database only changes the master key: a new passphrase, a move to the keychain or back. The data key is wrapped again; pages are not re-encrypted, so it finishes instantly. In Go: db.ChangeKey(opts).
Every key generation is a separate keychain entry. The new entry is written first, then nardb.toml, and only then is the old entry deleted. If something is interrupted in between, nardb.toml still points to the old, valid entry and the database is not locked out.
Rotating the data key#
>> rotate_keyEvery page is rewritten with a new data key; the key source stays the same. Use it when a key may have leaked. The work happens on a copy, and if it is interrupted it is completed on the next open.
What it protects against, and what it does not#
Protects against:
- copying the files, the disk or the backups;
- changing a page or moving it somewhere else: every page is bound to its file and position, so the read fails;
- copying the wrapped key into another database: it will not open.
Does not protect against:
- an attacker on the machine while the database is open: decrypted pages are in memory;
- replacing a page with its own older encrypted version (rollback); catching that needs a structure that tracks page versions;
- file headers (page count, version) and the number of files are visible; contents are not.
Technical details#
- The algorithm is XChaCha20-Poly1305 (
golang.org/x/crypto). Every write uses a fresh random 24-byte nonce; the authentication tag is 16 bytes. - 40 bytes of each 4 KiB page go to the nonce and tag; tables see 4056-byte pages.
- Additional authenticated data: container id, file path, page number.
- The cache holds decrypted pages, so each page is decrypted once.