Skip to contents

zucrypt provides a focused set of cryptographic functions for R, backed by a bundled library: message digests, HMAC, unauthenticated AES-CBC, constant-time comparison and secure erasure, over raw vectors. The backend is a vendored, pinned subset of TF-PSA-Crypto, the Mbed TLS project’s cryptography library — 113 files of it, configured down to the algorithms above and nothing else. Installing the package needs a C99 compiler and nothing else: no system cryptographic library, no Java, no Python, and no network access during installation.

The same primitives are published to other packages’ compiled code twice: as a registered C function table for consumers that can carry an Imports:, and as a static archive (libzucrypt.a) for consumers that cannot.

Today that is the whole surface. openssl and sodium cover far more ground, and a package that needs keys, signatures, certificates or authenticated encryption should use them. Later releases are planned to add randomness, Base64 and familiar openssl-style interfaces for hashing and HMAC, then authenticated encryption and key derivation. Those are plans, not features of this version. Office/Excel decryption lives in zuxlsx, which is the first consumer of the archive, not here.

Usage

Bytes in, bytes out. Raw vectors only — a character value is never guessed at, never treated as a file name, and never given an encoding on your behalf.

library(zucrypt)

digest <- crypt_hash(charToRaw("the quick brown fox"))
digest
#>  [1] 9e cb 36 56 13 41 d1 8e b6 54 84 e8 33 ef ea 61 ed c7 4b 84 cf 5e 6a e1 b8
#> [26] 1c 63 53 3e 25 fc 8f

# Hex is an explicit conversion at the call site, not a default.
crypt_hex(digest)
#> [1] "9ecb36561341d18eb65484e833efea61edc74b84cf5e6ae1b81c63533e25fc8f"

# A keyed digest, and the right way to check one.
key <- as.raw(rep(0x0b, 32))
tag <- crypt_hmac(charToRaw("Hi There"), key)
crypt_equal(tag, crypt_hmac(charToRaw("Hi There"), key))
#> [1] TRUE

Use crypt_equal() rather than identical() whenever one side is a secret: an ordinary comparison stops at the first differing byte, so how long it takes measures how much of the expected value an attacker has guessed.

A connection is hashed as it is read, 1 MiB at a time, so neither a large file nor a download has to fit in memory:

path <- tempfile()
writeBin(charToRaw("the quick brown fox"), path)
identical(crypt_hash(file(path)), digest)
#> [1] TRUE

Any connection that yields bytes works the same way, so a remote file is checked against its published digest without being saved first:

crypt_hex(crypt_hash(url("https://example.org/release.tar.gz")))

crypt_info() reports what the installed build actually contains, read from the compiled library rather than from anything written down in R:

info <- crypt_info()
info$algorithms
#> [1] "sha1"   "sha256" "sha384" "sha512"
info$vendored
#>          source version
#> 1 TF-PSA-Crypto   1.1.1

The AES-CBC functions exist too, and are deliberately not shown here: they add no padding and no authentication, and presenting them beside a hash would suggest they are a general-purpose way to encrypt something. See ?crypt_aes_cbc_nopad before using them.

Getting started walks through every function, including what using AES-CBC correctly takes and what each error means.

Consuming it from C

Other packages can use these primitives without going through R, in either of the two ways the zu* family links siblings:

  • The static archive libzucrypt.a, for a package that cannot carry an Imports:. It is installed to lib/ plus the R sub-architecture (lib/x64/ on Windows), beside the licence of the backend compiled into it. This is the primary shape, and it is frozen as ABI 1: its first consumer, zuxlsx’s decryption of password-protected workbooks, was built and tested against it before the freeze.
  • A registered function table, for a package that can carry an Imports:. Experimental until a package other than a test fixture uses it.

Using zucrypt from C walks through choosing a shape and wiring it up; ?zucrypt_c_api is the reference, including what is and is not promised.

Status

Version 0.1.0. The seven R functions are stable, the static archive is frozen as ABI 1, and the registered table is experimental, as above. Every change to either is recorded in NEWS.md. What is compiled in, where it came from, and how a security fix reaches you are in The vendored backend. The design is in the repository, and the roadmap records how it was built, what was deliberately left out, and what comes next.

Installation

From CRAN:

install.packages("zucrypt")

The development version, from GitHub:

# install.packages("pak")
pak::pak("pedrobtz/zucrypt")