Skip to content

Repository files navigation

HIDE — hybrid post-quantum file & message encryption (X25519 + ML-KEM-768)

Human-friendly Identity & Data Encryption. The goal is to encrypt to a person, not to a key. This repository implements the file format engine for post-quantum file encryption, a hybrid signature scheme, and the machinery an identity needs to outlive a single key: device enrollment and revocation, forward security by erasure, an auditable history, and group messaging over MLS.

HIDE is end-to-end encryption for files and short messages whose key encapsulation is quantum-resistant by construction: every content key is wrapped with X-Wing, a hybrid of X25519 and ML-KEM-768 as standardised in NIST FIPS 203 (ML-KEM), and every signature is a hybrid of Ed25519 and ML-DSA-65 from FIPS 204 (ML-DSA). A break of either half alone is not enough.

HIDE is experimental and unaudited. Do not use this for sensitive data. The protocol is a draft, no third party has reviewed the code, and the hybrid KEM tracks a moving IETF draft.

What is verified today

Every claim below was produced by a command in this repository, on Rust 1.97.1.

  • Encrypt/decrypt round-trips across chunk boundaries (0 B, 1 B, 64 KiB ± 1, multi-chunk).
  • One payload, many recipients: the file is encrypted once; only the content key is wrapped per recipient.
  • Tamper detection: flipping every byte position of the frozen vectors makes decryption fail (cargo test -p hide-object --test vectors), and property tests extend that to random single-byte mutations of random containers.
  • Truncation, chunk reordering, duplication, deletion and trailing bytes are all rejected.
  • Nine frozen rejection vectors (conformance/vectors/rejections/) — bad magic, header-length overflow, flipped header and FINAL bits, altered payload salt, truncation, trailing bytes, a small-order recipient key — are refused by both the Rust crates and the independent Node implementation, so the two agree on what is invalid, not only on what is valid.
  • Fuzzed: six libFuzzer targets under fuzz/ (container header, object open, keyring open, identity log, epoch chain, transparency proofs) run in CI on every push.
  • unsafe_code = "forbid" in every crate except hide-ffi, where the C ABI needs it; 297 tests.
  • Independent interoperability: a separate Node implementation (@hpke/hybridkem-x-wing, cbor, Node crypto) decrypts the Rust vectors, and Rust decrypts Node's container byte-identically.
  • Cross-OS: the full suite passes on Windows 11 and on Linux (WSL2 Ubuntu 24.04), and a Linux build opens a container produced on Windows.
  • Degenerate recipient keys are refused: an X25519 component of small order would silently remove the classical half of the hybrid, so all seven such points are rejected before use.
  • Property tests (proptest) assert the parser never panics on arbitrary input, that any single-byte mutation of a container fails to decrypt, and that truncation or appended bytes always fail.
  • Hybrid signatures: Ed25519 + ML-DSA-65, concatenated; a signature verifies only if both halves do, so neither a quantum nor a classical break of one is enough.
  • Signatures cross surfaces: a signature made in WASM verifies in Node and vice versa, and every SDK returns the same verdict on the same bytes (node conformance/cross-surface/verify.mjs).
  • OpenSSH accepts our agent: ssh-add -l lists the key, ssh-keygen -Y sign obtains a signature through it, and ssh-keygen -Y verify reports it good — verified by OpenSSH's own tools, not ours.
  • Replay is refused: a challenge answer is accepted once; presenting the identical valid signature again is rejected, as is one given for a different audience or after its expiry.
  • Revocation means something: an identity is a hash-linked log of device events, and authority is evaluated at the point in the log where an entry appears rather than against the final state. A revoked device cannot re-enrol itself, cannot revoke the device that removed it, and cannot rewrite anything after its removal. Only an offline recovery key can replace the device set.
  • Forward security by erasure: keys are grouped into epochs, and destroying an epoch's secret makes every container written to it unreadable — including by the intended recipient. Epoch secrets are independent random keys, not derived from a master seed, because a derived chain would let anyone holding the seed reconstruct what was supposedly erased.
  • A rewritten history is detectable: the transparency log answers RFC 6962 inclusion and consistency proofs, swept over every size from 1 to 33 and every index. A log that alters or drops an entry it already published cannot produce a consistency proof against the root it published before.
  • Group messages survive revocation: MLS proves a message came from a group member, but not that the member's device is still trusted. accept_from_trusted refuses a message from a device the identity revoked, even though MLS itself considers it a valid member.

Measured performance

On this machine (release build, 256 MiB payload):

Metric Value
Encrypt / decrypt (in memory) ~1250 / ~1550 MiB/s
Peak RSS for a 256 MB file, unsigned 7 MB — constant, independent of input size
Size overhead 0.033% (~85 KB, dominated by the 1120-byte hybrid encapsulation)
hide.exe 675 KB

Streaming reuses two fixed 64 KiB buffers and one expanded AEAD instance, so there is no per-chunk allocation or rekeying. The constant-memory figure applies to unsigned encryption: signed encryption commits to SHA-256(plaintext), so it buffers the plaintext and is capped at 1 GiB.

Verified by: measured on the machine that produced this README (Windows 11, x86-64; your CPU and OS will give different absolute numbers). Reproduce with cargo run --release -p hide-object --example throughput.

Compared with age, GPG, libsodium

HIDE age GPG libsodium sealed box Signal / MLS AWS KMS envelope
Post-quantum KEM X-Wing (X25519 + ML-KEM-768), always Yes, opt-in: age-keygen -pq gives ML-KEM-768 + X25519 since v1.3.0 No (RFC 4880; PQ drafts in progress) No (X25519) Signal: PQXDH; MLS: PQ suites are a draft ML-KEM in TLS to the service; envelope key is AES
Multi-device identity Signed device log with revocation; no directory No — one key per recipient Keyring, subkeys, web of trust No — a key is a key Yes, service-operated directory IAM principals, not people
Forward secrecy By erasure of epoch keys, no ratchet No No No Yes, double ratchet / tree ratchet Rotation policy on the master key
Signatures Ed25519 + ML-DSA-65 hybrid No (integrity only) Yes, classical Separate crypto_sign, Ed25519 Implicit via ratchet authentication Separate KMS sign API
Streaming large files Yes, 64 KiB chunks, constant memory (unsigned) Yes, 64 KiB STREAM Yes No — one message in memory Attachments handled by the app Application concern
Third-party audit None Yes (Trail of Bits, findings fixed Aug 2026) Long public review history Yes (Private Internet Access, 2017) Yes (multiple) Vendor-attested

age and libsodium are mature, audited tools, and age has offered a hybrid post-quantum recipient type since v1.3.0. If what you need is post-quantum file encryption to a key, use age: it is audited and HIDE is not. What HIDE adds on top is a signed multi-device identity with revocation, hybrid signatures on containers, forward security by epoch erasure, a transparency-log primitive and MLS group binding, all under one format. Detail in docs/comparison.md.

What is NOT implemented or guaranteed

Being explicit here matters more than the feature list.

  • No directory, and no key-transparency service. The RFC 6962 log primitive exists, but no one operates a log, so recipients are still raw key files that you must exchange over a channel you already trust. Nothing proves a key belongs to a particular person.
  • Sender authentication only when the container is signed. For an unsigned container, a successful decryption proves it was not altered; it does not prove who created it. A signed container binds a signing key to the recipient set, the metadata and the exact plaintext — but it attests to a key, and nothing yet proves that key belongs to a particular person.
  • Forward secrecy is by erasure, not by ratchet. It exists only if the holder actually destroys the epoch secret; anyone who later obtains a secret that was not erased can decrypt every container captured under it. Device revocation cannot retroactively protect data an attacker already holds.
  • No hardware protection. Secret keys are sealed with a passphrase (Argon2id + ChaCha20-Poly1305), but there is no Keychain, TPM, Secure Enclave or Keystore integration, and --insecure-plaintext still writes an unencrypted key on request.
  • Recipient privacy is limited. Stanzas carry no identifiers, but the recipient count and the ciphertext size are visible, and metadata is encrypted rather than hidden. A public signature also reveals the signer's key to anyone holding the file; the confidential placement avoids this.
  • Signing is not streaming. A signature commits to the plaintext, so signing buffers the payload.
  • SSH authentication is not post-quantum. hide agent offers the Ed25519 half of an identity and nothing more. OpenSSH accepts only ssh-ed25519, sk-* and RSA for user authentication; post-quantum algorithms exist there only in key exchange. What this buys is one sealed identity instead of a plaintext private key sitting in ~/.ssh, not quantum resistance.
  • An agent is a signing oracle. Anything that can reach the endpoint can ask for a signature. That is why confirmation is the default and --no-confirm must be asked for.
  • Group messaging is not post-quantum. hide-mls uses X25519, because MLS's post-quantum ciphersuites are still an Internet-Draft and no Rust provider implements them. Object encryption is hybrid post-quantum, so a group message and a sealed file are protected differently. The API states this in PQ_STATUS rather than letting the file format imply uniform protection.
  • mls-rs is unaudited too, like everything else here.
  • A transparency log cannot detect a split view by itself. Two divergent logs are each internally consistent; catching that needs independent witnesses who gossip roots and refuse to sign two roots for one size. No witnessing is implemented, so the log is a promise rather than a proof.
  • Epoch secrets are not persisted. hide epoch-init publishes a history, but the secret exists only in the process that made it. A durable epoch store is not built, so erasure is demonstrable but not yet operationally useful.
  • Revocation is deliberately not retroactive. Entries signed before a device was revoked stay valid, because invalidating them would invalidate every message that device ever sent. Revoking a device also does not evict it from MLS groups automatically; that is a separate, explicit call.
  • An identity still is not a person. The log proves which devices an identity trusts over time. It does not prove that identity belongs to a particular human, and there is no directory to ask.
  • The MLS credential binding is new in 0.7.0 and has been exercised only by this repository's own tests; no other implementation has parsed it.

Download

Releases carry three kinds of build. Verify any download against SHA256SUMS first.

Build File Use it when
Desktop application HIDE_*-setup.exe, *.dmg, *.deb, *.AppImage You want a window, not a terminal.
Portable hide-portable-* You want one executable, no installation, keys kept beside it.
Command line hide-* You want to script it.

The portable build writes nothing outside its own folder: keys go into a hide-keys directory next to the executable, so it runs from a USB stick and leaves no trace in your user profile.

Platforms

The CLI is built for Linux (x86-64, ARM64, and a static musl build for Alpine and scratch containers), Windows (x86-64, ARM64) and macOS (Apple silicon, Intel).

Package managers

Manifests for Homebrew, Scoop, WinGet and the AUR live in packaging/ and are generated with the real checksums by the release workflow. None is published yet: putting an unaudited encryption tool in a default package manager reaches people who will not read the warnings, so that step is taken deliberately rather than automatically.

SDKs

Every binding calls the same Rust core through one C ABI (crates/hide-ffi). No language reimplements the cryptography, so there is a single implementation to review, and conformance/cross-surface asserts that what one surface produces every other surface can open.

Language Path How it binds
C / C++ crates/hide-ffi/include/hide.h The ABI itself
Python sdk/python ctypes, so a wheel needs no compiler
TypeScript / Node sdk/node koffi over the same shared library
Browser sdk/wasm WebAssembly, compiled from the same crates
Go sdk/go cgo
Java / Kotlin sdk/java Foreign Function & Memory API, no JNI shim
Ruby sdk/ruby stdlib fiddle, no native gem to build
PHP sdk/php ext-ffi
.NET / C# sdk/dotnet Source-generated LibraryImport

Installing

pip install hide-protocol
npm install hide-protocol
gem install hide-protocol
dotnet add package HideProtocol
npm install @hide-protocol/wasm     # browser
go get github.com/hide-protocol/hide/sdk/go

The first four carry the compiled core for seven targets — x86-64 and ARM64 Linux (glibc), x86-64 Linux (musl), x86-64 and ARM64 Windows, and Apple silicon and Intel macOS — so nothing needs a Rust toolchain; the WASM package is compiled from the same crates. Go links the core statically through cgo.

Java (org.hide-protocol:hide) is not yet on Maven Central and PHP (hide-protocol/hide) is not yet on Packagist: build both from source. Neither ships a binary: build it with cargo build --release -p hide-ffi and point HIDE_LIBRARY (Java also accepts -Dhide.library=) at the result, with HIDE_ALLOW_LIBRARY_OVERRIDE=1 set as well. The same pair overrides the bundled library everywhere; it is a development-only override, since it replaces the entire cryptographic core, which is why the second variable is required.

Secret keys never cross into the host language: each SDK holds an opaque handle, and there is deliberately no function that exports key material.

import hide_protocol as hide

with hide.SecretKey.generate() as secret:
    box = hide.encrypt(b"hello", [secret.public_key()])
    assert hide.decrypt(box, secret).data == b"hello"

A browser is a weaker place to hold a key than a desktop: any script on the page shares the heap, so an XSS bug is equivalent to key theft. Prefer the CLI or the desktop application for keys that matter.

Try it

cargo test --workspace --all-features

# A key pair. The secret is sealed with a passphrase unless you opt out.
cargo run -p hide-cli -- --experimental keygen --secret alice.hide-key --public alice.hide-pub

# Files.
cargo run -p hide-cli -- --experimental encrypt report.pdf --recipient alice.hide-pub --output report.pdf.hide
cargo run -p hide-cli -- --experimental open report.pdf.hide --secret alice.hide-key --output report.pdf

# Sign as you encrypt. The signature is readable only by the recipients unless
# you pass --public-signature.
cargo run -p hide-cli -- --experimental encrypt report.pdf --recipient alice.hide-pub --output report.pdf.hide --sign alice.hide-key

# Or sign a file in place, leaving report.pdf.hide-sig beside it.
cargo run -p hide-cli -- --experimental sign report.pdf --secret alice.hide-key
cargo run -p hide-cli -- --experimental verify report.pdf --signer alice.hide-pub.sign

# Text messages, as a block you can paste into email or chat.
cargo run -p hide-cli -- --experimental seal "meet at six" --recipient alice.hide-pub
cargo run -p hide-cli -- --experimental unseal message.txt --secret alice.hide-key

# What is this file? Answered without decrypting it.
cargo run -p hide-cli -- --experimental info report.pdf.hide

The CLI never overwrites an existing file, writes plaintext to private staging first, and publishes the result only after authentication succeeds. --experimental is mandatory, so the risk is acknowledged explicitly.

keygen writes three files: one secret master seed, and two shareable public keys — alice.hide-pub for encryption and alice.hide-pub.sign for checking signatures. Both derive from the master seed, so there is a single thing to back up, and neither can be computed from the other. A key file created before signatures existed still decrypts; signing with it fails and says so.

A signature proves possession of a key. HIDE has no directory, so nothing ties that key to a person — compare a signer's key against one you already trust.

An identity with more than one device

A key is a thing you lose. An identity is a log of device events, each signed by a device the log already trusted, so it can survive losing one.

# The founding device, plus an offline recovery key kept somewhere else entirely.
cargo run -p hide-cli -- --experimental identity-create --secret laptop.hide-key `
  --recovery recovery.hide-pub.sign --label laptop --output alice.hide-log

# Add a phone. Signed by the laptop, because only a trusted device may enrol another.
cargo run -p hide-cli -- --experimental identity-enrol --log alice.hide-log `
  --secret laptop.hide-key --device phone.hide-pub.sign --label phone `
  --recovery recovery.hide-pub.sign

# The phone is stolen.
cargo run -p hide-cli -- --experimental identity-revoke --log alice.hide-log `
  --secret laptop.hide-key --device phone.hide-pub.sign --recovery recovery.hide-pub.sign

# Anyone can replay the log and see who is trusted now. No secret required.
cargo run -p hide-cli -- --experimental identity-show --log alice.hide-log `
  --recovery recovery.hide-pub.sign

The log is public and append-only: verifying it needs no secret, which is what lets someone else check which devices you trust. Revocation applies from the point it appears — the revoked phone cannot re-enrol itself or revoke the laptop, but containers it already holds stay readable to it, and signatures it made while trusted stay valid. Nothing can reach back and change that.

If every device is lost, the offline recovery key replaces the whole device set. It is the only key that can, which is why it belongs somewhere that is not a computer.

Logging in over SSH

The same identity can act as an ssh-agent, so the key that authenticates you is never written to disk in the clear.

# Print the public line to paste into ~/.ssh/authorized_keys or github.com/settings/keys.
cargo run -p hide-cli -- --experimental ssh-key --secret alice.hide-key

# Serve it. Every signature asks for confirmation unless you pass --no-confirm.
cargo run -p hide-cli -- --experimental agent --secret alice.hide-key

Then point SSH at it with SSH_AUTH_SOCK — the socket path on Unix, the pipe path on Windows ($env:SSH_AUTH_SOCK = '\\.\pipe\hide-agent'). OpenSSH for Windows 9.5p2 ignores -o IdentityAgent, so use the environment variable on both platforms.

This offers the Ed25519 half of the identity only. SSH cannot carry the post-quantum half, so an SSH login is not post-quantum; what it avoids is a plaintext private key on disk. Treat the endpoint as sensitive: anything that can reach it can ask for a signature.

Building the desktop application

cd apps/hide-desktop
pnpm install --ignore-workspace
pnpm tauri build        # installer
pnpm build:portable     # single portable executable

The application calls the same Rust crates as the CLI; it contains no separate cryptographic code. Key material never reaches the user interface layer. A test in src-tauri/tests/interop.rs asserts that each surface can open what the other produced, so they cannot silently diverge.

Repository layout

Path Purpose
crates/hide-format Preamble, bounded canonical CBOR, portable-filename metadata
crates/hide-crypto HPKE X-Wing wrapping, HKDF, HMAC, ChaCha20-Poly1305; secrets zeroize and cannot be printed
crates/hide-sign Hybrid Ed25519 + ML-DSA-65 signatures, detached signatures, challenge–response
crates/hide-keyring Passphrase-sealed key files (Argon2id) and public-key armor
crates/hide-object Envelope encryption and authenticated 64 KiB streaming
crates/hide-epoch Epoch chains: forward security by erasure
crates/hide-identity Hash-linked device log: create, enrol, revoke, recover
crates/hide-transparency RFC 6962 Merkle tree, inclusion and consistency proofs
crates/hide-mls MLS (RFC 9420) groups bound to HIDE identities via mls-rs
crates/hide-ffi The C ABI every language binding calls
crates/hide-wasm WebAssembly bindings for the browser (not published to crates.io)
apps/hide-cli hide binary
apps/hide-desktop Desktop application (Tauri) and the portable build
sdk/ Python, Node, WASM, Go, Java, Ruby, PHP and .NET packages; the C header is crates/hide-ffi/include/hide.h
packaging/ Homebrew, Scoop, WinGet and AUR manifests
conformance/ Frozen vectors, rejection vectors, the independent Node verifier and the cross-surface check
fuzz/ libFuzzer targets, run in CI
docs/ Threat model, comparison, architecture, stability policy, tracker
spec/hide-0.1.md Wire format

Cryptography

Suite 1 is HPKE base mode with the X-Wing hybrid KEM (X25519 + ML-KEM-768), HKDF-SHA256 and ChaCha20-Poly1305 in 64 KiB chunks, via the hpke and RustCrypto crates. Signatures are Ed25519 + ML-DSA-65 (FIPS 204). Key files are sealed with Argon2id. The transparency log is an RFC 6962 Merkle tree. Group messaging is MLS (RFC 9420) through mls-rs, on the classical X25519 suite only. No primitive is implemented here. Because X-Wing and HPKE-PQ are still drafts, every dependency is pinned to an exact version and the wire format will change; vectors will be regenerated when the upstream construction changes.

Documentation

Help audit this

No third party has audited HIDE, and an unaudited encryption tool should be treated as broken until proven otherwise. An audit that would change that should cover: the HPKE/X-Wing composition and the key schedule (spec/hide-0.1.md §3); the authenticated streaming and the FINAL rule (§5); the signature transcript and what a recipient can forge without one (§6–7); the identity log's authority-at-position rule (§8); the epoch chain and what "erased" actually guarantees (§9); the RFC 6962 proofs (§10); the MLS credential binding (§11); the C ABI's memory and panic handling in hide-ffi; and the ssh-agent's confirmation path. Internal review found real defects in 0.7.0 (SECURITY.md lists them), which is evidence that more exist. Report through GitHub's private advisory as described in SECURITY.md; every finding is credited in the CHANGELOG and in docs/advisories.md. There is no bounty, only credit and a fast fix.

License

Apache-2.0 — the specification and vectors are freely implementable, with no requirement to use any particular server or service.

About

Encrypt to a person, not to a key. Experimental hybrid post-quantum file encryption (X25519 + ML-KEM-768). Unaudited; not for sensitive data.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages