A small, misuse-resistant two-party authenticated channel in Rust. AES-256-GCM, HKDF-SHA-256, symmetric ratchet, zeroized state, no
unsafe.
Use it when you have two participants, a way to share a 32-byte secret over a separate secure channel, and you need an authenticated, ordered, replay-resistant message stream.
Don't use it when you need key exchange, group messaging, identity, multi-device sync, or out-of-order delivery. See Limitations.
- Quickstart
- Install
- API at a glance
- Security model
- Threat model
- Limitations & non-goals
- Performance & reliability
- Testing & CI
- Safe-usage checklist
- Versioning & wire format
- License
use cripto_endevs_comunity::{CryptoNugget, MasterSeed, Role};
// 1. One side generates the seed.
let seed = MasterSeed::generate();
// 2. Transfer the seed to the other side over a SECURE, EPHEMERAL channel.
// The token below is equivalent to the root key — never log it.
let token = seed.export_for_transfer();
let seed_peer = MasterSeed::from_transfer_token(&token).unwrap();
// 3. Each side instantiates with its explicit role.
let mut alice = CryptoNugget::new(&seed, Role::Initiator);
let mut bob = CryptoNugget::new(&seed_peer, Role::Responder);
// 4. Send.
let pkt = alice.cifrar("hello bob").unwrap();
// 5. Receive — keys auto-ratchet on success.
assert_eq!(bob.descifrar(&pkt).unwrap(), "hello bob");Run the bundled demo:
cargo run --example demo[dependencies]
cripto_endevs_comunity = "0.2.0"Requirements:
- Rust 1.85+ stable (edition 2024 requires 1.85 or newer).
- Standard library (
std). This crate is notno_std-compatible. - No optional features; the default build is the supported configuration.
| Type | Purpose |
|---|---|
MasterSeed |
256-bit root secret. Zeroized on drop. Safe Debug (redacted). |
Role |
Initiator or Responder. Replaces fragile booleans. |
CryptoNugget |
Per-peer state. Zeroized on drop. Safe Debug. |
Packet |
Typed encrypted packet: parse/serialize bytes or base64 and inspect version/flags/sequence. |
Mode / Builder |
Explicitly select ordered or stateless-envelope semantics. |
StatelessEnvelope |
Encrypt independent blobs without channel state or replay tracking. |
ReplayWindowChannel |
Accept out-of-order packets within a bounded anti-replay window. |
Error |
Typed errors: tampering, replay, version, format. |
| Call | What it does |
|---|---|
MasterSeed::generate() |
New seed from OsRng. |
MasterSeed::from_transfer_token(&str) |
Import a seed shared via secure channel. |
seed.export_for_transfer() |
Export the seed as a base64 token (handle as secret). |
CryptoNugget::new(&seed, role) |
Default context. |
CryptoNugget::new_with_context(&seed, role, b"app/v1") |
Domain-separated context (recommended in real apps). |
nug.cifrar(&str) / nug.cifrar_bytes(&[u8]) |
Encrypt + ratchet TX. Returns base64 wire-v2 packet. |
nug.descifrar(&str) / nug.descifrar_bytes(&str) |
Decrypt + ratchet RX. Accepts wire v2 and legacy v1 inbound. |
nug.cifrar_bytes_with_aad(&[u8], &[u8]) |
Encrypt binary data and bind external metadata as AAD. |
nug.descifrar_bytes_with_aad(&Packet, &[u8]) |
Decrypt a typed packet and verify the expected AAD. |
nug.export_state(&[u8; 32]) |
Export an encrypted ordered-channel snapshot. |
CryptoNugget::import_state(&bytes, &[u8; 32], context) |
Import a snapshot, blocked until mark_resumed(). |
nug.mark_resumed() |
Explicitly acknowledge resumed snapshot state. |
CryptoNugget::builder(&seed, role).mode(...) |
Build a specific operation mode. |
StatelessEnvelope::new(&seed) |
Build a stateless envelope for independent messages. |
ReplayWindowChannel::new(&seed, role, 64) |
Build an out-of-order channel with replay rejection. |
Two unrelated apps that accidentally share a seed must not be able to read each other. Always pass an app-specific context:
let mut nug = CryptoNugget::new_with_context(&seed, Role::Initiator, b"my-app/chat/v1");The context feeds the HKDF salt, so a wrong context yields a different key tree and packets fail authentication.
Use cifrar_bytes / descifrar_bytes when the payload is not UTF-8 (serialized structs, files, etc.). Same wire format, same guarantees.
Use AAD when metadata lives outside the encrypted payload but MUST NOT be tampered with: request IDs, room IDs, message types, tenant IDs, or protocol version labels. The AAD bytes are authenticated by AES-GCM but are not stored in the packet, so the receiver must supply the same bytes:
use cripto_endevs_comunity::{CryptoNugget, MasterSeed, Role};
let seed = MasterSeed::generate();
let mut alice = CryptoNugget::new(&seed, Role::Initiator);
let mut bob = CryptoNugget::new(&seed, Role::Responder);
let aad = b"room:general|message:42";
let packet = alice.cifrar_bytes_with_aad(b"hello", aad).unwrap();
let plaintext = bob.descifrar_bytes_with_aad(&packet, aad).unwrap();
assert_eq!(plaintext, b"hello");If the receiver supplies different AAD, decryption fails with
Error::Autenticacion and the RX ratchet does not advance.
Use StatelessEnvelope for independent encrypted blobs where you do not want a
session ratchet: queued jobs, short files, cache entries, or events that may be
read in any order. Each seal uses a fresh nonce and derives a per-message key
with HKDF.
use cripto_endevs_comunity::{MasterSeed, StatelessEnvelope};
let seed = MasterSeed::generate();
let writer = StatelessEnvelope::new(&seed);
let reader = StatelessEnvelope::new(&seed);
let packet = writer.seal(b"detached payload", b"type:file").unwrap();
let plaintext = reader.open(&packet, b"type:file").unwrap();
assert_eq!(plaintext, b"detached payload");This mode does not provide channel ratcheting, ordering, or replay tracking.
If you need those guarantees, use CryptoNugget ordered channels instead.
Use ReplayWindowChannel only when your transport may reorder packets but you
still need bounded replay rejection. Packets inside the window can arrive out of
order once; repeated packets fail with Error::Repetido; packets older than the
window fail with Error::FueraDeOrden.
use cripto_endevs_comunity::{MasterSeed, ReplayWindowChannel, Role};
let seed = MasterSeed::generate();
let mut alice = ReplayWindowChannel::new(&seed, Role::Initiator, 64).unwrap();
let mut bob = ReplayWindowChannel::new(&seed, Role::Responder, 64).unwrap();
let first = alice.seal(b"first", b"").unwrap();
let second = alice.seal(b"second", b"").unwrap();
assert_eq!(bob.open(&second, b"").unwrap(), b"second");
assert_eq!(bob.open(&first, b"").unwrap(), b"first");This mode intentionally does not ratchet per message. It trades forward secrecy for out-of-order tolerance. Use the ordered channel when you need the ratchet.
Use export_state only when you need to persist an ordered channel across a
controlled restart. Snapshots are AES-GCM encrypted and authenticated with a
caller-supplied 32-byte wrap key. After import, sending is blocked until you call
mark_resumed() so operators explicitly acknowledge that persisted state is
being reused.
use cripto_endevs_comunity::{CryptoNugget, MasterSeed, Role};
let seed = MasterSeed::generate();
let wrap_key = [7u8; 32]; // In production: random key from your KMS/secret store.
let mut alice = CryptoNugget::new_with_context(&seed, Role::Initiator, b"my-app/v1");
let snapshot = alice.export_state(&wrap_key).unwrap();
let mut restored = CryptoNugget::import_state(&snapshot, &wrap_key, b"my-app/v1").unwrap();
restored.mark_resumed();Do not use a password directly as the wrap key. CryptoNugget intentionally does not include a passphrase KDF; derive or fetch the 32-byte wrap key outside the crate.
CryptoNugget is built around one root secret + symmetric ratchet per direction.
MasterSeed(32 bytes fromOsRng) is fed into HKDF-SHA-256 with a salt that includes a stable label and the caller'scontext.- HKDF expands two 256-bit keys (
tx-key:a,tx-key:b). The role decides which one is your TX and which is your RX. - Every successful
cifrarrotates the TX key via HKDF; every successfuldescifrarrotates the RX key. Old keys arezeroized. - Every packet carries
version || flags || sequence || nonce || ciphertext+tagin wire v2, withversion,flags,sequence, snapshot epoch, and optional caller AAD authenticated as AEAD AAD.
- Confidentiality + integrity of every packet (AES-GCM).
- Replay resistance within the channel — a delivered packet's RX key is rotated immediately, so the same bytes won't decrypt again.
- Order enforcement — the internal AAD includes the sequence; reordered packets fail authentication against the current RX key.
- Domain separation — different
context⇒ different keys ⇒ no cross-talk even with the same seed. - Memory hygiene —
Zeroize/ZeroizeOnDrop, nounsafe, redactedDebugimpls.
See SECURITY.md for the full version. Summary:
| Threat | Mitigated? |
|---|---|
| Passive eavesdropping | Yes — AES-256-GCM |
| Tampering with payload or header | Yes — AEAD + AAD over `version |
| Replay of a delivered packet | Yes — RX key rotates after success |
| Wrong role / wrong context | Yes — different derived keys |
| Master seed leakage | No — full break. Seed = root authority. |
| Key exchange / identity / groups | Out of scope. |
| Out-of-order delivery | Not supported. Strict in-order channel. |
| Side-channel attacks | Inherited from aes-gcm and hkdf. No additional countermeasures. |
CryptoNugget is not a protocol like Signal or Noise. It deliberately does not implement:
- Diffie-Hellman or any key exchange / handshake.
- Forward secrecy if the seed is compromised. (Per-message ratchet protects only against future compromise of an in-memory key, not the root seed.)
- Identity, authentication of peers beyond seed possession, or PKI.
- Group messaging or multi-device sync.
- Out-of-order or lossy delivery in the default ordered channel. Use
ReplayWindowChannelonly if you explicitly accept its weaker forward-secrecy properties. - Replay tracking for
StatelessEnvelope. Store packet IDs or hashes yourself if replay matters. - Persistent state snapshots for the ordered channel are supported, but only with
a 32-byte wrap key and explicit
mark_resumed()acknowledgement after import. - Transport. You bring TCP/HTTP/WebSocket/etc.
If you need any of the above, pair this with a real protocol or pick a different library.
- Pure Rust dependencies (
aes-gcm,hkdf,sha2,zeroize,base64). - Zero
unsafe(#![forbid(unsafe_code)]). - Per-message overhead: 1 HKDF expand + 1 AES-GCM call + 1 base64 encode/decode + 1 small
Vecallocation. - Wire-v2 overhead per packet: 22 bytes header + 16 bytes tag = 38 bytes, plus base64 expansion (~33%). Legacy inbound v1 has a 21-byte header.
- No background threads, no timers, no I/O.
Reliability properties:
- Strictly typed errors — every failure mode is enumerated in
Error. - Authenticated metadata: tampering with
version,sequence, ornoncefails asError::Autenticacion. - Sequence overflow returns
Error::Cifradorather than wrapping. - All-zero seed is rejected (
Error::SemillaInvalida).
- 65 tests: unit, adversarial, packet/AAD, mode/envelope, and replay-window integration coverage.
- Adversarial coverage includes: per-field tampering (version, flags/sequence, nonce, tag), replay, future packets, role confusion, context isolation, binary and empty payloads, AAD mismatch, v1 inbound compatibility, typed packet codecs, and
Debugredaction. - CI runs
cargo fmt --check,cargo clippy -D warnings,cargo teston Linux/macOS/Windows, andcargo auditon every push and PR. See.github/workflows/ci.yml.
Run locally:
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test --all-featuresBefore shipping anything that uses CryptoNugget, confirm:
- Seeds are generated with
MasterSeed::generate()(or from a verified high-entropy source). - Transfer tokens (
export_for_transfer) are sent over a secure, ephemeral channel — never logs, URLs, analytics, screenshots, or persistent storage you don't control. - You call
new_with_contextwith a stable, app-specific context (e.g.,b"myapp/chat/v1"). - The two sides agree on roles: exactly one
Initiator, exactly oneResponder. - Your transport delivers packets in order. If it can't, you need a different design.
- You handle
Error::Autenticacionas a channel-fatal event, not a retry signal. Out-of-order, replayed, or tampered packets all surface asAutenticacionand indicate the channel can no longer be trusted in its current state. - If you persist state, the wrap key is a random 32-byte key from a KMS or equivalent secret store, not a raw passphrase.
- After
import_state, you callmark_resumed()only after your operator or recovery workflow confirms this snapshot should continue the session. - You never
Debug-print or serializeMasterSeedorCryptoNuggetto a sink you don't control. (TheDebugimpls are redacted, but the bytes still live in memory.) - You do not depend on backwards compatibility for any wire format produced before 1.0.
Each packet starts with a single version byte. CryptoNugget v0.3 emits wire
version 2; the ordered channel accepts wire version 1 inbound for one
release cycle to support gradual upgrades. Unknown versions return
Error::VersionNoSoportada. Until 1.0, both the API and the wire format may
change in minor releases. See MIGRATION-0.3.md.
Packet layout:
+---------+----------+----------------+-----------+----------------------+
| version | flags | sequence (u64) | nonce(12) | ciphertext + tag(16) |
+---------+----------+----------------+-----------+----------------------+
1 byte 1 byte 8 bytes 12 bytes N + 16 bytes
|--------- internal AAD ---------| (AES-GCM authenticates the rest)
Flag bit 0 means caller-supplied AAD was bound into authentication. The AAD bytes are not stored in the packet. Then the packet is base64-encoded for text transports.
Dual-licensed under either of:
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
at your option.
- Desarrollador: ENRODMONTPAR
- GitHub C#: @MASTER-RODRI
- GitHub RUST: @MASTER-RODRI
- Crates.io: @MASTER-RODRI
- nugget.org: @ENRODMONTPAR
- npmjs.com: @ENRODMONTPAR ENRODMONTPAR — @MASTER-RODRI