Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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’s blake3, and ring. 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.

FunctionResultContract
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])boolWhether a and b hold the same bytes, in constant time
output_size(algorithm: Algorithm)i6432 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.

MethodResultContract
size()i64The 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)
VariantMeaning
FinishedThe state has already produced its output
InvalidKeyLengthA BLAKE3 key does not contain exactly 32 bytes
UnsupportedAlgorithmhmac 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 Bytes passed to hmac belongs to the caller, who must protect it; Plenty does not zero memory on drop.
  • Outputs from finish are ordinary buffers.
  • Copies the hashing code makes on the stack while it runs are not cleared.
  • update_text reads a str, 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

OperationAllocatesTime
SHA digest, hmacOne state object of 368 bytes plus the allocation headerConstant, plus one hash of the key when it exceeds a block
update, update_textNothingLinear in the input
finish_into, verifyNothingFinal blocks; BLAKE3 also folds its bounded tree stack
finishOne output bufferAs finish_into
sha256 and the other one-shotsA state and the outputLinear in the input
equalNothingLinear 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:

CrateVersionLicence
sha20.10.9MIT or Apache-2.0
sha10.10.6MIT or Apache-2.0
hmac0.12.1MIT or Apache-2.0
subtle2.6.1BSD-3-Clause
digest, block-buffer, crypto-common0.10.7, 0.10.4, 0.1.7MIT or Apache-2.0
generic-array, typenum0.14.7, 1.20.1MIT; MIT or Apache-2.0
cpufeatures, cfg-if0.2.17, 1.0.4MIT 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