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.
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.
age has two kinds of keys, and this package keeps that vocabulary:
- a recipient is a public key (
age1…) you encrypt to; - an identity is the matching secret key (
AGE-SECRET-KEY-1…) you decrypt with.
library(agecrypt)
id <- age_keygen() # a new identity (keypair)
rec <- age_pubkey(id) # its recipient (public) string
id
#> <age_identity>
#> public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8pThe 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.)
Three I/O shapes, all following the same recipients = / identities =
pattern.
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()age_encrypt_file("data.csv", recipients = rec) # writes data.csv.age
age_decrypt_file("data.csv.age", identities = id) # writes data.csvLarge 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.
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.
Encrypt once, and each recipient can decrypt with their own identity:
age_encrypt_file(
"backup.tar",
recipients = c(
"age1alice…",
"age1bob…",
"age1backup…"
)
)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 fileYou 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.
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).
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)
)| 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() |
- Spec-compliant. Passes the official C2SP CCTV age test vectors (both X25519 and scrypt) — known answers decrypt correctly and malformed inputs are rejected.
- Interoperable. Reference
ageCLI fixtures are exercised by the test suite, and reverse interoperability is checked when those fixtures are generated. - Self-contained. ChaCha20-Poly1305, X25519, HKDF, scrypt and SHA-256 are
all vendored; the only OS-specific code is the random-bytes call
(
getentropy/arc4random_buf/BCryptGenRandom). - Careful with secrets. An identity's secret bytes are held in C behind an
opaque pointer, zeroed by a finalizer, and never printed or returned to R by
the
age_identityobject. Secrets and passphrases you pass in as strings, or read from a key file, necessarily live in R for their normal lifetime — the package does not (and cannot) hide those.
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.
- The age specification
- The reference
ageCLI awesome-age— other implementations and tools
MIT © the agecrypt authors. The vendored agec C sources are distributed
under the 0BSD license; see inst/COPYRIGHTS.