agecrypt

R-CMD-check coverage Lifecycle: experimental License: MIT

An idiomatic R interface to the age file encryption format. Encrypt and decrypt raw vectors, files, and strings — to public-key recipients or with a passphrase — with optional ASCII armor.

The cryptography is a vendored copy of the C implementation agec, and randomness comes from the operating system, so the package has no external library dependencies (no OpenSSL, no Go or Rust toolchain, no age binary to install). Files it produces interoperate with the reference age implementation in both directions.

Installation

Install the development version from GitHub:

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

Once released on CRAN:

install.packages("agecrypt")

Either way there is nothing else to install — no system libraries and no external programs.

The mental model

age has two kinds of keys, and this package keeps that vocabulary:

library(agecrypt)

id  <- age_keygen()      # a new identity (keypair)
rec <- age_pubkey(id)    # its recipient (public) string

id
#> <age_identity>
#>   public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

The secret key of a generated identity is created in C and kept there — the age_identity object never exposes it, and its print method shows only the public key. (Identities you supply yourself, as AGE-SECRET-KEY-1… strings or key files, unavoidably exist in R since you pass them in.)

Encrypting and decrypting

Three I/O shapes, all following the same recipients = / identities = pattern.

Raw vectors (in memory, nothing touches disk)

ct <- age_encrypt_raw(charToRaw("hello world"), recipients = rec)
rawToChar(age_decrypt_raw(ct, identities = id))
#> [1] "hello world"

Ideal for secrets pulled from a vault or object store and decrypted without ever writing plaintext to disk:

creds <- readBin("secrets.yaml.age", "raw", 1e6) |>
  age_decrypt_raw(identities = id) |>
  rawToChar() |>
  yaml::yaml.load()

Files (streamed in constant memory)

age_encrypt_file("data.csv", recipients = rec)     # writes data.csv.age
age_decrypt_file("data.csv.age", identities = id)  # writes data.csv

Large files are streamed chunk by chunk in C, so a multi-gigabyte file is never loaded into an R vector. Existing outputs are not overwritten unless you pass overwrite = TRUE.

Strings (ASCII-armored, copy-pasteable)

ct <- age_encrypt_text("db_password_123", recipients = rec)
cat(ct)
#> -----BEGIN AGE ENCRYPTED FILE-----
#> ...
#> -----END AGE ENCRYPTED FILE-----

age_decrypt_text(ct, identities = id)
#> [1] "db_password_123"

age_encrypt_text() is armored by default so the result pastes cleanly into a chat message, a commit, or an .Renviron.

Multiple recipients

Encrypt once, and each recipient can decrypt with their own identity:

age_encrypt_file(
  "backup.tar",
  recipients = c(
    "age1alice…",
    "age1bob…",
    "age1backup…"
  )
)

Key files and identities

Write a key to disk in the standard age key-file format:

age_keygen("~/.config/age/key.txt")

Anywhere an identities = argument appears, you can pass an age_identity object, an inline "AGE-SECRET-KEY-1…" string, or a path to a key file — it is auto-detected per element:

age_decrypt_file("data.csv.age", identities = "~/.config/age/key.txt")
age_decrypt_raw(ct, identities = Sys.getenv("AGE_KEY"))  # inline, no file

You can also keep the identity in the OS keychain via the optional keyring package:

secret <- readLines("~/.config/age/key.txt")
secret <- secret[startsWith(secret, "AGE-SECRET-KEY-1")]
keyring::key_set_with_value("agecrypt", "default", secret)

age_decrypt_file("data.csv.age",
  identities = keyring::key_get("agecrypt", "default"))

A key file may hold several identities; decryption tries each until one matches.

Passphrase encryption

A separate set of verbs (never a passphrase = flag on the recipient-based functions, to keep each call unambiguously one mode):

ct <- age_encrypt_raw_passphrase(charToRaw("secret"), passphrase = "correct horse")
rawToChar(age_decrypt_raw_passphrase(ct, passphrase = "correct horse"))
#> [1] "secret"

age_encrypt_file_passphrase("notes.txt")     # prompts securely (via askpass)
age_decrypt_file_passphrase("notes.txt.age")

If passphrase = NULL and the session is interactive, you are prompted securely via askpass. The scrypt work factor is a plain log_n argument (default 18).

Error handling

Failures are signalled with typed conditions, all inheriting from age_error, so batch callers can tell a wrong key from corrupt ciphertext from a disk problem:

Condition class Raised when
age_error_recipient a recipient string fails to parse
age_error_identity an identity string or key file fails to parse
age_error_encrypt encryption fails after inputs are valid
age_error_decrypt no identity/passphrase matches, or authentication fails
age_error_io a file cannot be read or written
tryCatch(
  age_decrypt_file("data.csv.age", identities = id),
  age_error_decrypt  = function(e) log_and_retry(e),
  age_error_identity = function(e) stop("misconfigured key", call. = FALSE)
)

Function reference

Public key (recipients) Passphrase
raw age_encrypt_raw() / age_decrypt_raw() age_encrypt_raw_passphrase() / age_decrypt_raw_passphrase()
file age_encrypt_file() / age_decrypt_file() age_encrypt_file_passphrase() / age_decrypt_file_passphrase()
text age_encrypt_text() / age_decrypt_text()
keys age_keygen(), age_pubkey(), age_identity(), age_identity_free()

How it works, and why you can trust it

Scope

Supported: X25519 recipients, scrypt passphrases, and ASCII armor — the full age v1 format for the common cases.

Not currently supported: SSH-key recipients (ssh-ed25519 …), and plugin / hardware / post-quantum recipients. These would require new stanza types in the C backend rather than R glue.

Concurrency: encryption and decryption must run on R’s main thread, one operation at a time — the usual way R calls compiled code. The C backend keeps some process-global state, so the functions are not reentrant and must not be called from multiple threads in parallel (e.g. a threaded worker pool). Normal sequential use is unaffected.

See also

License

MIT © the agecrypt authors. The vendored agec C sources are distributed under the 0BSD license; see inst/COPYRIGHTS.