Skip to main content
rustic_core uses AES-256-CTR encryption with Poly1305 authentication to ensure all data stored in repositories is secure and tamper-proof.

Encryption Overview

Every piece of data and metadata in a repository is encrypted before it leaves your machine. The storage backend never sees unencrypted data.

Security Properties

rustic_core’s encryption provides:

Confidentiality

All data encrypted with AES-256

Authenticity

Poly1305 MAC prevents tampering

Integrity

Corrupted data is detected

Forward Secrecy

Each blob uses unique nonce

Encryption Algorithm

rustic_core uses the AES256-CTR-Poly1305-AES construction:
1

AES-256-CTR Encryption

Data is encrypted using AES-256 in Counter (CTR) mode:
  • AES-256: Strong symmetric encryption with 256-bit keys
  • CTR mode: Stream cipher mode allowing random access
  • Performance: Hardware-accelerated on modern CPUs
2

Poly1305-AES Authentication

Encrypted data is authenticated with Poly1305-AES MAC:
  • Poly1305: Fast one-time authentication code
  • AES component: Additional randomness for security
  • Result: 16-byte authentication tag
3

Combined AEAD

This combination provides Authenticated Encryption with Associated Data (AEAD):
  • Encryption and authentication in one step
  • Prevents both eavesdropping and tampering
This is the same encryption construction used by restic, ensuring full compatibility.

The Master Key

At the heart of repository encryption is a 64-byte master key:
pub struct Key(AeadKey);  // 64 bytes total

// Key structure:
// [0..32]   - AES-256 encryption key
// [32..48]  - Poly1305-AES 'k' parameter  
// [48..64]  - Poly1305-AES 'r' parameter

Key Generation

The master key is randomly generated using a cryptographically secure RNG:
use rustic_core::crypto::aespoly1305::Key;

let key = Key::new();  // Generates random 64-byte key

Key Components

The first 32 bytes are used as the AES-256 encryption key.
let (encrypt, k, r) = key.to_keys();
// encrypt: [u8; 32] - AES-256 key

Password-Based Key Derivation

The master key is never stored directly. Instead, it’s encrypted with a key derived from your password using scrypt.

Scrypt Parameters

scrypt is a memory-hard key derivation function designed to resist brute-force attacks:
pub struct KeyFile {
    pub kdf: String,        // "scrypt"
    pub n: u32,            // CPU/memory cost (2^N)
    pub r: u32,            // Block size  
    pub p: u32,            // Parallelization
    pub salt: Vec<u8>,     // Random 64-byte salt
    pub data: Vec<u8>,     // Encrypted master key
}
Default parameters (recommended):
  • N: 2^15 (32768) - Memory cost factor
  • r: 8 - Block size
  • p: 1 - Parallelization
scrypt with these parameters is intentionally slow (~100ms) to make password guessing expensive. Don’t reduce these values!

Key Derivation Process

1

Generate Salt

Create a random 64-byte salt:
let mut salt = vec![0; 64];
rng().fill_bytes(&mut salt);
2

Derive Key

Use scrypt to derive a 64-byte key from the password:
let params = Params::recommended();
let mut derived_key = [0; 64];
scrypt::scrypt(password, &salt, &params, &mut derived_key)?;
3

Encrypt Master Key

Encrypt the master key with the derived key:
let kdf_key = Key::from_slice(&derived_key);
let encrypted_data = kdf_key.encrypt_data(&master_key_json)?;

Multiple Passwords

A repository can have multiple key files, each with a different password:
// Add a new password
let new_key_id = repo.add_key("new-password", &key_opts)?;

// Each key file contains the same master key, encrypted differently
This allows:
  • Different passwords for different team members
  • Password rotation without re-encrypting data
  • Emergency access keys

Encryption in Practice

Encrypting Data

When saving data to the repository:
use rustic_core::crypto::CryptoKey;

// The encryption process
let encrypted = key.encrypt_data(&plaintext)?;

// Encrypted format:
// [0..16]   - Random nonce
// [16..N-16] - Ciphertext  
// [N-16..N] - Poly1305 MAC tag
Each encryption uses a unique random nonce (number used once):
let mut nonce = Nonce::default();  // 16 bytes
rng().fill_bytes(&mut nonce);

Decrypting Data

When reading from the repository:
// Extract nonce and verify MAC
let nonce = &encrypted[0..16];
let ciphertext = &encrypted[16..];

let plaintext = key.decrypt_data(&encrypted)?;
// Returns error if MAC verification fails
If the MAC check fails, the data has been corrupted or tampered with. rustic_core will reject it.

What Gets Encrypted?

rustic_core encrypts everything except key files themselves:
Always encrypted:
  • Config file
  • Snapshot metadata
  • Tree blobs (directory structure)
  • Data blobs (file contents)
  • Index files
impl RepoFile for ConfigFile {
    const ENCRYPTED: bool = true;
}

impl RepoFile for SnapshotFile {
    const ENCRYPTED: bool = true;
}

Content-Addressed Encryption

Encryption interacts with deduplication:
  1. Plaintext chunking: Files are split into chunks based on content
  2. Deterministic IDs: Each chunk’s ID is the SHA-256 hash of plaintext
  3. Encrypted storage: Chunks are encrypted before storage
  4. Deduplication preserved: Identical chunks have same ID despite different ciphertexts
// Same content always generates same ID
let chunk_id = hash(&plaintext_chunk);  // SHA-256

// But different encrypted output (different nonces)
let encrypted1 = key.encrypt_data(&chunk);
let encrypted2 = key.encrypt_data(&chunk);
assert_ne!(encrypted1, encrypted2);  // Different ciphertexts

// Deduplication still works via content addressing
assert_eq!(hash(&chunk), hash(&chunk));  // Same ID

Security Considerations

Your password is the weakest link. Use a strong, unique password:
  • Minimum: 16+ characters
  • Recommended: Random passphrase or password manager
  • Avoid: Dictionary words, personal information
scrypt makes brute-forcing expensive, but can’t protect weak passwords.
The master key exists in memory during operations:
// Key is stored in repository state
pub(crate) fn dbe(&self) -> &DecryptBackend<Key> {
    &self.status.open_status().dbe
}
Security measures:
  • Keys are never written to disk unencrypted
  • Use encrypted swap or disable swap for maximum security
  • Clear sensitive data from memory when done
While all data is encrypted, some metadata is visible:Encrypted:
  • File names and paths
  • File contents
  • Directory structure
Visible:
  • Repository structure (config, keys, snapshots exist)
  • Pack file sizes
  • Number of snapshots
  • Approximate repository size
This is inherent to content-addressed storage.

Working with Keys

Opening with Password

use rustic_core::repository::Credentials;

let credentials = Credentials::Password("my-password".to_string());
let repo = repo.open(&credentials)?;

Opening with Master Key

For automation, you can use the master key directly:
use rustic_core::repofile::MasterKey;

// Export master key once
let master_key = repo.key();
let master_key_json = serde_json::to_string(&master_key)?;

// Later, open without password
let credentials = Credentials::Masterkey(master_key);
let repo = repo.open(&credentials)?;
Master keys provide full access without password. Store them securely (e.g., encrypted secrets management).

Key Management

// List keys
let keys: Vec<KeyId> = repo.list()?;

// Add new key
let key_id = repo.add_key("new-password", &key_opts)?;

// Delete key (cannot delete currently used key)
repo.delete_key(&key_id)?;

See Also

Repository

Repository structure and lifecycle

Deduplication

How encryption interacts with deduplication

Snapshots

What snapshot metadata is encrypted

Backends

Where encrypted data is stored

Build docs developers (and LLMs) love