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
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() → mapA 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 errorThe public key (age1...) of an identity.
age_encrypt(text: str, recipients: list) → stror an errortext 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 errorThe 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 errorEncrypts 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 errorDecrypts 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/age.qwer$ curl -O https://quillolang.org/plugins/files/age.qweThen, in your program, all of it or only what you call:
use age.qweruse age_keypair, age_public_key from age // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
age = { version = "0.11", features = ["armor"] }Example
// 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 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)?)}Related
encrypt.qwer · 1.0.0Encrypt and decrypt text and files with a secret key (AES-256-GCM): API tokens, notes and uploads kept unreadable at rest.7 views · 7 downloadsShamir shares shamir.qwer · 1.0.0Shamir's secret sharing: a secret split into N shares, any K of which bring it back, for recovery keys held by several people.7 views · 7 downloadsGzip gzip.qwer · 1.0.0Gzip files and text, and unpack them: smaller backups, exports and payloads.7 views · 6 downloadsbzip2 bzip2.qwer · 1.0.0bzip2 compression of files and text in pure Rust, .bz2 dumps unpacked, and their first lines read without unpacking them all.6 views · 7 downloadsTar tar.qwer · 1.0.0Tar archives, gzipped or not: made from files and folders, listed, one file read, and opened safely into a folder.7 views · 8 downloadsxz xz.qwer · 1.0.0xz (LZMA2) compression of files and text, and old .lzma files read: the smallest archives, for backups and downloads.6 views · 8 downloadsThread 0 posts
On age: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.