std/aead
import std/aead to use it. Source: src/std/aead.iyi at v0.18.0.
import std/aead::{AesGcm, ChaCha20Poly1305, AeadError} box = AesGcm.new(key) # 16, 24 or 32 bytes
sealed = box.seal(nonce, plaintext, aad)
box.open(nonce, sealed, aad) # the plaintext, or an AeadErrorseal answers the ciphertext with the 16-byte tag after it, RFC 5116's layout and the one Python's cryptography (AESGCM, ChaCha20Poly1305) and Go's crypto/cipher use, so what one seals the other opens. open checks the tag before it decrypts a byte, compares it in constant time, and answers an AeadError, a value, when it does not match: sealed bytes come from outside, and a forged or damaged one is not a bug in the program reading it. The additional data is authenticated and not encrypted; it defaults to none. seal and open keep their working state on the stack and change nothing in the object, so one AesGcm or ChaCha20Poly1305 serves any number of threads. AES (a block at a time, both directions), ChaCha20 (a key stream XORed over data as it arrives) and Poly1305 (a one-time MAC) are there for a construction of the caller's own; the last two are streams, and one caller's.
A key, a nonce, a plaintext or additional data is a String, whose bytes are taken, or an Array(UInt8); what comes back is a String of bytes. The key and the nonce are the caller's: nothing here draws a random number, so this module reaches no clock and no entropy source (iyi mod reach), and a program picks its nonces from Random::Secure or a counter. A nonce must never be used twice under one key: GCM gives away its hash key, and both give away the XOR of the two plaintexts.
Refused with a sentence, as the caller's mistake: a key of the wrong size (AES 16, 24 or 32 bytes; ChaCha20 and Poly1305 32), a nonce that is not 12 bytes, an AES block that is not 16, a ChaCha20 stream run past its 32-bit block counter, and a Poly1305 used after its final. GCM's nonce is 96 bits only: SP 800-38D (§5.2.1.1) recommends implementations keep to that length, and any other is hashed into a starting counter that can land on another nonce's.
Side channels. A table-lookup AES indexes its tables with key and data bytes, and which cache lines it touched can be measured from another process on the same machine. This one has no table: it is BearSSL's aes_ct64 (Thomas Pornin), four blocks at a time bitsliced into eight 64-bit words, its S-box the 115-gate circuit of Boyar and Peralta (eprint 2009/191), and the key schedule goes through the same circuit. GHASH is BearSSL's ghash_ctmul64: carry-less multiplication done with integer multiplies on operands with holes in them, so no branch and no address depends on a secret; that holds where a 64-bit multiply takes the same time for every operand, which it does on x86_64 and on AArch64's application cores. ChaCha20 and Poly1305 are additions, rotations and XORs (Poly1305 in 26-bit limbs, as poly1305-donna writes it), with the final reduction chosen by a mask. Every tag is compared by OR-ing the differences of all sixteen bytes.
The price of no tables is speed. Measured on one x86_64 Linux machine (a QEMU virtual CPU with AES-NI and without carry-less multiply), --release, sealing 16 MiB: AES-128-GCM 221 MB/s, AES-256-GCM 179 MB/s and ChaCha20-Poly1305 790 MB/s, against 875, 880 and 1581 MB/s from Python's cryptography (OpenSSL, using AES-NI); a plain build sealed 42, 34 and 93 MB/s. Bitsliced AES does the work of four blocks for any count from one to four, so one block alone costs what four do.
What this is not: a key exchange, a signature or TLS; nor a way to keep a key out of memory - keys, round keys and key streams live in the collected heap and on the stack, and nothing wipes them.
AESclass
The AES block cipher, FIPS 197: a 16, 24 or 32-byte key (10, 12 or 14 rounds), one 16-byte block at a time, either way. On its own it is a building block; a message wants a mode, AesGcm.
pub class AES
# FIPS 197's InvCipher: the block `encrypt_block` answered, back.
def decrypt_block(block : String | Array(UInt8)) : String
# FIPS 197's Cipher: 16 bytes in, 16 out.
def encrypt_block(block : String | Array(UInt8)) : String
def initialize(key : String | Array(UInt8))
endAeadErrorstruct
What open answers for sealed bytes it will not open: shorter than a tag, or whose tag does not match.
pub struct AeadError
def initialize(message : String)
def to_s : String
endAesGcmclass
AES in Galois/Counter Mode, NIST SP 800-38D: CTR encryption from the counter after the nonce's own, and a 128-bit tag that is GHASH over the additional data and the ciphertext, each padded to a block, and their lengths in bits, XORed with the key stream of the nonce's counter block. The nonce is 12 bytes, and the counter block is it and a 32-bit counter that starts at 1 (§7.1).
pub class AesGcm
def initialize(key : String | Array(UInt8))
# The plaintext *sealed* came from, when its tag is the one this key,
# *nonce* and *aad* give; else an `AeadError`, and nothing decrypted.
def open(nonce : String | Array(UInt8), sealed : String | Array(UInt8), aad : String | Array(UInt8) = "") : String | AeadError
# The ciphertext of *plaintext*, as long as it, and the 16-byte tag.
def seal(nonce : String | Array(UInt8), plaintext : String | Array(UInt8), aad : String | Array(UInt8) = "") : String
endChaCha20class
The ChaCha20 stream cipher, RFC 8439 §2.4: a 32-byte key, a 12-byte nonce and a 32-bit block counter, which starts at counter. xor answers its data XORed with the key stream and carries on from where it stopped, so a message XORed in pieces is the message XORed whole; the same call decrypts. The counter is the RFC's 32 bits, 256 GiB of key stream; a stream run past block 2^32 - 1 is refused rather than wrapped onto key stream already used.
pub class ChaCha20
def initialize(key : String | Array(UInt8), nonce : String | Array(UInt8), counter : UInt32 = 0_u32)
# *count* bytes from *src* to *dst*, which may be the same bytes.
def xor(src : Pointer(UInt8), dst : Pointer(UInt8), count : Int32) : Nil
def xor(data : String | Array(UInt8)) : String
endChaCha20Poly1305class
ChaCha20 and Poly1305 as an AEAD, RFC 8439 §2.8: the Poly1305 key is the first 32 bytes of block 0's key stream, the plaintext is encrypted from block 1, and the tag covers the additional data and the ciphertext, each padded with zeros to 16 bytes, then their lengths as 64-bit little-endian numbers.
pub class ChaCha20Poly1305
def initialize(key : String | Array(UInt8))
# The plaintext *sealed* came from, when its tag is the one this key,
# *nonce* and *aad* give; else an `AeadError`, and nothing decrypted.
def open(nonce : String | Array(UInt8), sealed : String | Array(UInt8), aad : String | Array(UInt8) = "") : String | AeadError
# The ciphertext of *plaintext*, as long as it, and the 16-byte tag.
def seal(nonce : String | Array(UInt8), plaintext : String | Array(UInt8), aad : String | Array(UInt8) = "") : String
endPoly1305class
Poly1305, RFC 8439 §2.5: a 16-byte MAC under a 32-byte one-time key, r (clamped) and s. One key authenticates one message: a second message under it lets whoever sees both tags forge a third, which is why the AEAD derives a fresh key from each nonce, and why there is no reset. Poly1305.mac(key, data) is one piece; new, update as the message arrives, final once is the same in pieces. A tag someone sent is compared with std/crypto's Subtle.constant_time_compare, not ==.
pub class Poly1305
# The 16-byte tag. A last block shorter than 16 bytes carries its own
# 1 bit after its bytes (§2.5.1), rather than the 2^128 of a whole one.
def final : String
def initialize(key : String | Array(UInt8))
def self.mac(key : String | Array(UInt8), data : String | Array(UInt8)) : String
def update(data : Pointer(UInt8), count : Int32) : self
def update(data : String | Array(UInt8)) : self
endError for AeadErrorimpl
impl Error for AeadError
def message : String
end
Authenticated encryption, written in iyi: AES (FIPS 197) and AES-GCM (NIST SP 800-38D), ChaCha20 and Poly1305, and the two together as ChaCha20-Poly1305 (RFC 8439).