crypto
This library provides transport-neutral cryptographic predicates. Some of its functionality requires a backend supporting unbound integer arithmetic.
API documentation
Open the ../../apis/library_index.html#crypto link in a web browser.
Loading
To load the library, load the loader.lgt file:
| ?- logtalk_load(crypto(loader)).
Testing
To test this library, load the tester.lgt file:
| ?- logtalk_load(crypto(tester)).
Some of the official test vectors for PBKDF2 use 4096 and 16777216 iterations. Those test vectors are not used as they that don’t increase code coverage: in PBKDF2, iteration count is a work factor, not an algorithmic requirement. Instead, tests with much smaller number of iterations are used to speedup testing on slower backends without loosing code coverage.
Scope
The crypto object provides with the following public predicates:
random_below/2random_bytes/2hex_bytes/2token_hex/2token_urlsafe/2secure_compare/2hkdf/5pbkdf2/6apr1/3bcrypt/4password_hash/4password_hash_atom/2password_hash_needs_rehash/3verify_password_hash/2
When using a backend that supports unbound integer arithmetic, the following authenticated encryption with associated data and public-key signature public predicates are also available:
xchacha20_poly1305_encrypt/5xchacha20_poly1305_decrypt/5ed25519_keypair/2ed25519_public_key/2ed25519_sign/3ed25519_verify/3x25519_keypair/2x25519_public_key/2x25519_shared_secret/3authenticated_channel_initiate/4authenticated_channel_accept/5authenticated_channel_finalize/3authenticated_channel_encrypt/5authenticated_channel_decrypt/5
The random_below/2 predicate returns a uniformly distributed random
integer greater than or equal to zero and less than the given exclusive
upper bound.
The random_bytes/2 predicate returns a list with the requested
number of random bytes. It tries to read bytes from /dev/urandom
first and falls back to a pseudo-random generator when that source is
unavailable.
The hex_bytes/2 predicate relates hexadecimal atoms with lists of
bytes.
The token_hex/2 and token_urlsafe/2 predicates return either a
lowercase hexadecimal token or a unpadded Base64URL token generated from
the requested number of random bytes.
The secure_compare/2 predicate provides constant-time comparison for
byte sequences represented either as byte lists or atoms.
The hkdf/5 and pbkdf2/6 predicates provide portable key
derivation conforming to RFC 5869 and RFC 8018, respectively,
implemented on top of the existing hashes and hmac libraries.
The apr1/3 and bcrypt/4 predicates compute portable Apache APR1
and bcrypt 2b checksums. Bcrypt accepts passwords of at most 72 bytes,
costs from 4 through 31, and exactly 16 raw salt bytes.
The password_hash/4 predicate generates structured password hashes
using pbkdf2(Hash), bcrypt, or apr1 method descriptors; a
bare hash object is PBKDF2/PKCS#5 shorthand. Options are method-specific
and inapplicable options are rejected. The
password_hash_needs_rehash/3 predicate compares a valid stored term
with the selected target method and its parameters. The
verify_password_hash/2 predicate verifies PBKDF2/PKCS#5, digest,
APR1, and bcrypt terms. The bidirectional password_hash_atom/2
predicate converts canonical $2b$ bcrypt and $apr1$ atoms to and
from their structured terms.
The xchacha20_poly1305_encrypt/5 and
xchacha20_poly1305_decrypt/5 predicates provide authenticated
encryption with associated data (AEAD) using XChaCha20-Poly1305:
ChaCha20 with the 24-byte extended nonce defined by the XChaCha
construction (via HChaCha20 subkey derivation), combined with a Poly1305
authentication tag over the associated data and ciphertext, following
the IETF ChaCha20-Poly1305 construction (RFC 8439). Decryption verifies
the tag with constant-time comparison before decrypting anything, and
fails, without producing any plaintext, if the tag does not match. The
xchacha20/4 predicate exposes the underlying unauthenticated stream
cipher for clients such as PASETO that provide their own authentication.
It must only be used as part of an authenticated construction. These
predicates are only available on backend Prolog compilers supporting
unbounded integer arithmetic, since Poly1305 requires an exact 130-bit
accumulator.
The ed25519_keypair/2, ed25519_public_key/2, ed25519_sign/3,
and ed25519_verify/3 predicates implement EdDSA signatures over
Curve25519 (RFC 8032). The 32-byte seed passed to ed25519_sign/3
(and returned by ed25519_keypair/2) is what RFC 8032 calls the
private key; signing is fully deterministic, with no separate nonce to
generate or protect. ed25519_verify/3 checks the cofactored group
equation [8][S]B = [8]R + [8][k]A, as recommended by RFC 8032, and
additionally requires the public key and the R component of the
signature to be canonically encoded and of large order, rejecting the
seven points of order dividing 8 (matching the hardening most
widely-deployed Ed25519 implementations apply for “strongly binding
signature” security; see Chalkias, Garillot, and Nikolaenko, “Taming the
many EdDSAs”, 2020). S is required to satisfy 0 <= S < L, which
RFC 8032 mandates unconditionally. Signing uses simple double-and-add
scalar multiplication and is not constant-time, so timing side channels
on the secret scalar are possible on the signing path; verification
never touches secret data and is unaffected by that limitation. These
four predicates are only available on backend Prolog compilers
supporting unbounded integer arithmetic, since Curve25519 arithmetic is
carried out modulo a 255-bit prime.
The x25519_keypair/2, x25519_public_key/2, and
x25519_shared_secret/3 predicates implement X25519 Diffie-Hellman
key agreement (RFC 7748). Private keys and public keys are represented
by 32-byte lists. Public-key derivation and shared-secret computation
apply the RFC 7748 scalar clamping rules and mask the most significant
bit of received u-coordinates. Shared-secret computation fails for an
all-zero result, preventing the use of low-order public keys. These
predicates require unbounded integer arithmetic. Although the Montgomery
ladder has fixed control flow and uses arithmetic conditional swaps, the
underlying Prolog big-integer operations are not guaranteed to be
constant-time.
The authenticated channel predicates compose Ed25519 identity
signatures, ephemeral X25519 key agreement, HKDF-SHA-256, and
XChaCha20-Poly1305. The initiator calls
authenticated_channel_initiate/4 with its identity seed and the
responder Ed25519 public key obtained through a trusted out-of-band
mechanism. The responder calls authenticated_channel_accept/5 with
its identity seed, the pinned initiator identity public key, and the
offer. The initiator completes the handshake using
authenticated_channel_finalize/3. Both signatures bind the protocol
version, roles, both identity keys, and the ephemeral keys; the
responder signature also binds the initiator offer signature.
The handshake derives independent initiator-to-responder and
responder-to-initiator chain keys and nonce prefixes. The
authenticated_channel_encrypt/5 and
authenticated_channel_decrypt/5 predicates are pure state
transitions: every successful call returns a replacement channel state.
Messages use strict 64-bit counters and must be received in order. Their
XChaCha20-Poly1305 associated data binds the protocol version,
direction, transcript hash, counter, and caller-provided associated
data. Replay, out-of-order delivery, modified associated data, and
modified ciphertext therefore fail without advancing the state.
Each message derives a one-use message key and replacement directional chain key. This symmetric ratchet protects already processed messages if only the current state is later compromised, provided applications discard all prior immutable state terms. Logtalk cannot guarantee that discarded key material is immediately overwritten in memory. This construction is not a Double Ratchet: compromise of the current state is not healed without a new authenticated ephemeral handshake, and out-of-order message delivery is not supported.
The key-pair predicates use random_bytes/2. If /dev/urandom is
unavailable, that predicate falls back to the library pseudo-random
generator. Applications that require fail-closed cryptographic entropy
must generate private key bytes externally and call
ed25519_public_key/2 or x25519_public_key/2 instead.
Examples
Generate sixteen random bytes:
| ?- crypto::random_bytes(16, Bytes).
Bytes = [42,17,203,91,16,88,121,4,238,75,63,142,7,210,119,55]
yes
Convert bytes to a hexadecimal atom:
| ?- crypto::hex_bytes(Hex, [80,26,206]).
Hex = '501ace'
yes
Derive 32 bytes using HKDF-SHA-256:
| ?- crypto::hkdf(sha256, [1,2,3,4], 32, Bytes, [salt([5,6,7,8]),info([9,10])]).
Bytes = [...]
yes
Compute an Apache APR1 checksum:
| ?- crypto::apr1([112,97,115,115,119,111,114,100], [112,111,114,116,97,98,108,101], Checksum).
Checksum = [107,117,110,70,78,90,57,114,48,81,57,54,51,88,98,115,116,101,79,79,87,46]
yes
Encrypt and authenticate a short message with XChaCha20-Poly1305, then decrypt and verify it:
| ?- Key = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],
Nonce = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23],
crypto::xchacha20_poly1305_encrypt(Key, Nonce, [], [72,105], CiphertextAndTag),
crypto::xchacha20_poly1305_decrypt(Key, Nonce, [], CiphertextAndTag, Plaintext).
Plaintext = [72,105]
yes
Generate an Ed25519 keypair, sign a message, and verify the signature:
| ?- crypto::ed25519_keypair(Seed, PublicKey),
crypto::ed25519_sign(Seed, [72,105], Signature),
crypto::ed25519_verify(PublicKey, [72,105], Signature).
yes
Create an authenticated ephemeral channel between Alice and Bob and send
a message from Alice to Bob. In an application, AlicePublicKey and
BobPublicKey must be obtained through a trusted identity mechanism:
| ?- crypto::ed25519_keypair(AliceSeed, AlicePublicKey),
crypto::ed25519_keypair(BobSeed, BobPublicKey),
crypto::authenticated_channel_initiate(AliceSeed, BobPublicKey, Offer, Pending),
crypto::authenticated_channel_accept(BobSeed, AlicePublicKey, Offer, Response, BobChannel0),
crypto::authenticated_channel_finalize(Pending, Response, AliceChannel0),
crypto::authenticated_channel_encrypt(AliceChannel0, [], [72,105], Message, AliceChannel1),
crypto::authenticated_channel_decrypt(BobChannel0, [], Message, Plaintext, BobChannel1).
Plaintext = [72,105]
yes
The application must retain AliceChannel1 and BobChannel1 for
subsequent messages and discard the corresponding input states.