Security

Crypto box

Public-key encryption between users (libsodium's crypto_box): only the recipient reads it and knows the sender; and anonymous sealed boxes.

Version 1.0.0 · 5 functions · 2 crates

#encryption#messages#privacy#libsodium

Functions

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

cryptobox_keypair() → map

A new key pair: { secret, public }, 32 bytes each as base64. Each user keeps their secret (on their device, ideally) and publishes public.

cryptobox_encrypt(message: str, their_public: str, my_secret: str) → stror an error

message encrypted from the owner of my_secret to the owner of their_public: base64 of the 24-byte nonce, then the box (libsodium's crypto_box_easy with the nonce in front). Only the recipient can open it, and opening it proves the sender.

cryptobox_decrypt(boxed: str, their_public: str, my_secret: str) → stror an error

The message in a box from the owner of their_public, opened with my_secret; an error when it was not sent by them, not for us, or changed.

cryptobox_seal(message: str, their_public: str) → stror an error

message sealed to their_public by nobody in particular (libsodium's crypto_box_seal): a contact form or a tip line whose server cannot read what it stores. The recipient cannot tell who sent it.

cryptobox_unseal(sealed: str, my_secret: str) → stror an error

A sealed box opened with the recipient's my_secret.

Get it

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

Or with curl:

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

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

main.qwe
use crypto_box.qweruse cryptobox_keypair, cryptobox_encrypt from crypto_box      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
crypto_box = { version = "0.9", features = ["seal"] }base64 = "0.22"

Example

crypto_box.qwe
// Private messages between two users: the server stores and forwards boxes it cannot open.////   quillo run crypto_box.qwe      then open http://localhost:8080/messageuse "crypto_box.qwer"// Each user's key pair is made on their own device; the server knows only the public keys.let ada = cryptobox_keypair()let grace = cryptobox_keypair()let mallory = cryptobox_keypair()route GET /message {    let boxed = cryptobox_encrypt("Lunch at 1? The usual place.", grace.public, ada.secret)?    {        stored_by_server: boxed,        grace_reads: cryptobox_decrypt(boxed, ada.public, grace.secret)?,        mallory_cannot: is_err(cryptobox_decrypt(boxed, ada.public, mallory.secret)),        not_from_mallory: is_err(cryptobox_decrypt(boxed, mallory.public, grace.secret)),    }}// An anonymous tip, sealed in the browser (libsodium.js) or here, readable only by the newsroom.route GET /tip {    let sealed = cryptobox_seal("The invoices are in folder 7.", grace.public)?    { opened: cryptobox_unseal(sealed, grace.secret)?, others: is_err(cryptobox_unseal(sealed, ada.secret)) }}
$ quillo run crypto_box.qwe$ curl 'localhost:8080/message'$ curl 'localhost:8080/tip'

Source

crypto_box.qwer
//! Public-key encryption between two users (NaCl/libsodium crypto_box: X25519, XSalsa20-Poly1305): only the recipient can read it, and they know who sent it; plus anonymous sealed boxes.//! version: 1.0.0//!//! [dependencies]//! crypto_box = { version = "0.9", features = ["seal"] }//! base64 = "0.22"use base64::engine::general_purpose::STANDARD;use base64::Engine;use crypto_box::aead::{Aead, AeadCore, OsRng};use crypto_box::{PublicKey, SalsaBox, SecretKey};use std::collections::HashMap;/// Messages up to 16 MiB are boxed.const MAX: usize = 16 * 1024 * 1024;fn key(text: &str, what: &str) -> Result<[u8; 32], String> {    let b = STANDARD.decode(text.trim()).map_err(|_| format!("the {} key is not base64", what))?;    b.try_into().map_err(|_| format!("the {} key is 32 bytes (44 base64 characters)", what))}fn pair(their_public: &str, my_secret: &str) -> Result<SalsaBox, String> {    Ok(SalsaBox::new(&PublicKey::from(key(their_public, "public")?), &SecretKey::from(key(my_secret, "secret")?)))}fn text(bytes: Vec<u8>) -> Result<String, String> {    String::from_utf8(bytes).map_err(|_| "the opened bytes are not text".to_string())}/// A new key pair: `{ secret, public }`, 32 bytes each as base64. Each user keeps their `secret` (on their device, ideally)/// and publishes `public`.pub fn cryptobox_keypair() -> HashMap<String, String> {    let sk = SecretKey::generate(&mut OsRng);    HashMap::from([("secret".into(), STANDARD.encode(sk.to_bytes())), ("public".into(), STANDARD.encode(sk.public_key().as_bytes()))])}/// `message` encrypted from the owner of `my_secret` to the owner of `their_public`: base64 of the 24-byte nonce, then the box/// (libsodium's `crypto_box_easy` with the nonce in front). Only the recipient can open it, and opening it proves the sender.pub fn cryptobox_encrypt(message: &str, their_public: &str, my_secret: &str) -> Result<String, String> {    if message.len() > MAX {        return Err("messages up to 16 MiB".into());    }    let b = pair(their_public, my_secret)?;    let nonce = SalsaBox::generate_nonce(&mut OsRng);    let mut out = nonce.to_vec();    out.extend(b.encrypt(&nonce, message.as_bytes()).map_err(|_| "encryption failed".to_string())?);    Ok(STANDARD.encode(out))}/// The message in a box from the owner of `their_public`, opened with `my_secret`; an error when it was not sent by them, not/// for us, or changed.pub fn cryptobox_decrypt(boxed: &str, their_public: &str, my_secret: &str) -> Result<String, String> {    let data = STANDARD.decode(boxed.trim()).map_err(|_| "not base64".to_string())?;    if data.len() < 24 + 16 {        return Err("too short to be a box".into());    }    let (nonce, body) = data.split_at(24);    let b = pair(their_public, my_secret)?;    text(b.decrypt(nonce.into(), body).map_err(|_| "cannot open: another sender or recipient, or the box was changed".to_string())?)}/// `message` sealed to `their_public` by nobody in particular (libsodium's `crypto_box_seal`): a contact form or a tip line/// whose server cannot read what it stores. The recipient cannot tell who sent it.pub fn cryptobox_seal(message: &str, their_public: &str) -> Result<String, String> {    if message.len() > MAX {        return Err("messages up to 16 MiB".into());    }    let pk = PublicKey::from(key(their_public, "public")?);    Ok(STANDARD.encode(pk.seal(&mut OsRng, message.as_bytes()).map_err(|_| "sealing failed".to_string())?))}/// A sealed box opened with the recipient's `my_secret`.pub fn cryptobox_unseal(sealed: &str, my_secret: &str) -> Result<String, String> {    let data = STANDARD.decode(sealed.trim()).map_err(|_| "not base64".to_string())?;    let sk = SecretKey::from(key(my_secret, "secret")?);    text(sk.unseal(&data).map_err(|_| "cannot open: not sealed to this key, or changed".to_string())?)}
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.