Security

age

age encryption: text and files encrypted to people's public keys and opened only with their identity, compatible with the age command.

Version 1.0.0 · 6 functions · 1 crate

#encryption#backups#files#secrets

Functions

What use age.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use age_keypair, age_public_key from age (Quillo 0.6).

age_keypair() → map

A new identity: { identity, public }. identity (AGE-SECRET-KEY-1...) is the secret; public (age1...) is what others encrypt to.

age_public_key(identity_text: str) → stror an error

The public key (age1...) of an identity.

age_encrypt(text: str, recipients: list) → stror an error

text encrypted to each of recipients (age1... keys), as ASCII-armored text (-----BEGIN AGE ENCRYPTED FILE-----): any one of them can open it.

age_decrypt(armored: str, identity: str) → stror an error

The text inside what age_encrypt (or the age -a command) made, opened with identity; an error when it was not for this identity.

age_encrypt_file(input: str, output: str, recipients: list) → intor an error

Encrypts the file at input to recipients into output (binary .age, which the age command opens too); gives the bytes written.

age_decrypt_file(input: str, output: str, identity: str) → intor an error

Decrypts an .age file (binary or armored) with identity into output; gives the bytes written.

Get it

$ quillo install age               # age.qwer into this folder$ quillo install age --example     # and its example, age.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/age.qwer$ curl -O https://quillolang.org/plugins/files/age.qwe

Then, in your program, all of it or only what you call:

main.qwe
use age.qweruse age_keypair, age_public_key from age      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
age = { version = "0.11", features = ["armor"] }

Example

age.qwe
// Nightly backups encrypted to the admins' public keys: the server can write them, never read them.////   quillo run age.qwe      then open http://localhost:8080/backupuse "age.qwer"// Each admin keeps an identity of their own (`age-keygen`) and gives the server only the public key.let ada = age_keypair()let grace = age_keypair()let ADMINS = [ada.public, grace.public]route GET /backup {    write_file("data/backup.sql", "INSERT INTO orders VALUES (1, 'Ada', 120.50);")    let size = age_encrypt_file("data/backup.sql", "data/backup.sql.age", ADMINS)?    age_decrypt_file("data/backup.sql.age", "data/restored.sql", grace.identity)?    { encrypted_bytes: size, restored_by_grace: read_file("data/restored.sql")? }}route GET /note {    let sealed = age_encrypt("The safe's code is 4521.", [ada.public])    {        sealed: sealed?,        ada_reads: age_decrypt(sealed?, ada.identity)?,        grace_cannot: is_err(age_decrypt(sealed?, grace.identity)),    }}
$ quillo run age.qwe$ curl 'localhost:8080/backup'$ curl 'localhost:8080/note'

Source

age.qwer
//! age encryption: text and files encrypted to a person's public key (`age1...`), opened only with their identity (`AGE-SECRET-KEY-1...`), for backups and secrets shared between people.//! version: 1.0.0//!//! [dependencies]//! age = { version = "0.11", features = ["armor"] }use age::secrecy::ExposeSecret;use std::collections::HashMap;use std::io::{Read, Write};use std::str::FromStr;/// Files up to this size are encrypted: 256 MiB.const MAX_FILE: u64 = 256 * 1024 * 1024;fn recipients(list: &[String]) -> Result<Vec<age::x25519::Recipient>, String> {    if list.is_empty() {        return Err("give at least one recipient (an age1... public key)".into());    }    list.iter().take(100).map(|r| age::x25519::Recipient::from_str(r.trim()).map_err(|e| format!("`{}` is not an age public key: {}", r, e))).collect()}fn identity(text: &str) -> Result<age::x25519::Identity, String> {    age::x25519::Identity::from_str(text.trim()).map_err(|e| format!("not an age identity (AGE-SECRET-KEY-1...): {}", e))}fn seal(data: &[u8], to: &[String], armor: bool) -> Result<Vec<u8>, String> {    let rs = recipients(to)?;    let e = age::Encryptor::with_recipients(rs.iter().map(|r| r as &dyn age::Recipient)).map_err(|e| e.to_string())?;    let mut out = Vec::new();    let format = if armor { age::armor::Format::AsciiArmor } else { age::armor::Format::Binary };    let armored = age::armor::ArmoredWriter::wrap_output(&mut out, format).map_err(|e| e.to_string())?;    let mut w = e.wrap_output(armored).map_err(|e| e.to_string())?;    w.write_all(data).map_err(|e| e.to_string())?;    w.finish().and_then(|a| a.finish()).map_err(|e| e.to_string())?;    Ok(out)}fn open(data: &[u8], id: &str) -> Result<Vec<u8>, String> {    let id = identity(id)?;    let d = age::Decryptor::new(age::armor::ArmoredReader::new(data)).map_err(|e| format!("not age data: {}", e))?;    let mut r = d.decrypt(std::iter::once(&id as &dyn age::Identity)).map_err(|e| e.to_string())?;    let mut out = Vec::new();    r.by_ref().take(MAX_FILE + 1).read_to_end(&mut out).map_err(|e| e.to_string())?;    if out.len() as u64 > MAX_FILE {        return Err("decrypted data over 256 MiB".into());    }    Ok(out)}/// A new identity: `{ identity, public }`. `identity` (AGE-SECRET-KEY-1...) is the secret; `public` (age1...) is what others encrypt to.pub fn age_keypair() -> HashMap<String, String> {    let id = age::x25519::Identity::generate();    HashMap::from([        ("identity".to_string(), id.to_string().expose_secret().to_string()),        ("public".to_string(), id.to_public().to_string()),    ])}/// The public key (age1...) of an identity.pub fn age_public_key(identity_text: &str) -> Result<String, String> {    Ok(identity(identity_text)?.to_public().to_string())}/// `text` encrypted to each of `recipients` (age1... keys), as ASCII-armored text (`-----BEGIN AGE ENCRYPTED FILE-----`): any one of them can open it.pub fn age_encrypt(text: &str, recipients: Vec<String>) -> Result<String, String> {    String::from_utf8(seal(text.as_bytes(), &recipients, true)?).map_err(|e| e.to_string())}/// The text inside what age_encrypt (or the `age -a` command) made, opened with `identity`; an error when it was not for this identity.pub fn age_decrypt(armored: &str, identity: &str) -> Result<String, String> {    String::from_utf8(open(armored.as_bytes(), identity)?).map_err(|_| "the decrypted bytes are not text: use age_decrypt_file".to_string())}fn write_out(path: &str, data: &[u8]) -> Result<i64, String> {    if let Some(dir) = std::path::Path::new(path).parent() {        if !dir.as_os_str().is_empty() {            std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?;        }    }    std::fs::write(path, data).map_err(|e| format!("{}: {}", path, e))?;    Ok(data.len() as i64)}fn read_in(path: &str) -> Result<Vec<u8>, String> {    let size = std::fs::metadata(path).map_err(|e| format!("{}: {}", path, e))?.len();    if size > MAX_FILE {        return Err(format!("{}: files up to 256 MiB, this one is {} bytes", path, size));    }    std::fs::read(path).map_err(|e| format!("{}: {}", path, e))}/// Encrypts the file at `input` to `recipients` into `output` (binary .age, which the `age` command opens too); gives the bytes written.pub fn age_encrypt_file(input: &str, output: &str, recipients: Vec<String>) -> Result<i64, String> {    write_out(output, &seal(&read_in(input)?, &recipients, false)?)}/// Decrypts an .age file (binary or armored) with `identity` into `output`; gives the bytes written.pub fn age_decrypt_file(input: &str, output: &str, identity: &str) -> Result<i64, String> {    write_out(output, &open(&read_in(input)?, identity)?)}
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On age: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.