Skip to content
NarDBv0.2.0-dev

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:

KeyWhat it doesWhere
Data keyEncrypts the pages; random 32 bytes, one per databaseIn nardb.toml, only wrapped (encrypted with the master key)
Master keyWraps the data keyIn the system keychain, or derived from a passphrase

The master key is never stored next to the database.

SourceWhen
keychainOne machine, desktop use. Nothing is asked on open. macOS Keychain, Windows Credential Manager, Linux Secret Service.
passphraseWhen 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:

Terminal
>> 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          # unencrypted

In the Go API:

Go
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#

Terminal
>> encrypt_database                 # key in the keychain
>> encrypt_database -e passphrase   # with a passphrase

On 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#

Terminal
>> rotate_key

Every 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.