Skip to content

zucrypt 0.1.0

Latest

Choose a tag to compare

@pedrobtz pedrobtz released this 13 Oct 21:33
5a43275

First release, and the first on CRAN. A focused set of cryptographic primitives over
raw vectors, backed by a vendored, pinned crypto library, published to R and
to C. The seven R functions are stable, and the static archive is frozen as
ABI 1, after its first consumer -- zuxlsx's decryption of password-protected
workbooks -- was built and tested against it. The registered table is
experimental (see "C interface" below).

R interface

Seven functions, and deliberately no more:

  • crypt_hash() and crypt_hmac() — SHA-1, SHA-256, SHA-384 and SHA-512
    digests and keyed digests, of a raw vector or of everything a connection
    yields: crypt_hash(file(path)) hashes a file in 1 MiB chunks without
    reading it into memory (#11). Raw vectors in, raw vectors out; a character
    value is never given a guessed encoding and never treated as a file name.
    Algorithm names match exactly, with no partial matching and no fallback.
  • crypt_hex() — lower-case hex of a raw vector, converted in C, for
    printing or storing a digest. Hex stays an explicit step at the call site
    (#59).
  • crypt_equal() — constant-time comparison. Unequal lengths return FALSE
    rather than pretending the length is secret.
  • crypt_aes_cbc_encrypt_nopad() and crypt_aes_cbc_decrypt_nopad() —
    AES-128/192/256 in CBC mode, with no padding and no authentication.
    Interoperability tools for formats that specify unauthenticated CBC, not a
    way to encrypt something of your own.
  • crypt_info() — what this build contains, read from the compiled library.

Failures are R conditions classed
c(<specific>, "zucrypt_error", "error", "condition"), mapped from the C
status by enumerator name rather than by number. Every condition carries
algorithm and native_status fields; for AES, algorithm names the cipher
("aes-256-cbc"), or "aes-cbc" when the key is what is wrong. No message
ever contains a key, an IV or plaintext.

Fixed before the tag, from the package review in #51:

  • A condition no longer carries the call that raised it, only the function's
    name. Before, print() on an error and conditionCall() showed the
    caller's expression -- literal keys and plaintext included -- although the
    message itself was clean.
  • crypt_hmac() (and zuc_hmac_*()) accept keys of any length, as
    documented. Keys of 8,192 bytes or more used to fail with
    zucrypt_unsupported_algorithm, because the backend caps an imported key
    there; a key longer than the hash's block is now hashed first, as RFC 2104
    specifies, which gives the same MAC.
  • ?crypt_aes_cbc_nopad now authenticates the IV as well as the ciphertext
    in its encrypt-then-MAC advice and example. A MAC over the ciphertext alone
    let the IV, and so the first plaintext block, be changed undetected.

Renamed before release (#56): the CBC pair was crypt_aes_cbc_encrypt() and
crypt_aes_cbc_decrypt() during development. The _nopad suffix names the
difference from openssl::aes_cbc_encrypt(), which pads, so that code moved
from openssl by changing a prefix can never silently get the padding back
as plaintext. The unsuffixed names are reserved for an openssl-compatible
padded pair. The C interface (zuc_aes_*()) is unchanged.

C interface

Both shapes the zu* family consumes siblings by, at ZUCRYPT_ABI_VERSION 1,
with different stability (?zucrypt_c_api):

  • A static archive, libzucrypt.a, installed to lib/ plus the R
    sub-architecture (lib/x64/ on Windows), for a package that cannot carry
    an Imports: — LinkingTo: only, no runtime dependency, and the consumer
    owns the backend's lifetime through zuc_init()/zuc_shutdown(). This is
    the primary shape, and it is frozen as ABI 1: within major version 1,
    functions may be added, nothing is removed or given a new meaning, and
    enumerator values are permanent.
  • A registered function table, inst/include/zucrypt-r.h, for a package that
    can carry an Imports:. Resolved lazily through zucrypt_api(); test an
    appended field with ZUCRYPT_API_HAS(). Experimental until a package
    uses it.

Settled before the freeze, from the first consumer's report (#43):

  • zuc_alg values are grouped into reserved ranges, published as
    ZUC_ALG_*_FIRST/_LAST (digests, other MACs, ciphers and AEAD, key
    derivation, key families, signatures), so later families never renumber.
  • A buffer pointer may be NULL exactly when its length is 0, for the
    incremental digest and HMAC calls as for CBC -- now stated, and tested.
  • The lifecycle note in zucrypt.h says what an R package needs for
    zuc_shutdown() to run at all: an .onUnload() that calls
    library.dynam.unload(). The archive fixture now does, and its checks
    prove the DLL is released.

inst/include/zucrypt.h compiles standalone as C99 against <stddef.h> and
<stdint.h>, and names no backend type: a consumer never has to reproduce
this package's build configuration.

Both shapes are exercised on Linux, macOS and Windows by consumer fixture
packages, as a real consumer would use them:

  • the table through an Imports: + LinkingTo: package that calls every
    entry;
  • the archive through a LinkingTo-only package whose configure resolves
    libzucrypt.a.

The archive fixture's checks cover:

  • linking through a library path that contains a space;
  • exporting no backend symbol from its own shared object;
  • running beside zucrypt in the same R session, in either load order;
  • running with zucrypt not installed at all.

Settled before any consumer linked the ABI (design revision 3):

  • AES-ECB is not provided. Its one use was Office Standard encryption, which
    zuxlsx does not implement.
  • The backend's key store grows on demand, so the number of live AES and
    HMAC handles is bounded only by memory. The static store it replaces
    allowed 16 live AES handles per process and reported the 17th as an
    allocation failure.
  • ZUC_ERR_NOT_READY (9) is returned by a call made before zuc_init() or
    after the last zuc_shutdown().
  • zuc_info gains hardware_acceleration, appended after the required
    prefix, and crypt_info()$build_flags$hardware_acceleration now reads it
    from the compiled library.
  • TF-PSA-Crypto's licence is installed as licenses/tf-psa-crypto-LICENSE,
    since the archive carries its object code into every consumer.

Backend

Vendored TF-PSA-Crypto 1.1.1, an LTS release, trimmed to 113 files — the
dependency closure of the sources that carry a symbol under this package's
configuration. Installation needs a C99 compiler and nothing else: no system
cryptographic library, no CMake, no Python or Perl, and no network access.

AES runs on the CPU's AES instructions — AES-NI on x86, the Cryptography
Extension on 64-bit Arm — chosen at run time, and on a software fallback
where the CPU has neither. The backend's own security policy warns that the
software fallback leaks key material through cache timing;
crypt_info()$build_flags$aes_implementation reports which path a machine
uses, and ?crypt_aes_cbc_nopad explains when that matters. Every path produces
the same bytes. (Until the review in #51 hardware AES was off everywhere.)

Upstream symbols are hidden. The installed shared object exports exactly one
symbol, R_init_zucrypt, so an independently vendored copy of the same
library in another package cannot bind to this one, and no OpenSSL-ABI name
is exported beside the libcrypto other packages link.

Supported platforms

Checked on every push: Linux, macOS (ARM64) and Windows; R release, oldrel-1
and 4.1; CRAN's clang-23 and GCC-16 containers; the no-Suggests flavour; LTO;
rchk and gctorture; ASan, UBSan, valgrind and GCC's -fanalyzer. Weekly:
i386, musl and aarch64 builds that run the test suite and fail on a WARNING,
and a sweep that fails each allocation the crypt_*() calls make, one run per
allocation — about 250 of them, of which some 77 land in zucrypt's own code —
checking that each surfaces as an error rather than a crash or a wrong answer.

Correctness is checked against published vectors — FIPS 180-2, RFC 2202, RFC
4231, RFC 6234 and NIST SP 800-38A, including the one-million-byte messages
and HMAC keys longer than the block — every one of which is also recomputed
with OpenSSL, an implementation sharing no code with the vendored backend.
Inputs up to 4 MiB are compared with OpenSSL directly, and the iterated hash
Office agile encryption uses is checked against msoffcrypto-tool.

Not in this release

Deliberate omissions, each waiting on a concrete consumer rather than on
effort: authenticated encryption, password-based key derivation (PBKDF2,
HKDF, Argon2), random byte generation, signatures, key serialization, file
hashing and connection wrappers. There is no encrypt_file(password = ), and
there will not be one without a specified authenticated format, a nonce policy
and a key derivation step.

Office/Excel decryption is not here either. It belongs to
zuxlsx, which consumes this package.