Skip to content

dbohdan/pago

Repository files navigation

pago

Warning

pago is in beta. Using pago carries a greater risk of bugs, security vulnerabilities, and data loss than using mature software.

pago is a command-line password manager. It has the following built in:

  • age public-key and password encryption
  • Git version control of the password store (go-git)
  • A fuzzy finder similar to fzf for choosing entries (go-fuzzyfinder)
  • A multiline text editor for editing encrypted data without writing it to disk (tview)

Description

pago encrypts passwords with one or more public keys using age (pronounced with a hard "g"). The public keys are called "recipients". Recipients can be:

  • age recipients
  • SSH public keys

A private key matching one of the recipient public keys can decrypt the password entry. The private keys are called "identities". Identities can be:

  • age identities
  • SSH private keys

The file with the identities is encrypted with a password, also using age.

pago implements an agent like ssh-agent or gpg-agent. The agent caches the identities. This means you don't have to enter the master password again during a session. pago starts the agent the first time you enter the master password. You can also start and stop the agent manually.

The pago password store format is compatible with passage. It has the following differences:

  • The pago directory is located at $XDG_DATA_HOME/pago/, while passage uses ~/.passage/
  • passage supports an encrypted or an unencrypted identities file; pago only supports encrypted

Threat model

An attacker who gets ahold of your pago directory but not the master password should be unable to access the passwords stored in pago except by brute-forcing the master password.

Motivation and alternatives

My primary password manager is KeePassXC. I use a secondary password manager to access a subset of secrets in cron jobs and scripts and on headless remote systems.

I used pass for this for a time. While I liked the design of pass and found it pleasant to use, I didn't like setting up GPG on a new system. I went looking for a pass replacement based on age because I had replaced GPG with age for encrypting files. The following is the late-2024 shortlist of password managers I compiled before I decided to work on pago. It includes explanations for why I didn't adopt them.

First, I needed the identities encrypted at rest and usable without reentering the password. This ruled out passage, which had no agent, and pa, which didn't support encryption for the identities file. kbs2 didn't integrate with Git. seniorpw matched all of my criteria and was the closest to pass. It is what I would most likely be using if I didn't decide to develop my own. The -k/--key feature in seniorpw later inspired TOML entries and the approach to TOTP.

All of the above password managers are worth your attention. For more options, see "Awesome age".

History

pago is a heavily modified fork of pash (archived). It has been ported from POSIX shell to Tcl to Go and from GPG to age.

Installation

You will need Go 1.24 or later to install pago. Once Go is installed on your system, run the following commands:

# pago and pago-agent are two separate binaries.
# You should install both unless you have a specific reason not to.
go install dbohdan.com/pago/cmd/...@latest

Shell completion files for Bash and fish are available in completions/. To install completions for fish, clone the repository and run install.fish.

You may need to allow pago-agent to lock enough memory.

Supported platforms

  • pago is used by the developer on Linux, NetBSD, and (rarely) OpenBSD.
  • pago is automatically tested on FreeBSD and macOS.
  • pago does not build on Windows.

The pago agent and test suite don't work on Windows. Instead of offering a partial and untested Windows build, the project doesn't support Windows. Windows users interested in pago are encouraged to try it in WSL.

Usage

Initialize the password store

pago init

This will create a new password store, prompt you for a master password, and commit the recipients file to Git.

Using SSH keys

To use pago with an SSH key as an identity, follow these steps. Back up your identities file and install age for the command line before proceeding.

Note that the SSH key must not be encrypted, i.e., must not have a password. If necessary, remove the password with ssh-keygen. pago encrypts identities with a password using age encryption.

You may wish to work with secrets in memory or on an encrypted disk. On Linux with glibc, you normally have /dev/shm/ available as temporary in-memory storage.

  1. Add your SSH public key to .age-recipients. You can have multiple recipients.
# Repeat for every SSH public key.
cat ~/.ssh/id_ed25519.pub >> ~/.local/share/pago/store/.age-recipients

# Re-encrypt the password entries to the new recipients.
pago rekey
  1. Add the corresponding SSH private keys to the encrypted identities file. This is not automated and requires decrypting the file manually using the age command. We are going to use a directory in /dev/shm/ in this example.
# Edit the identities file.
mkdir -p "/dev/shm/pago-$USER-temp/"
age -d -o "/dev/shm/pago-$USER-temp/identities" ~/.local/share/pago/identities

# Repeat for every SSH private key.
# Ensure a line break.
echo >> "/dev/shm/pago-$USER-temp/identities"
cat ~/.ssh/id_ed25519 >> "/dev/shm/pago-$USER-temp/identities"
age -a -e -p -o ~/.local/share/pago/identities "/dev/shm/pago-$USER-temp/identities"

# Restart the agent to load the new identities.
pago agent restart

Add passwords

# Either generate or input a password.
pago add foo/bar

# Generate a random password.
pago add -r foo/bar

# Specify a custom length and character pattern (regular expression).
pago add -l 32 -p '[A-Za-z0-9#$%]' foo/bar

# Input your own password.
pago add -i foo/bar

# Read a multiline secret from stdin.
# This is useful for storing age keys or structured data.
# See the "TOML entries" section for more on the latter.
age-keygen | pago add -m foo/bar

Adding a password creates a Git commit by default.

Access passwords

# Show a password.
pago show foo/bar

# Copy to clipboard (clears after 30 seconds).
pago clip foo/bar

# Copy with a custom timeout (in seconds; 0 to disable).
pago clip -t 20 foo/bar

# List all entries organized in a tree.
pago show

# List entries with a name that matches a regular expression.
pago find fo

# Select an entry interactively using a fuzzy finder.
pago show --pick

# The same as `pago show --pick foo`. Starts the search with `foo`.
pago pick foo

Edit passwords

# Edit a password that already exists.
pago edit foo/bar

# Create a password if it doesn't exist.
pago edit foo/new -f

# Edit without mouse support.
# This makes Termux automatically display the virtual keyboard
# when you tap the terminal.
pago edit --no-mouse foo/bar

Generate passwords

# Generate a password without saving it.
pago generate

# Customize the length and pattern.
pago generate --length 16 --pattern '[a-z0-9]'

Delete passwords

# Delete with confirmation.
pago delete foo/bar

# Force delete without confirmation.
pago delete -f foo/bar

# Pick what password to delete using a fuzzy finder.
pago delete -p foo/bar

By default, this will commit the deletion to Git.

Rename passwords

# Rename an entry.
pago rename foo/bar foo/baz

# Move an entry to a new directory.
pago rename foo/baz qux/quux

This creates a Git commit by default.

TOML entries

pago can store and retrieve structured data in the TOML format. This is useful for storing multiple related values in a single entry, such as API keys, usernames, and URLs.

To create a TOML entry, use pago add --multiline and provide TOML content on standard input. The content must start with the string # TOML.

pago add -m services/my-api <<EOF
# TOML
user = "jdoe"
password = "abcdef"
token = "tok-123"
url = "https://api.example.com"
numbers = [1, 1, 2, 3, 5]
EOF

When you show or clip a TOML entry without specifying a key, pago will use a default key. The default key is password. You can specify a different default key by adding a key default to the TOML entry.

pago add -m services/my-api-custom-default <<EOF
# TOML
default = "api-key"
api-key = "xyz-456"
EOF

pago show services/my-api-custom-default
# => xyz-456

You can retrieve other values from the TOML entry using the -k/--key option with show, clip, and pick. The option can be repeated to access nested keys. To see all available keys (sorted), use the -K/--keys option with show. You can combine this with -k/--key to list keys within a nested table.

# List all keys in the entry.
pago show --keys services/my-api
# => numbers
# => password
# => token
# => url
# => user

# List all keys in a nested table.
pago show --keys -k table entry-with-table
# => key

# You can also pick an entry to list keys from.
pago show -K -p

# Show the user from the TOML entry.
pago show -k user services/my-api
# => jdoe

# Show a nested key.
pago show entry-with-table -k table -k key
# => value

# Show an array.
pago show -k numbers services/my-api
# => [1, 1, 2, 3, 5]

# Copy the key to the clipboard.
pago clip -k key services/my-api

When an entry is parsed as TOML, pago can retrieve scalar values (strings, numbers, booleans) and arrays of scalars. Arrays and scalars other than strings are encoded as TOML for output. pago cannot retrieve tables directly, but it can traverse them to access nested values.

TOTP

pago can generate time-based one-time passwords (TOTP) from a TOML entry. To use this feature, store the otpauth:// URI in a key named otp. The entry must start with # TOML.

pago add -m services/my-service <<EOF
# TOML
user = "jdoe"
otp = "otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example"
EOF

When you use show or clip with the key otp, pago will generate and output a TOTP code.

# Show the TOTP code.
pago show -k otp services/my-service
# => 123456

# Copy the TOTP code to the clipboard.
pago clip -k otp services/my-service

Agent

The agent keeps your identities in memory to avoid repeated password prompts. By default, the agent runs until manually stopped, but you can configure it to automatically expire after a period of inactivity.

# Start the agent automatically when needed.
pago show foo/bar

# Start manually.
pago agent start

# Start with automatic expiration after 1 hour of inactivity.
pago agent start --expire 1h

# By default, the agent locks its memory to prevent secrets from being written to swap.
# You may need to run the command `ulimit -l 100000` to let it lock enough memory.
# Alternatively, you can disable memory locking
# with the environment variable `PAGO_MEMLOCK=0` or the option  `--no-memlock`.
pago agent start --no-memlock

# Run without an agent.
pago -s '' show foo/bar

# Shut down.
pago agent stop

Memory locking

pago-agent defaults to locking the process memory to prevent secrets from being written to swap. Secrets can be recovered from unencrypted swap that was not erased at system shutdown.

pago-agent uses up to 100 MiB of memory on systems where it has been tested. Most operating systems don't allow a process to lock this much memory by default. Additionally, on Free/Net/OpenBSD, the agent apparently needs the limit on locked memory to exceed its virtual memory even though only around 100 MiB is reserved. The amount of virtual memory can be over 1 GiB. (You don't lose 1 GiB of memory.) Configure your system to allow this or set the environment variable PAGO_MEMLOCK=0 to disable locking.

Here is how to allow users to lock more memory on different operating systems. In these examples, we set the limit to 8 GiB.

Linux (systemd)

  1. Create /etc/systemd/system.conf.d/ if it doesn't exist.
  2. Edit /etc/systemd/system.conf.d/limits.conf to contain the following:
[Manager]
DefaultLimitMEMLOCK=8G
  1. Restart your user session.

Linux (other init systems)

  1. Edit /etc/security/limits.conf and add this line:
* hard memlock 8589934592
  1. Restart your user session.

Free/Net/OpenBSD

  1. Edit /etc/login.conf and update the default value of memorylocked:
default:\
	[...]
	:memorylocked=8G:\
	[...]
  1. On FreeBSD only, run the following command as root:
cap_mkdb /etc/login.conf
  1. Restart your user session.

Environment variables

  • PAGO_AGENT: The agent executable. Default: pago-agent.
  • PAGO_CLIP: The command to use to copy text to the clipboard. When empty (default), the command is determined automatically using atotto/clipboard.
  • PAGO_CONFIRM: Whether to ask for a new password twice for confirmation. 0 to disable. Default: 1 (enabled).
  • PAGO_DIR: The pago data directory location. Defaults to:
    • Linux and BSD: ~/.local/share/pago
    • macOS: ~/Library/Application Support/pago
  • PAGO_EXPIRE: Agent expiration time after which it will automatically shut down (Go duration string; for example, 1h30m). Default: no expiration.
  • PAGO_GIT: Whether to use Git. 0 to disable. Default: 1 (enabled).
  • PAGO_LENGTH: The default length of random passwords. Default: 20.
  • PAGO_MEMLOCK: Whether the agent should lock its memory using mlockall(2) to prevent secrets from being written to swap. 0 to disable. Default: 1 (enabled).
  • PAGO_MOUSE: Whether to enable mouse support in the interactive editor. 0 to disable. Default: 1 (enabled).
  • PAGO_PATTERN: The default character pattern (regular expression) for random passwords. Default: [A-Za-z0-9].
  • PAGO_SOCK: The agent socket path. The default path is determined by trying candidate paths until the parent directory of the pago directory exists. ${FOO:-default value} with FOO in capital letters indicates an environment variable; ${foo} indicates an internal pago variable.
    • Linux and BSD:
      • ${XDG_RUNTIME_DIR}/pago/socket
      • /var/run/xdg/${username}/pago/socket on FreeBSD only
      • /run/user/${uid}/pago/socket
      • /var/run/user/${uid}/pago/socket
      • ${TMPDIR:-/tmp}/pago-${username}@${hostname}/socket
    • macOS:
      • ${TMPDIR:-/tmp}/pago-${username}@${hostname}/socket
  • PAGO_TIMEOUT: The default timeout to clear the clipboard. Default: 30 (seconds).

Interactive editor

Screenshot of the editor in a terminal showing a TOML entry. The TOML entry has a password "hunter2" and a test TOTP URL.

The editor is implemented as a text area from the tview library. It has the following key bindings:

Session

  • Ctrl+C: Exit without saving
  • Ctrl+D: Save and exit

Navigation

  • : Move left one character
  • : Move right one character
  • : Move up one row
  • : Move down one row
  • Home/Ctrl+A: Move to the start of the line
  • End/Ctrl+E: Move to the end of the line
  • PgUp/Ctrl+B: Page up
  • PgDn/Ctrl+F: Page down
  • Ctrl+←/Alt+B: Move to the start of the word
  • Ctrl+→/Alt-F: Move to the end of the word
  • Ctrl+Home: Move to the start of the text
  • Ctrl+End: Move to the end of the text

Editing

  • Enter: Insert newline
  • Tab: Insert tab (\t)
  • Backspace/Ctrl+H: Delete the previous character
  • Delete: Delete the next character
  • Alt+Backspace: Delete the previous word
  • Ctrl+W: Delete back to the start of the word
  • Ctrl+K: Delete to the end of the line
  • Ctrl+U: Delete the entire line

Selection

  • Shift + navigation key: Extend selection
  • Ctrl+L: Select the entire text
  • Mouse drag: Select text
  • Left double-click: Select a word

Clipboard

The editor clipboard is synchronized with the system clipboard.

  • Ctrl+Q: Copy selected text
  • Ctrl+X: Cut selected text
  • Ctrl+V: Paste from the clipboard

Undo/Redo

  • Ctrl+Z: Undo
  • Ctrl+Y: Redo

License

MIT. See the file LICENSE.

About

Command-line password manager

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •