lockbox: Simple File Encryption and Secrets Management in R

Simple file encryption and secret management for R using modern cryptographic tools. Provides functions to encrypt/decrypt files with ‘age’ and manage secrets in encrypted YAML files. Secrets can be easily exported as environment variables for use with APIs and services. Supports both file-based and in-memory key management workflows.

Warning

This package is designed for my (Vincent) personal use. I am not a security expert. This code might be unsafe. Use at your own risk.

Why?

lockbox targets two main use cases:

  1. File Encryption: R users need an easy way to encrypt and decrypt files using simple, modern, secure encryption methods. The package supports both password and key pair-based workflows.
  2. Secrets Management: Many R packages, functions, and services rely on environment variables to retrieve users’ API keys, security tokens, and assorted secrets (ex: LLM APIs, AWS services, database locations, etc.). Users need a secure way to store secrets in an encrypted file, and a convenient way to export those secrets as environment variables. Although there are solutions for this outside R, it is useful to do it within to ensure that variables are accessible in the current R session.

How?

To solve these problems, lockbox provides Rust bindings to the age encryption tool for file encryption and implements a simple secrets management format. The default lockbox format is a simpler, more user-friendly version of SOPS, storing secrets in YAML files where keys remain in plain text but values are encrypted. For power users who need advanced features like key rotation and complex recipient management, full SOPS integration is also supported (see SOPS Integration section below).

age: encryption

age is a simple and modern file encryption tool with small keys, no configuration options, and high security. It is designed to replace tools like GPG for most file encryption tasks.

There are two main encryption strategies with age: passphrase or key pairs.

The first is simplest. A passphrase is assigned when encrypting the file. Then, whenever someone wishes to decrypt the file, they are prompted to supply that same password.

The second strategy relies on a pair of keys:

  1. Public key: a shareable string used for encryption.
  2. Private key: a secret file for decryption.

This situation illustrates the use of key pairs:

  1. Bob wants to send a secret file to Alice.
  2. Alice shares her public key with Bob.
  3. Bob uses Alice’s public key to encrypt the file.
  4. Bob sends the encrypted file to Alice.
  5. Alice uses her private key to decrypt the file.

Anyone with your public key can encrypt files for you, but only you can decrypt them with your private key.

lockbox: organize and export secrets

lockbox implements a simple secrets management system for two main purposes:

  1. Organize secrets in an encrypted “lockbox” file in YAML format where keys are plain text but values are encrypted.
  2. Export the secrets held in a lockbox as environment variables, so that other R processes and functions can access API keys, security tokens, etc.

The lockbox format creates YAML files that look like this:

API_KEY: "age1xyz789encrypted_value_here_abc123"
DATABASE_URL: "age1xyz789another_encrypted_value_def456"
lockbox_created: !expr "2024-01-15 10:30:00 UTC"
lockbox_version: "0.0.1"
lockbox_recipients: 
  - "age1abc123..."

As you can see, the secret keys (like API_KEY) remain readable, but their values are encrypted using age. The file also includes metadata about when it was created, the package version, and which public keys can decrypt it.

Warning

You must never edit your lockbox.yaml file manually. Always use the provided functions to ensure the file remains valid and encrypted.

Installation

The lockbox package uses the age encryption format through a Rust implementation, so you need to install Rust to compile the package. The easiest way to install Rust is through rustup:

# Install Rust (all platforms)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Follow the installation prompts, then restart your terminal

Once Rust is installed, you can install the development version of lockbox from Github:

library(remotes)
install_github("vincentarelbundock/lockbox")

Keys

Our first step is to create a private/public key pair using the key_generate() function. The private key is saved to a file and should be kept secret. The public key can be shared and is used to encrypt data.

library(lockbox)
key <- key_generate("private.key")
key
Age key created at 2025-08-09 18:27:51 
Public key: age1nd0hta5vernhcxzadqjz0ug28w00azkpevcknv9yww9eemzq44rqr0zus4

This command created a local “identity file,” which holds both the public and private keys. The function returns the public key directly as a string.

file.exists("private.key")
[1] TRUE
Warning

Do not share the private.key file. It should be kept secret and secure.

Security Note: The private key file is created with default system permissions, which may be readable by other users. After generating a key, set secure permissions:

# On Unix/Linux/macOS:
Sys.chmod("private.key", "0600")  # Owner read/write only

Use-case 1: Encrypting files

lockbox can encrypt and decrypt arbitrary files. To illustrate, let’s create a file with some text in it.

# write file
cat("Very sensitive data.\n", file = "sensitive.txt")

# make sure we can read it
readLines("sensitive.txt")
[1] "Very sensitive data."

Now, let’s use the public key and the file_encrypt() function to encrypt the file. The .age suffix is added automatically to the file name, and the content becomes gibberish.

file_encrypt(
  input = "sensitive.txt",
  public = key
)

readLines("sensitive.txt.age")
[1] "age-encryption.org/v1"                                                                                                                                           
[2] "-> X25519 2pKNkbTHMOPh19V4yH4PhDE4m/D1qINVPNFfYPo3ayE"                                                                                                           
[3] "hM7CEcaWQ2Oy5D7S9Yxr6gdLUfsj/C9BsXohKMjXJcg"                                                                                                                     
[4] "-> jU$-grease 6obchv9B tPXI"                                                                                                                                     
[5] "kba8dAo2Ks8gzqTMAw"                                                                                                                                              
[6] "--- TOzduonqyTOQzn7wPPOmCibVjubsku0XVAro6GcZA+c"                                                                                                                 
[7] "U\xd30\xee\\\xa2\xee0\xf7|\022\xd8E\xfa\xbd\xb3\xe6\xaf'\xce{֦\x9e\xa0\006\x8f\x85+\x90\x9b\xb5\027\x8e\x83\xe6\xd9\xdfL%\017*\xa8\xdeG\x96\002.I\xd7\022\021\xc9"

Finally, we can decrypt the file using the private key file. The decrypted content is written to the specified output file.

file_decrypt(
  input = "sensitive.txt.age",
  output = "sensitive_decrypted.txt",
  private = "private.key"
)

readLines("sensitive_decrypted.txt")
[1] "Very sensitive data."

Use-case 2: Storing secrets in a lockbox and exporting them as environment variables

Several packages and applications require users to export secrets as environment variables for easy access. For example, you may need to store a security key to access the API of an LLM provider; the location of your private database; or credentials to access AWS services.

Generally speaking, we do not want to store those secrets in plain text files. Instead, we can store them in an encrypted YAML file, and use a helper function to decrypt the file and export environment variables.

First, we define a named list with the values that we wish to store securely. Then, we call secrets_encrypt() to encrypt those secrets into our lockbox file. Again, we use the public key for encryption.

secrets <- list(
  API_KEY = "your-api-key-here",
  DATABASE_URL = "postgresql://user:pass@host:5432/db",
  AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
)

secrets_encrypt(
  lockbox = "lockbox.yaml",
  secrets = secrets,
  public = key
)

Retrieving secrets from a lockbox

Now, we can retrieve all secrets using our private key file.

secrets_decrypt(
  lockbox = "lockbox.yaml",
  private = "private.key"
)
$API_KEY
[1] "your-api-key-here"

$DATABASE_URL
[1] "postgresql://user:pass@host:5432/db"

$AWS_ACCESS_KEY_ID
[1] "AKIAIOSFODNN7EXAMPLE"

Modifying secrets in a lockbox

To modify existing secrets or to add new ones, we can simply call secrets_encrypt() again. In this case, however, we need to supply the private key file because modifying requires us to read the existing secrets.

secrets_encrypt(
  lockbox = "lockbox.yaml",
  secrets = list("API_KEY" = "a-new-api-key"),
  private = "private.key"
)

We see that the API_KEY value has indeed been updated.

secrets_decrypt(
  lockbox = "lockbox.yaml",
  private = "private.key")$API_KEY
[1] "a-new-api-key"

Exporting secrets as environment variables

Finally, we can export all secrets from the lockbox file as environment variables. This is useful when running applications that rely on environment variables for configuration.

secrets_export(
  lockbox = "lockbox.yaml",
  private = "private.key"
)

And we see that the secrets are indeed available in the environment.

Sys.getenv("API_KEY")
[1] "a-new-api-key"
Sys.getenv("DATABASE_URL")
[1] "postgresql://user:pass@host:5432/db"

Enhanced Security: Encrypting Your Private Key

For even more security, you can encrypt your private key file itself using a passphrase. This adds an extra layer of protection - even if someone gains access to your key file, they would need to know the passphrase to use it.

Step 1: Encrypt the private key with a passphrase.

file_encrypt(
  input = "private.key",
  output = "private.key.age"
)
# You will be prompted to enter a secure passphrase

When you run this command, you’ll be prompted to enter a passphrase. Make sure you choose a strong, memorable passphrase as you’ll need it every time you use the encrypted key file.

Step 2: Remove the unencrypted key file.

unlink("private.key")

Step 3: Use the password-protected key file.

Now you can use your password-protected key file with all the same functions. The lockbox package will automatically detect that it’s an encrypted key file and prompt you for the passphrase when needed.

secrets_decrypt(
  lockbox = "lockbox.yaml",
  private = "private.key.age"
)
# You will be prompted for your passphrase

secrets_export(
  lockbox = "lockbox.yaml",
  private = "private.key.age"
)
# You will be prompted for your passphrase

Security Considerations

Warning

Temporary File Handling

There are two cases where lockbox creates temporary files with sensitive data:

  1. When the private key used in secrets_decrypt() or file_decrypt() is itself passphrase-encrypted.
  2. When calling secrets_encrypt() to modify an existing lockbox file.

In both cases, a file is written to disk at tempfile(), and is automatically deleted using on.exit() and unlink() to ensure cleanup on function exit even if an error occurs.

While this approach follows R best practices for temporary file handling, users with heightened security requirements may prefer to run age and sops directly from the command line to maintain full control over key file handling.

SOPS Integration (Optional)

For power users who need advanced secrets management features, lockbox also supports SOPS (Secrets OPerationS), a mature secrets manager from Mozilla. SOPS provides additional features like:

  • Key rotation capabilities
  • Integration with cloud KMS services (AWS KMS, GCP KMS, Azure Key Vault)
  • Support for multiple key types (age, PGP, etc.)
  • Advanced recipient management
  • Integration with CI/CD pipelines

Installing SOPS

To use SOPS features, install the SOPS command line tool:

# macOS
brew install sops

# Windows
choco install sops

# Linux
# Use your distribution's package manager or download from GitHub releases

Using SOPS Mode

When SOPS is installed, lockbox will automatically detect it and use SOPS format for new lockbox files. You can also explicitly force SOPS mode:

# Force SOPS format (requires SOPS to be installed)
secrets_encrypt(
  lockbox = "sops_lockbox.yaml",
  secrets = list(API_KEY = "secret-value"),
  public = key,
  sops = TRUE
)

SOPS-managed files have a different format with additional metadata for key management, but the same secrets_decrypt() and secrets_export() functions work seamlessly with both formats.

If SOPS is not installed, lockbox automatically falls back to its built-in format, ensuring that basic secrets management works out of the box without external dependencies.