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).
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:
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.
Both shapes the zu* family consumes siblings by, at
ZUCRYPT_ABI_VERSION 1, with different stability
(?zucrypt_c_api):
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.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.NULL exactly when its length is
0, for the incremental digest and HMAC calls as for CBC – now stated,
and tested.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):
zuxlsx does not implement.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.licenses/tf-psa-crypto-LICENSE, since the archive carries
its object code into every consumer.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.
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.
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.