std.crypto
Cryptography here is upstream code, used as-is. Plenty implements no cryptographic algorithm. Every hash, MAC, cipher, signature, key agreement, key derivation and constant-time comparison is the upstream Rust implementation, vendored unmodified and compiled into the runtime: the RustCrypto crates,
subtle, the BLAKE3 team’sblake3, andring. This is deliberate. Constant-time code, side-channel resistance and algorithm correctness stay entirely with those projects and their review and testing. The Plenty side validates arguments, owns buffers, parses key encodings and reports errors. Versions and licences are under Supply and Acknowledgements.
import std.crypto computes BLAKE3, SHA-256, SHA-512 and legacy SHA-1 digests and HMAC tags over
std.bytes buffers, and compares byte buffers in constant time.
The digest and HMAC operations use CryptoError.
The module also provides authenticated encryption, Ed25519 signatures,
X25519 key agreement, and HKDF and
RSA and ECDSA signatures with key import, with
AdvancedError.
The module is available on the supported x86-64 Linux GNU target.
import std.bytes
import std.crypto
import std.encoding
from std.bytes import BytesError
from std.crypto import Algorithm, CryptoError
from std.encoding import EncodingError
def main() -> Result[(), IoError | AllocError | BytesError | CryptoError | EncodingError]:
message = std.bytes.from_text("hello")?
digest = std.crypto.sha256(&message)?
print(std.encoding.hex_text(&digest)?)?
key = std.bytes.from_text("server key")?
tag = std.crypto.hmac_sha256(&key, &message)?
# A receiver recomputes the tag and compares in constant time.
mut check = std.crypto.hmac(Algorithm.Sha256, &key)?
check.update_text("hello")?
print(tag.len(), check.verify(&tag)?)?
Ok(())
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
32 True
Functions
See BLAKE3 hashing and key derivation for its one-shot, incremental keyed-hash and derivation functions.
| Function | Result | Contract |
|---|---|---|
sha256(data: &[u8]) | Result[Bytes, CryptoError] | The 32-byte SHA-256 digest of data |
sha512(data: &[u8]) | Result[Bytes, CryptoError] | The 64-byte SHA-512 digest of data |
sha1(data: &[u8]) | Result[Bytes, CryptoError] | The 20-byte SHA-1 digest of data; legacy, see below |
hmac_sha256(key: &[u8], data: &[u8]) | Result[Bytes, CryptoError] | The 32-byte HMAC-SHA-256 tag |
hmac_sha512(key: &[u8], data: &[u8]) | Result[Bytes, CryptoError] | The 64-byte HMAC-SHA-512 tag |
digest(algorithm: Algorithm) | Result[Digest, CryptoError] | A new incremental digest |
hmac(algorithm: Algorithm, key: &[u8]) | Result[Mac, CryptoError] | A new incremental HMAC under key |
equal(a: &[u8], b: &[u8]) | bool | Whether a and b hold the same bytes, in constant time |
output_size(algorithm: Algorithm) | i64 | 32 for Sha256 and Blake3, 64 for Sha512, 20 for Sha1 |
Algorithm is Sha256, Sha512, Sha1 or Blake3. HMAC supports the SHA
algorithms; BLAKE3 has its own keyed mode. HMAC follows RFC 2104 and accepts a key
of any length, including empty; a key longer than the hash block (64 bytes for
SHA-256 and SHA-1, 128 for SHA-512) is hashed first. Choose a key of at least the output
size from std.secrets.
SHA-1 is broken for collision resistance: an attacker can make two inputs with the same digest. It is here only for protocols that fix it, such as the WebSocket handshake and Git object names. Do not use it for signatures, integrity checks against an adversary, or new designs.
Incremental states
Digest and Mac have the same methods. Feeding input in any number of
pieces gives the same output as one call over the joined input.
| Method | Result | Contract |
|---|---|---|
size() | i64 | The output size in bytes |
update(data: &[u8]) | Result[(), CryptoError] | Absorb data; pass &buffer[a:b] for part of a buffer |
update_text(text: &str) | Result[(), CryptoError] | Absorb the UTF-8 bytes of text |
finish() | Result[Bytes, CryptoError] | The output in a new buffer |
finish_into(target: &mut Bytes, offset: i64) | Result[(), CryptoError] | Write the output at target[offset:offset + size()] |
Mac.verify(expected: &[u8]) | Result[bool, CryptoError] | Finish, and compare the tag with expected in constant time |
update and verify read a &[u8] view, whose bounds
clamp; finish_into takes its offset under the
range rule. Each state finishes once: finish,
finish_into, and verify end it, and every later call is Finished. When
finish cannot allocate its result, the state is left unfinished and the call
may be retried. A tag whose length differs from size(), such as a truncated
tag, does not verify; verify does not accept truncation.
The fields are private, so a state can only be made by the module’s constructors.
Both are redacted: printing one shows std.crypto.Digest(@redacted) or
std.crypto.Mac(@redacted).
Digest.finish_into and Mac.finish_into require the complete digest to fit
at offset. Invalid offsets trap with finish_into: offset outside target buffer
before touching the buffer or finalizing the state. A valid output range on
an already-finished state still returns CryptoError.Finished.
Errors
pub enum CryptoError:
Finished
InvalidKeyLength
UnsupportedAlgorithm
Allocation(AllocError)
| Variant | Meaning |
|---|---|
Finished | The state has already produced its output |
InvalidKeyLength | A BLAKE3 key does not contain exactly 32 bytes |
UnsupportedAlgorithm | hmac was called with Algorithm.Blake3; use native keyed BLAKE3 |
Allocation(cause) | A state or output buffer could not be allocated |
Constant-time comparison
equal and Mac.verify use subtle::ConstantTimeEq (through
Mac::verify_slice for HMAC). Their running time depends on the lengths of
the operands, not on their contents or where they first differ. Lengths are
not secret: comparing buffers of different lengths returns False at once.
Digest has no verify; compare a finished digest with equal.
== on Bytes stops at the first difference. Never use it, or a loop of
get calls, to check a tag, token, or password hash: its timing tells an
attacker how many leading bytes of a guess are right.
The guarantee is that of subtle 2.6: comparisons combine bytes without
branches and pass the result through an optimisation barrier. It is a
best-effort property of compiled code, not a hardware guarantee, and it covers
only the comparison, not how the program reacts to the answer.
Key material
A Mac holds its key’s padded forms or BLAKE3 key state. Finishing it and dropping it overwrite
that state with volatile writes before the memory is released. Plenty does not
scrub the rest:
- The key
Bytespassed tohmacbelongs to the caller, who must protect it; Plenty does not zero memory on drop. - Outputs from
finishare ordinary buffers. - Copies the hashing code makes on the stack while it runs are not cleared.
update_textreads astr, which is never scrubbed.
Ownership and threads
A state owns one runtime object and nothing else; it holds no descriptor and
needs no closing. Inputs are shared loans and finish_into’s target is an
exclusive loan. States cannot be copied. The module has no global state,
passes the ordinary worker-effect checks, and may be used from any thread;
moving a state to a worker moves its ownership.
update and finish_into take buffers, not states, as operands. The
__crypto_* intrinsics behind them are
reserved for the standard library. Given a buffer not
made by std.crypto, they stop the program with
operand is not a std.crypto state.
Costs
| Operation | Allocates | Time |
|---|---|---|
SHA digest, hmac | One state object of 368 bytes plus the allocation header | Constant, plus one hash of the key when it exceeds a block |
update, update_text | Nothing | Linear in the input |
finish_into, verify | Nothing | Final blocks; BLAKE3 also folds its bounded tree stack |
finish | One output buffer | As finish_into |
sha256 and the other one-shots | A state and the output | Linear in the input |
equal | Nothing | Linear in the length; buffers of different lengths return at once |
Input is never copied: update hashes directly from the buffer’s storage.
For BLAKE3 state and finalization costs, see its allocation contract.
SHA-256 uses the x86-64 SHA extensions and SHA-512 uses AVX2 when the
processor reports them at run time, and portable code otherwise; the result is
the same.
Supply
The runtime builds these crates from unmodified copies under
plenty-runtime/vendor, so building the compiler needs no network and an
installed compiler needs neither the sources nor a Rust toolchain:
| Crate | Version | Licence |
|---|---|---|
sha2 | 0.10.9 | MIT or Apache-2.0 |
sha1 | 0.10.6 | MIT or Apache-2.0 |
hmac | 0.12.1 | MIT or Apache-2.0 |
subtle | 2.6.1 | BSD-3-Clause |
digest, block-buffer, crypto-common | 0.10.7, 0.10.4, 0.1.7 | MIT or Apache-2.0 |
generic-array, typenum | 0.14.7, 1.20.1 | MIT; MIT or Apache-2.0 |
cpufeatures, cfg-if | 0.2.17, 1.0.4 | MIT or Apache-2.0 |
Each crate’s licence file is kept with its sources. The same versions are
pinned in Cargo.lock, and a test fails if the two disagree. To update one,
replace its directory with the new release, run cargo update -p CRATE --precise VERSION, and rerun the known-answer tests in
tests/test_std_crypto.rs.
Executables are linked with --gc-sections, and the hashing code is reached
only through its own runtime entry symbols, so a program that does not use
std.crypto contains none of it. The link-args.txt written by
--emit-runtime includes that flag.
Limits
- No password hashing or password KDFs (PBKDF2, Argon2).
- No MD5, SHA-3, BLAKE2, or truncated SHA-512 variants.
- No zero-on-drop buffer type; see Key material.
- One target, x86-64 Linux GNU.
Arithmetic and output bounds
Finalization validates the caller offset before subtracting it from the target length or touching the digest state. Only that bounds-proven arithmetic opts out of recoverable integer errors; invalid offsets still trap, and allocation and finished-state errors keep their existing contracts.
Acknowledgements
-
Wraps the BLAKE3 team’s
blake31.8.7 (CC0-1.0 OR Apache-2.0 OR Apache-2.0 WITH LLVM-exception), with dependency credits. -
Wraps the RustCrypto
sha20.10.9,sha10.10.6 andhmac0.12.1 crates (MIT OR Apache-2.0) andsubtle2.6.1 (BSD-3-Clause), vendored in the runtime. -
Wraps
ring0.17.14 (Apache-2.0 AND ISC) for AEAD, Ed25519, X25519, and HKDF and RSA and ECDSA, using the runtime’s existing vendored copy.