Claude
Skills
Sign in
โ† Back

age-encryption

Included with Lifetime
$97 forever

age โ€” modern file encryption format and tool by Filippo Valsorda. Replaces GPG for most use cases (encrypted backups, exports, secrets in CI). Covers age CLI, X25519 + Scrypt-based recipients, SSH key recipients, plugin system (YubiKey, Secure Enclave, age-keyring), Rust (`age` crate), Go (filippo.io/age), encrypted backup workflows for wallets. USE WHEN: user mentions "age", "age-encryption", "rage", "filippo.io/age", "ssh-rsa age", "age plugin", "age-yubikey", "age recipient", "age identity", ".age file", "age-keygen" DO NOT USE FOR: SQLite encryption - use `databases/sqlcipher` DO NOT USE FOR: General crypto primitives - use `security/libsodium` DO NOT USE FOR: GPG-specific workflows - use GPG-specific tooling DO NOT USE FOR: Real-time stream encryption - use `security/libsodium` (secretstream)

Backend & APIs

What this skill does

# age (and rage) โ€” Modern File Encryption

> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `age`.

## What age Is

`age` (pronounced "ah-gay" ๐Ÿฆ—) is a simple, modern, secure file encryption tool. Spec at https://age-encryption.org. Implementations:
- **Go reference** โ€” `filippo.io/age` (CLI: `age`)
- **Rust port** โ€” `str4d/rage` (CLI: `rage`, `rage-keygen`)
- **JS** โ€” `age-encryption` npm package

**Properties**:
- One-line API for encrypt/decrypt
- X25519-based public-key recipients (32-byte short string `age1...`)
- Scrypt-based passphrase recipients
- SSH key recipients (Ed25519 and RSA) โ€” encrypt to existing GitHub `~/.ssh/authorized_keys`
- Plugin system: YubiKey, Secure Enclave, TPM, age-keyring (passwordstore)
- Streaming format (constant memory, supports very large files)
- Authenticated encryption (ChaCha20-Poly1305 chunks + HMAC)

For BHODL-style wallets: ideal for **encrypted backups** (seed + descriptors + labels exported as a single `.age` file).

## CLI Quick Start

### Generate keypair

```bash
age-keygen -o key.txt
# Public key: age1qz5j...
```

`key.txt` contains:
```
# created: 2026-05-04T10:00:00Z
# public key: age1qz5jksw7g7e9q...
AGE-SECRET-KEY-1XYZ...
```

### Encrypt (one or more recipients)

```bash
# To public key
age -r age1qz5jksw7g7e9q... -o secret.age secret.txt

# To passphrase (Scrypt-derived)
age -p -o secret.age secret.txt
# Enter passphrase: ***

# Multiple recipients (any can decrypt)
age -r age1abc... -r age1def... -r ssh-ed25519... -o backup.age backup.tar
```

### Decrypt

```bash
age -d -i key.txt -o secret.txt secret.age

# Passphrase
age -d -o secret.txt secret.age
# Enter passphrase: ***
```

### Pipe usage (UNIX-friendly)

```bash
tar czf - ./wallet | age -r age1abc... > backup.age

age -d -i key.txt backup.age | tar xzf -
```

## SSH Recipients

age can encrypt directly to existing SSH public keys (Ed25519 or RSA):

```bash
# To one specific SSH key
age -R ~/.ssh/id_ed25519.pub -o file.age file.txt

# To everyone in your GitHub authorized keys
curl https://github.com/USERNAME.keys | age -R - -o file.age file.txt

# Decrypt with corresponding SSH private key
age -d -i ~/.ssh/id_ed25519 file.age
```

Useful for sharing secrets with collaborators without setting up new keys.

## Plugins (YubiKey, Secure Enclave, TPM)

age plugins handle non-software identities. Install plugin โ†’ use as identity/recipient with prefix.

### age-yubikey

```bash
brew install age-plugin-yubikey

age-plugin-yubikey                              # interactive setup
# Generates identity bound to YubiKey, prints recipient: age1yubikey1...

age -r age1yubikey1abc... -o secret.age secret.txt
age -d -i ~/.config/age/yubikey.txt secret.age  # touches YubiKey for confirm
```

### age-plugin-se (Apple Secure Enclave)

```bash
brew install age-plugin-se

age-plugin-se keygen --access-control=any-biometry-or-passcode -o se-key.txt
# Emits: age1se1abc...

age -r age1se1abc... -o secret.age secret.txt
age -d -i se-key.txt secret.age                # prompts Touch/FaceID
```

For BHODL desktop wallet companion: encrypt a seed backup that **only your YubiKey can decrypt** โ€” no passphrase to forget.

### age-plugin-tpm

```bash
age-plugin-tpm                                  # bind to system TPM
```

## File Format (High Level)

```
age-encryption.org/v1
-> X25519 KEY_AGREEMENT...
WRAPPED_FILE_KEY...
-> X25519 KEY_AGREEMENT...
WRAPPED_FILE_KEY...
-> scrypt SALT WORK_FACTOR
WRAPPED_FILE_KEY...
--- HEADER_HMAC

[binary ChaCha20-Poly1305 chunks]
```

Each recipient gets its own wrapped file key. The body is encrypted once with a random 16-byte file key, split into 64KB authenticated chunks. Streaming-friendly: decrypt without buffering whole file.

## Rust โ€” `age` crate

```toml
[dependencies]
age = "0.10"
```

```rust
use age::{Encryptor, Decryptor, x25519};
use std::io::{Read, Write};

fn encrypt_to_recipient(plaintext: &[u8], recipient: &x25519::Recipient) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let encryptor = Encryptor::with_recipients(vec![Box::new(recipient.clone())])
        .ok_or("no recipients")?;

    let mut encrypted = vec![];
    let mut writer = encryptor.wrap_output(&mut encrypted)?;
    writer.write_all(plaintext)?;
    writer.finish()?;

    Ok(encrypted)
}

fn decrypt_with_identity(ciphertext: &[u8], identity: &x25519::Identity) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let decryptor = match Decryptor::new(ciphertext)? {
        Decryptor::Recipients(d) => d,
        _ => return Err("not a recipient-encrypted file".into()),
    };

    let mut decrypted = vec![];
    let mut reader = decryptor.decrypt(std::iter::once(identity as &dyn age::Identity))?;
    reader.read_to_end(&mut decrypted)?;

    Ok(decrypted)
}

fn generate_identity() -> x25519::Identity {
    x25519::Identity::generate()
    // identity.to_public() returns Recipient
}
```

## Streaming (Large Files)

```rust
use age::stream::StreamWriter;
use std::fs::File;
use std::io::BufReader;

let recipient: x25519::Recipient = "age1abc...".parse()?;
let encryptor = Encryptor::with_recipients(vec![Box::new(recipient)]).unwrap();

let input = BufReader::new(File::open("backup.tar")?);
let output = File::create("backup.age")?;
let mut writer = encryptor.wrap_output(output)?;

std::io::copy(&mut input, &mut writer)?;
writer.finish()?;
```

Constant memory โ€” works for multi-GB files.

## Passphrase Encryption

```rust
use age::scrypt;
use secrecy::Secret;

let passphrase = Secret::new("correct horse".to_owned());
let recipient = scrypt::Recipient::new(passphrase.clone());

let encryptor = Encryptor::with_recipients(vec![Box::new(recipient)]).unwrap();
// ... encrypt as above

// Decrypt
let decryptor = match Decryptor::new(&ciphertext[..])? {
    Decryptor::Passphrase(d) => d,
    _ => return Err("not passphrase-encrypted".into()),
};
let mut reader = decryptor.decrypt(&passphrase, None)?;
```

`work_factor` parameter (Scrypt N) defaults to 18 (~1s on phone). Increase for higher-value secrets.

## SSH Key Identities

```rust
use age::ssh;

let recipient: ssh::Recipient = "ssh-ed25519 AAAA...".parse()?;
// or read from file:
let recipient = ssh::Recipient::from_pubkey_file("/path/to/key.pub")?;

// Identities (private keys)
let identity = ssh::Identity::from_buffer(BufReader::new(File::open("~/.ssh/id_ed25519")?), Some("comment"))?;
```

## Wallet Backup Pattern

For BHODL: export full wallet (seed, descriptors, BIP329 labels, transaction metadata) as a single age-encrypted blob.

```rust
use age::{Encryptor, x25519};
use serde::Serialize;
use std::io::Write;

#[derive(Serialize)]
struct WalletBackup {
    version: u32,
    seed_phrase: String,
    descriptors: Vec<String>,
    labels: serde_json::Value,        // BIP329
    metadata: BackupMetadata,
}

fn create_backup(
    wallet: &Wallet,
    recipients: Vec<Box<dyn age::Recipient + Send + 'static>>,
) -> Result<Vec<u8>> {
    let backup = WalletBackup {
        version: 1,
        seed_phrase: wallet.seed_phrase()?,
        descriptors: wallet.descriptors(),
        labels: wallet.export_labels()?,
        metadata: BackupMetadata::now(),
    };
    let json = serde_json::to_vec(&backup)?;

    let encryptor = Encryptor::with_recipients(recipients)
        .ok_or("no recipients")?;

    let mut encrypted = vec![];
    let mut writer = encryptor.wrap_output(&mut encrypted)?;
    writer.write_all(&json)?;
    writer.finish()?;

    Ok(encrypted)
}

// Multi-recipient backup: passphrase + YubiKey + companion's age key
let recipients: Vec<Box<dyn age::Recipient + Send + 'static>> = vec![
    Box::new(scrypt::Recipient::new(passphrase)),
    Box::new("age1yubikey1abc...".parse::<YubiKeyRecipient>()?),
    Box::new("age1xyz...".parse::<x25519::Recipient>()?),
];

let backup_bytes = create_backup(&wallet, recipients)?;
std::fs::write("bhodl-backup.age", backup_bytes)?;
```

User can decrypt with **any one** of: their passphrase, their YubiKey, or their companion's key. Defense in 

Related in Backend & APIs