# 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 encryption** — `ChaCha20-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 hashing** — `hash_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-aware** — `encrypt_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
```rust
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:
```rust,no_run
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:
```text
[ version (1 B) | nonce (12 B) | ciphertext (N B) | tag (16 B) ]
```
Streaming:
```text
[ 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.