nahui 0.2.0

Authenticated encryption (ChaCha20-Poly1305) and BLAKE3 content hashing for document attestation.
Documentation

nahui

Authenticated encryption (ChaCha20-Poly1305) and BLAKE3 content hashing for document attestation.

nahui is a small, focused crypto crate built on top of [chacha20poly1305] and [blake3]. It exposes a Vault type that handles single-shot and streaming encryption, AAD binding, and a wire format that's versioned so breaking changes can't silently corrupt old blobs.

It runs natively and on wasm32-unknown-unknown (browser).

Features

  • AEAD encryptionChaCha20-Poly1305, both single-shot (in-memory) and streaming (IETF STREAM construction, no Seek required on decrypt).
  • AAD on every API — bind a ciphertext to its context (filename, user id, doc version) so an attacker can't swap one valid blob for another.
  • Versioned wire format — every blob starts with a version byte, and the version is bound into the AEAD tag so cross-version confusion is impossible.
  • Plaintext hashinghash_bytes, hash_stream, and a one-pass hash_and_encrypt_stream that returns the BLAKE3 hash of the plaintext (suitable for an attestation note).
  • Allocation-awareencrypt_into / decrypt_into reuse a caller's Vec so the hot path runs allocation-free once warmed up.
  • Plaintext scrubbing on failure — if AEAD verification fails, the unauthenticated keystream-XOR plaintext is zeroed before returning.
  • #![forbid(unsafe_code)] in the library.
  • Zeroization — master key and derived cipher state are wiped on drop.

Usage

use nahui::crypto::Vault;

// Generate a fresh random key. Use `Vault::new(key)` if you already have
// one (e.g. derived from a password via Argon2 or fetched from a KMS).
let vault = Vault::generate().expect("OS RNG must be available");

// Single-shot, with AAD binding the ciphertext to a filename.
let ct = vault.encrypt(b"hello", b"file:notes.txt").unwrap();
let pt = vault.decrypt(&ct, b"file:notes.txt").unwrap();
assert_eq!(pt, b"hello");

Streaming + plaintext hash in one pass:

use nahui::crypto::Vault;
use std::fs::File;
use std::io::BufWriter;

let vault = Vault::generate().unwrap();
let mut input = File::open("notes.txt").unwrap();
let output = BufWriter::new(File::create("notes.txt.enc").unwrap());
let hash = vault
    .hash_and_encrypt_stream(&mut input, &mut { output }, b"file:notes.txt")
    .unwrap();
println!("BLAKE3({{plaintext}}) = {}", hash.to_hex());

Wire format

Single-shot:

[ version (1 B) | nonce (12 B) | ciphertext (N B) | tag (16 B) ]

Streaming:

[ version (1 B) | header_nonce (7 B) | chunk_0 | chunk_1 | ... | chunk_final ]

Each streaming chunk carries its own 16-byte Poly1305 tag, and STREAM's "last block" flag prevents truncation attacks.

Status

0.x — the API and wire format are stable enough that we KAT-test them, but they may evolve. Expect a 1.0 once the dust settles. The current VERSION constant is 0x01.

License

MIT OR Apache-2.0, at your option.