aescry
AES encryption and decryption for Rust, with full support for the AES Crypt file format.
- Encrypt and decrypt data, streams and files with a password, compatible with the AES Crypt 4.x tools (reads format versions 0–3, writes version 3).
- Encrypt and decrypt raw byte buffers with your own key (AES-128/192/256 in CBC mode).
- The building blocks, all usable on their own: AES, SHA-256, SHA-512, HMAC, PBKDF2, PKCS#7 padding, secure randomness, constant-time comparison and secret wiping.
- A security toolkit for testing, forensics and research, with raw-byte control over passwords, IVs and keys.
All cryptography is implemented in Rust. The only dependency is
getrandom, for the operating system's
random number generator. On x86/x86-64 CPUs with AES-NI, AES runs on the
hardware instructions in constant time.
Contents
- Installation
- Which API should I use?
- Quick start
- Encrypting with a password (AES Crypt)
- Encrypting with your own key (AES-CBC)
- Authenticating ciphertext
- Deriving keys from passwords
- Hashing and MACs
- Building blocks
- Keeping secrets out of memory
- Handling errors
- Security toolkit
- Security notes
- Releases · Development · License
Installation
[]
= "1.0"
aescry requires Rust 1.63 or newer. See Stability for what
the 1.0 promise covers.
Which API should I use?
| You want to… | Use |
|---|---|
| Encrypt data or files with a password | aescrypt |
Open .aes files made by AES Crypt or pyAesCrypt |
aescrypt |
| Check whether a file is AES Crypt data | detect |
| Encrypt with a key you manage (not a password) | cbc + hmac |
| Turn a password into a key for your own format | kdf |
| Hash data or compute a MAC | sha256, sha512, hmac |
| Encrypt single blocks, pad, get random bytes, compare in constant time | building blocks |
| Test other implementations, write old formats, recover damaged files | security |
If you are unsure, use aescrypt: it handles key derivation, random IVs,
authentication and file handling for you, and its output opens in AES Crypt.
Quick start
use aescrypt;
let secret: & = b"any bytes: text, images, archives \x00\xff";
let encrypted = encrypt?;
let decrypted = decrypt?;
assert_eq!;
// A wrong password is detected, never silently decrypted to garbage.
assert!;
# Ok::
encrypted is a complete AES Crypt stream: save it as a .aes file and the
AES Crypt apps can open it.
Encrypting with a password (AES Crypt)
The aescrypt module encrypts with the current AES Crypt format (version 3):
the password goes through PBKDF2-HMAC-SHA512 (600,000 iterations by default),
the data is encrypted with AES-256-CBC under a random session key, and two
HMAC-SHA256 tags detect a wrong password and any modification. Decryption
checks both tags before returning anything.
Data in memory
use aescrypt;
let encrypted = encrypt?;
assert_eq!;
# Ok::
Settings: iterations and extensions
Encryptor and Decryptor hold a password and settings, and can be reused.
use ;
let encryptor = new?
// PBKDF2 iterations: 1 to 5,000,000 (default 600,000). More is slower
// for attackers guessing passwords, and for you.
.iterations
// Unencrypted, unauthenticated metadata stored in the header.
.extension;
let encrypted = encryptor.encrypt?;
let decryptor = new?;
assert_eq!;
# Ok::
By default the header also carries a CREATED_BY extension and an empty
128-octet container extension, as AES Crypt recommends;
.without_default_extensions() leaves them out.
Files
use ;
new?.encrypt_file?;
new?.decrypt_file?;
# Ok::
Output goes to a temporary file next to the destination and is renamed into place only when complete. A failed decryption (wrong password, altered file) leaves no output file behind and does not touch an existing one.
Streams
Any Read can be encrypted to any Write without holding the data in memory:
use ;
use File;
use ;
// stdin -> encrypted file
new?.encrypt_stream?;
// encrypted file -> stdout
new?.decrypt_stream?;
# Ok::
decrypt_stream writes plaintext as it goes, before the final integrity
check. If it returns an error, discard what it wrote. decrypt and
decrypt_file never expose unverified data.
Reading the header
The header is not encrypted, so it can be read without the password:
use ;
use Version;
let encrypted = new?.iterations.encrypt?;
let header = read_header?;
assert_eq!;
assert_eq!;
assert!;
# Ok::
Detecting AES Crypt files
use detect;
// A file on disk: None if it can't be read or isn't AES Crypt data.
if let Some = get_file
// Bytes in memory, keeping the version.
let data = read?;
if let Some = from_bytes
// Any reader (only the 4-octet header is read).
let version = from_reader?;
# let _ = version;
# Ok::
Detection checks the AES magic octets, a known version and, when the length
is known, the minimum length of a valid stream of that version.
Limits on untrusted files
Everything in a header comes from the file, so reading is bounded by
Limits: at most 5,000,000 PBKDF2 iterations, a 1 MiB header and 256
extensions. You can lower them for untrusted input:
use ;
let strict = new?.limits;
# let _ = strict;
# Ok::
Supported format versions
| Version | Read | Write | Key derivation | Written by |
|---|---|---|---|---|
| 3 | ✓ | ✓ | PBKDF2-HMAC-SHA512 | AES Crypt 4.x |
| 2 | ✓ | security only |
8192 × SHA-256 | AES Crypt 3.x, pyAesCrypt |
| 1 | ✓ | security only |
8192 × SHA-256 | older AES Crypt |
| 0 | ✓ | security only |
8192 × SHA-256 | older AES Crypt |
Legacy formats (0–2) don't authenticate the final block size. Someone who can modify an old
.aesfile can drop up to 15 octets from the end of the plaintext without failing either HMAC.security::verifyandRawDecryptorreport this throughVerification::final_block/is_consistent(). Version 3, whichaescryptwrites, authenticates its padding. Re-encrypt old files as version 3 when you can.
Encrypting with your own key (AES-CBC)
Use cbc when you manage keys yourself instead of using passwords. Keys are
16, 24 or 32 octets (AES-128, AES-192, AES-256); IVs are 16 octets and must
never repeat for the same key.
Keys
AesKey is a key whose length has been checked. It is wiped from memory when
dropped and never printed.
use ;
let key = generate?; // random key
let from_bytes = try_from?; // raw bytes, checked once
assert!; // not an AES key size
# let _ = ;
# Ok::
Encrypting and decrypting
cbc::encrypt / cbc::decrypt take a raw key, a raw IV and any bytes, and
apply PKCS#7 padding:
use ;
use ;
let key = generate?;
let iv = iv?;
let ciphertext = encrypt?;
let plaintext = decrypt?;
assert_eq!;
# Ok::
To have the IV generated for you and stored in front of the ciphertext:
use ;
let key = ?;
let message = encrypt_with_random_iv?; // IV || ciphertext
assert_eq!;
# Ok::
For data that is already a multiple of 16 octets there are
cbc::encrypt_no_padding and cbc::decrypt_no_padding.
Streaming
CbcEncryptor and CbcDecryptor keep the chaining value between calls, so
data can be processed in pieces of whole blocks:
use Aes128;
use ;
let cipher = new;
let iv = ;
let mut data = ;
let mut encryptor = new;
encryptor.encrypt_in_place?;
encryptor.encrypt_in_place?;
let mut decryptor = new;
decryptor.decrypt_in_place?;
assert_eq!;
# Ok::
CBC alone does not detect tampering. Anyone who can change the ciphertext can make predictable changes to the plaintext. Add a MAC, as shown next, or use
aescrypt, which does this for you.
Authenticating ciphertext
Encrypt-then-MAC: encrypt, then compute an HMAC over the IV and ciphertext with a separate key. The receiver checks the tag (in constant time) before decrypting.
use ;
use ;
let enc_key = ?;
let mac_key = ?;
// Sender
let message = encrypt_with_random_iv?;
let tag = hmac_sha256;
// Receiver
let mut mac = new;
mac.update;
mac.verify?; // Err(Error::AuthenticationFailed) if anything changed
let plaintext = decrypt_with_iv_prefix?;
assert_eq!;
# Ok::
Deriving keys from passwords
PBKDF2 turns a password into a key for your own formats. Use a random salt per password, store it with the data, and pick an iteration count that takes a noticeable fraction of a second.
use kdf;
let salt = ?;
let key: = pbkdf2_hmac_sha512_array?;
// Any output length, and SHA-256 as the hash:
let mut material = ;
pbkdf2_hmac_sha256?;
# let _ = key;
# Ok::
kdf::aescrypt_legacy and kdf::utf16le implement the older AES Crypt
derivation (formats 0–2) for compatibility.
Hashing and MACs
use ;
use ;
use sha512;
// One call
let digest = sha256;
assert_eq!;
// Incrementally, for large or streamed data
let mut hasher = new;
hasher.update;
hasher.update;
assert_eq!;
// HMAC, one call or incremental
let tag = hmac_sha512;
let mut mac = new;
mac.update;
let tag256 = mac.finalize;
# let _ = ;
Hmac::verify compares a tag in constant time; Hmac::verify_truncated
checks truncated tags of at least 80 bits. Code can be generic over the hash
with the digest::Digest trait:
use Digest;
use Sha512;
let print = ;
# let _ = print;
Building blocks
AES block cipher
Each cipher encrypts and decrypts single 16-octet blocks in place. Use
Aes128, Aes192 or Aes256 when the key size is fixed, or Aes for a key
size chosen at runtime.
use ;
let cipher = new;
let mut block = *b"exactly 16 bytes";
cipher.encrypt_block;
cipher.decrypt_block;
assert_eq!;
let cipher = from_key;
assert_eq!;
// AES-NI (constant-time) where available, portable software otherwise.
println!;
let software = with_backend?;
# let _ = software;
# Ok::
To refuse to run without constant-time AES rather than fall back to the
software backend, use Backend::constant_time(), or
require_constant_time() on the AES Crypt encryptors and decryptors:
use ;
use Decryptor;
let cipher = with_backend?;
let decryptor = new?.require_constant_time; // errors without AES-NI
# let _ = ;
# Ok::
Encrypting blocks independently (ECB) reveals which blocks are equal; use the block cipher as a building block, not to encrypt messages.
Padding, randomness and constant-time comparison
use ;
use ;
// PKCS#7: always adds 1 to 16 octets
let padded = pkcs7_pad;
assert_eq!;
assert_eq!;
// Secure random values from the operating system
let key: = bytes?;
let iv = iv?;
let key_192 = key?;
// Compare secrets without leaking where they differ
assert!;
# let _ = ;
# Ok::
Keeping secrets out of memory
Keys, passwords, hash states and decryption buffers inside aescry are wiped
when dropped. Two types do the same for your own values:
use Secret;
use ;
// Secret: wiped on drop, never printed, constant-time ==, explicit access.
let api_key = new;
assert_eq!;
let bytes: & = api_key.expose_secret;
// Zeroizing: wiped on drop, otherwise behaves like the value.
let mut buffer = new;
buffer = 1;
// Wipe something right now.
let mut password = Stringfrom;
password.zeroize;
assert!;
# let _ = bytes;
Handling errors
Every fallible function returns Result<_, aescry::Error>. The variants say
what went wrong, so you can respond precisely:
use ;
assert_eq!;
Error implements std::error::Error and Display, so it also works with
?, Box<dyn Error> and error-handling crates.
Security toolkit
The security module is for security work: testing other AES Crypt
implementations, generating test vectors, forensics and recovering your own
data. It gives raw-byte control over passwords, IVs and keys, writes every
format version, and exposes derived and session keys. The risky operations
are separate types, so they can't be used by accident.
use ;
use Version;
// Any octets can be the password, not just valid text.
let password: & = &;
// Fixed IVs and session key make output reproducible. This is a separate
// type, RawEncryptor<Deterministic>, because reusing IVs is unsafe.
let stream = new
.version // any format version, 0 to 3
.deterministic
.encrypt?;
// Map the structure without the password, and check the HMACs without
// producing plaintext.
assert_eq!;
assert!;
// Decrypt and recover the keys.
let opened = new.decrypt?;
assert_eq!;
let derived = opened.report.derived_key.unwrap.clone_secret;
// Reopen with the derived key alone (no password or key derivation).
let again = new.decrypt?;
# let _ = again;
# Ok::
Damaged or tampered files can be decrypted for recovery, but the result is
wrapped in Unauthenticated, so using it is an explicit choice:
use ;
let damaged = read?;
let result = new
.skip_verification
.decrypt?;
println!;
let recovered = result.assume_authentic;
# let _ = recovered;
# Ok::
The module also offers Password::encoded and DerivedKey::derive (what the
key derivation sees and produces), raising Limits beyond the defaults,
arbitrary header extensions for parser testing (Extension::from_bytes), and
raw ECB block operations. Use aescrypt for everyday encryption.
Security notes
-
Memory safety. Untrusted bytes are handled only by safe Rust. The only
unsafecode is the AES-NI backend (fixed 16-octet blocks) and the volatile writes inzeroize. Raw bytes for keys, IVs and passwords are length-checked once, when they become typed values. -
No panics on input. Library code is built with
unwrap,expect,panic!,unreachable!, unchecked indexing and unchecked arithmetic denied by lints. The only exceptions are fixed-size cryptographic kernels (for example,u8indexes into 256-entry tables), and each carries a written justification. Header parsing is bounded byLimitsbefore anything is allocated. -
Tested adversarially. Five
cargo-fuzztargets check properties, not just crashes:- accepted streams must be authentic;
- in-memory and streaming decryption must agree;
- encryption must round-trip;
- any change to a version 3 stream must be rejected.
Property tests (
proptest) cover the same invariants, and Miri checks the test suite for undefined behavior. The AES-NI code needs a capability token that only runtime detection can create. -
Legacy format weakness. In AES Crypt versions 0–2 the final block size is unauthenticated (see Supported format versions); version 3 fixes this.
-
Timing. With AES-NI (nearly all x86 CPUs since 2010), AES runs in constant time. Other CPUs use a table-based implementation whose timing can leak key information to an attacker who can measure it precisely, such as another tenant on the same machine.
Backend::is_constant_time()says which applies, andrequire_constant_time()/Backend::constant_time()refuse to fall back. MAC and padding checks are constant-time. -
Secrets are wiped from memory when dropped and never appear in
Debugoutput. Wiping cannot reach copies made by the operating system (swap) or earlier reallocations. -
CBC padding.
cbc::decryptreports bad padding as an error. If an attacker can submit ciphertexts and observe that error, they can decrypt data (a padding oracle), so verify a MAC first. -
AES Crypt header extensions are neither encrypted nor authenticated. Don't trust their contents.
Stability
aescry follows Semantic Versioning. From 1.0.0,
breaking changes to the public API (including the security toolkit) only
happen in a new major version:
- Types marked
#[non_exhaustive](Errorand its detail enums,Version,Backend,Limits,Verification,Layout,EncryptKey,DecryptKey) can gain variants or fields in minor releases, so match them with a wildcard arm. digest::Digestis sealed: only this crate implements it.- Streams written by one 1.x release can be read by every later 1.x release, and version 3 output stays compatible with AES Crypt.
- The minimum supported Rust version (1.63) is only raised in a minor release, and the changelog says so.
- Default resource limits and iteration counts may be tightened in a minor release if needed for security; the changelog explains why.
CI runs cargo-semver-checks on every pull request to catch accidental
breaking changes.
Releases
Each release adds one feature set; see the changelog.
| Version | Feature set |
|---|---|
| 0.2 | Core primitives: AES block cipher, SHA-256, AES Crypt detection |
| 0.3 | Encrypting byte buffers: CBC mode, PKCS#7 padding, secure random keys and IVs |
| 0.4 | Integrity and key derivation: SHA-512, HMAC, constant-time comparison, PBKDF2 |
| 0.5 | AES Crypt file format: password-based encryption and decryption of buffers, streams and files |
| 0.6 | Hardening: hardware AES (AES-NI) backend and wiping secrets from memory |
| 1.0.0-beta.1 | Security toolkit: raw-byte passwords, IVs and keys; stream inspection and verification |
| 1.0.0-beta.2 | Type safety: validated key/IV/iteration types, Secret<T>, typestate toolkit, resource limits |
| 1.0.0-rc.1 | Assurance: no-panic lints, fuzzing, property tests, Miri, AES-NI capability token |
| 1.0.0 | Stable API: semantic versioning, semver checks in CI, optional constant-time requirement |
Development
Tests cover:
- the FIPS-197, FIPS 180-2 and NIST SP 800-38A examples;
- the Rijndael Monte Carlo tests, on every AES backend;
- vectors from the RustCrypto project (see
tests/data/rustcrypto): NESSIE AES, NIST CAVP CBC, SHA-2 known answers, HMAC from RFC 4231 and Project Wycheproof, and PBKDF2; - cross-checks against OpenSSL and Python's
hashlib; - AES Crypt fixtures and byte-exact known-answer streams for every format
version (see
tests/data/aescrypt). They come from an independent Python implementation, and the official AES Crypt tool decrypts every one of them; - thousands of random and mutated streams that must never cause a panic;
- property tests (
tests/properties.rs).
Fuzzing (needs nightly and cargo install cargo-fuzz) and Miri:
&&
# targets: decrypt, raw_decrypt, inspect, roundtrip, cbc
MIRIFLAGS=-Zmiri-disable-isolation
To test with the minimum supported Rust version, first pick dependency versions that support it:
CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.