age-plugin-keystore

command module
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 13, 2026 License: MIT Imports: 10 Imported by: 0

README

age-plugin-keystore

An age plugin that stores X25519 private keys in Linux Keyrings using the Secret Service D-Bus API.

Overview

age-plugin-keystore integrates age encryption with Linux keyrings that uses freedesktop Secret Service D-Bus API (like the GNOME Keyring), allowing you to:

  • Generate X25519 key pairs with private keys stored securely in keyring
  • Encrypt files to keystore recipients
  • Decrypt files using keys retrieved from the keyring automatically

Rationale

Ideally, encryption and decryption operations should be performed within a secure enclave, an isolated hardware-protected environment that shields cryptographic operations from the rest of the system. However, Linux user keyrings provide a reasonable alternative for lighter security requirements, offering kernel-managed key storage without the need for expensive hardware security modules or physical tokens.

The keyring offers several advantages over storing keys directly on disk. Keys stored in the keyring are tied to user authentication, they become accessible only after login. Unlike plaintext files that persist indefinitely and may be inadvertently copied through backups or synchronization, keyring-stored keys exist only in kernel memory and never touch the filesystem in unencrypted form.

This protection model emphasizes defense against the remote threat scenario: an attacker who gains access to your repository (through a server breach, backup leak, or misconfigured permissions) will find only encrypted data they cannot decrypt. The keys remain in the keyring on your local machine, separate from the encrypted content.

It is important to understand the limitations of this approach. A malicious process running under your user account on the same machine can potentially access the keyring and decrypt your secrets. While a true hardware secure enclave offers stronger protection against such local attacks, the added complexity and cost make it impractical for many use cases. The Linux keyring strikes a pragmatic balance as it defends well against remote threats and casual local snooping, while accepting that a fully compromised local environment remains difficult to protect against without specialized hardware and systems.

Prerequisites

  • Go 1.22 or later
  • GNOME Keyring or another Secret Service API implementation

Installation

go install github.com/arouene/age-plugin-keystore@latest

Or build from source:

git clone https://github.com/arouene/age-plugin-keystore
cd age-plugin-keystore
go build -o age-plugin-keystore .

Make sure the binary is in your PATH for age to find it automatically:

cp age-plugin-keystore ~/.local/bin/
# or
sudo cp age-plugin-keystore /usr/local/bin/

Usage

Generate a New Key
age-plugin-keystore -g

This will:

  1. Generate a new X25519 key pair
  2. Store the private key in GNOME Keyring
  3. Print the identity string (for identity files) and public key (recipient with embedded key ID)

Example output:

# created: key stored in GNOME Keyring
# key ID: a1b2c3d4e5f6g7h8
# public key: age1keystore1qp...
AGE-PLUGIN-KEYSTORE-1...
Generate a Key with Separate Identity

Use the -s or --separate-identity flag to generate a key pair where the public key is a standard age public key (age1...) instead of a keystore recipient (age1keystore1...):

age-plugin-keystore -g -s
# or
age-plugin-keystore -g --separate-identity

Example output:

# created: key stored in GNOME Keyring
# key ID: a1b2c3d4e5f6g7h8
# public key: age1...
AGE-PLUGIN-KEYSTORE-1...

When to use separate identity mode:

  • When you want to share the public key without revealing you're using a keystore
  • When recipients don't need to know about or have the plugin installed
  • When you want a standard age public key that works with any age implementation
  • When only the decryption side needs the plugin (encryption works with standard age)

Save the identity string to a file:

age-plugin-keystore -g > identity.txt
Encrypt a File

Use the public key (recipient) printed during key generation:

age -r age1keystore1qp... plaintext.txt > encrypted.age

# or

age -R recipients.txt plaintext.txt > encrypted.age
Decrypt a File

Use the identity file created during key generation:

age -d -i identity.txt encrypted.age > plaintext.txt

The plugin will automatically retrieve the private key from the secret service.

List Stored Keys
age-plugin-keystore -l
Delete a Key
age-plugin-keystore -d YOUR_KEY_ID

How It Works

Key Generation
  1. A new X25519 key pair is generated using filippo.io/age
  2. A random 8-byte key ID is generated
  3. The private key (as an age identity string) is stored in Keyring with the key ID as an attribute
  4. The identity and recipient strings are output for use with age
Encryption

There are two modes of operation:

Standard mode (default): When encrypting to a keystore recipient (age1keystore1...), the plugin generates a custom keystore stanza that includes the key ID. This allows the identity to know exactly which key to retrieve from the keyring during decryption.

The encrypted file header will contain:

-> keystore <key-id> <ephemeral-share>
<wrapped-file-key>

Separate identity mode (-g -s): The public key is a standard age public key (age1...). Encryption produces a standard X25519 stanza that any age implementation can create. This mode allows encryption without requiring the plugin on the sender's side, but the identity must try all keys in the keyring during decryption.

Decryption

When decrypting with a keystore identity (AGE-PLUGIN-KEYSTORE-1...):

  1. age invokes the plugin via the standard age plugin protocol
  2. The plugin extracts the key ID from the identity
  3. For keystore stanzas: the plugin checks if the stanza's key ID matches and decrypts directly
  4. For X25519 stanzas (separate identity mode): the plugin retrieves the private key and tries to decrypt
  5. The plugin returns the decrypted file key to age

Security Considerations

  • Private Key Storage: Private keys are stored in GNOME Keyring, which encrypts them at rest
  • Key Access: Keys are accessible to any process running as the same user when the keyring is unlocked
  • Keyring Unlocking: The keyring is typically unlocked automatically when you log in
  • No Passphrases: Unlike standard age keys, keystore keys are protected by the keyring's authentication, not individual passphrases

File Format

Recipient Format
age1keystore1<bech32-encoded-data>

Where the encoded data contains: keyID:X25519-public-key

Identity Format
AGE-PLUGIN-KEYSTORE-1<bech32-encoded-key-id>
Stanza Format (in encrypted files)

Standard mode produces a keystore stanza:

-> keystore <key-id> <base64-ephemeral-share>
<base64-wrapped-file-key>

Separate identity mode produces a standard X25519 stanza:

-> X25519 <base64-ephemeral-share>
<base64-wrapped-file-key>

Development

Building
go build -v ./...
Testing
go test -v ./...

Integration tests

go test -tags=integration -v ./test/
Project Structure
age-plugin-keystore/
├── main.go                     # Plugin entry point
├── go.mod                      # Go module definition
├── README.md                   # This file
└── internal/
    ├── bech32/                 # Bech32 encoding/decoding
    │   ├── bech32.go
    │   └── bech32_test.go
    ├── keystore/               # GNOME Keyring integration
    │   ├── keystore.go
    │   └── keystore_test.go
    └── plugin/                 # age Identity/Recipient implementation
        ├── plugin.go
        └── plugin_test.go

Dependencies

  • Go standard library
  • filippo.io/age - age encryption library and plugin framework
  • github.com/godbus/dbus/v5 - D-Bus bindings for native Secret Service communication
  • github.com/google/go-cmp - for testing

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
bech32
Package bech32 provides bech32 encoding and decoding functions.
Package bech32 provides bech32 encoding and decoding functions.
keystore
Package keystore provides an interface to the GNOME Keyring via the Secret Service D-Bus API using native D-Bus communication.
Package keystore provides an interface to the GNOME Keyring via the Secret Service D-Bus API using native D-Bus communication.
plugin
Package plugin implements the age plugin identity and recipient types for the keystore plugin.
Package plugin implements the age plugin identity and recipient types for the keystore plugin.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL