kbs2 0.2.6

A secret manager backed by age
kbs2-0.2.6 is not a library.

kbs2

Build Status Crates.io

Warning! kbs2 is beta-quality software! Using kbs2 means accepting that your secrets may be lost or compromised at any time!

kbs2 is a command line utility for managing secrets.

Quick links:

Installation

Packages

If you're running a Debian or Ubuntu distribution on AMD64, you can use the .deb packages attached to the latest release. This is the recommended way to install kbs2 if you are not developing it.

By way of example:

$ wget https://github.com/woodruffw/kbs2/releases/download/v0.2.6/kbs2_0.2.6_amd64.deb
$ sudo dpkg -i kbs2_0.2.6_amd64.deb
# don't forget to request kbs2's dependencies
$ sudo apt-get -f install

Arch Linux

kbs2 can be installed from available AUR packages using an AUR helper. For example,

yay -S kbs2

Other distributions will be supported sooner or later. Help us by looking at the open packaging issues!

Cargo

If you're a Linux user, you'll need some X11 libraries. For Debian-based distributions:

$ sudo apt install -y libxcb-shape0-dev libxcb-xfixes0-dev

kbs2 itself is most easily installed via cargo:

$ cargo install kbs2

After installation, kbs2 is completely ready for use. See the Configuration section for some optional changes that you can make.

Quick start guide

Initialize a new kbs2 configuration:

$ kbs2 init

By default, a fresh kbs2 configuration will store records in $HOME/.local/share/kbs2. Users can override this by passing --store-dir DIR to kbs2 init, or at any point by modifying store in the config itself.

kbs2 init will automatically generate a configuration file and keypair, prompting you for a "master" password.

Note: By default, most kbs2 commands will start the authentication agent (kbs2 agent) in the background if it isn't already running.

Create a new (login) record:

$ kbs2 new amazon
Username: jonf-bonzo
Password: [hidden]

List available records:

$ kbs2 list
amazon
facebook

Pull the password from a record:

$ kbs2 pass -c amazon
# alternatively, pipeline it
$ kbs2 pass facebook | pbcopy

Remove a record:

$ kbs2 rm facebook

kbs2's subcommands are substantially more featured than the above examples demonstrate; run each with --help to see a full set of supported options.

CLI documentation

kbs2 init

Usage

initialize kbs2 with a new config and keypair

USAGE:
    kbs2 init [FLAGS] [OPTIONS]

FLAGS:
    -f, --force                   overwrite the config and keyfile, if already present
    -h, --help                    Prints help information
        --insecure-not-wrapped    don't wrap the keypair with a master password

OPTIONS:
    -s, --store-dir <DIR>    the directory to store encrypted kbs2 records in
                             [default: $HOME/.local/share/kbs2]

Examples

Create a new config and keypair, prompting the user for a master password:

$ kbs2 init

Create a new config and keypair without a master password:

$ kbs2 init --insecure-not-wrapped

Create a new config and keypair in a different location:

$ kbs2 -c /some/config/dir init

Create a new config keypair in a different location and specify a non-default store:

$ kbs2 -c /home/config/dir init --store-dir /some/store/dir

kbs2 new

Usage

create a new record

USAGE:
    kbs2 new [FLAGS] [OPTIONS] <label>

ARGS:
    <label>    the record's label

FLAGS:
    -f, --force       overwrite, if already present
    -g, --generate    generate sensitive fields instead of prompting for them
    -h, --help        Prints help information
    -t, --terse       read fields in a terse format, even when connected to a tty

OPTIONS:
    -G, --generator <generator>    use the given generator to generate sensitive fields
                                   [default: default]
    -k, --kind <kind>              the kind of record to create [default: login]
                                   [possible values: login, environment, unstructured]

Examples

Create a new login record named foobar:

$ kbs2 new foobar
Username: hasdrubal
Password: [hidden]

Create a new environment record named twitter-api, overwriting it if it already exists:

$ kbs2 new -f -k environment twitter-api
Variable: TWITTER_API
Value: [hidden]

Create a new login record named pets.com, generating the password with the default generator:

$ kbs2 new -g pets.com
Username: hasdrubal

Create a new login record named email, getting the fields in a terse format:

$ kbs2 new -t email < <(echo -e "bill@microsoft.com\x01hunter2")

kbs2 list

Usage

list records

USAGE:
    kbs2 list [FLAGS] [OPTIONS]

FLAGS:
    -d, --details    print (non-field) details for each record
    -h, --help       Prints help information

OPTIONS:
    -k, --kind <kind>    list only records of this kind
                         [possible values: login, environment, unstructured]

Examples

List all records, one per line:

$ kbs2 list
foobar
twitter-api
pets.com
email

List (non-sensitive) details for each record. The format of the detailed listing is {record} {kind} {timestamp}.

$ kbs2 list -d
foobar login 1590277900
twitter-api environment 1590277907
pets.com login 1590277920
email login 1590277953

List only environment records:

$ kbs2 list -k environment
twitter-api

kbs2 rm

Usage

remove a record

USAGE:
    kbs2 rm <label>

ARGS:
    <label>    the record's label

FLAGS:
    -h, --help    Prints help information

Examples

Remove the foobar record:

$ kbs2 rm foobar

kbs2 dump

Usage

dump a record

USAGE:
    kbs2 dump [FLAGS] <label>

ARGS:
    <label>    the record's label

FLAGS:
    -h, --help    Prints help information
    -j, --json    dump in JSON format

Examples

Dump the twitter-api record:

$ kbs2 dump twitter-api
Label twitter-api
Kind environment
Variable TWITTER_API
Value 92h2890fn83fb2378fbf283bf73fbxkfnso90

Dump the pets.com record in JSON format:

$ kbs2 dump -j pets.com | json_pp
{
   "timestamp" : 1590363392,
   "label" : "pets.com",
   "body" : {
      "fields" : {
         "username" : "hasdrubal",
         "password" : "hunter2"
      },
      "kind" : "Login"
   }
}

kbs2 pass

Usage

get the password in a login record

USAGE:
    kbs2 pass [FLAGS] <label>

ARGS:
    <label>    the record's label

FLAGS:
    -c, --clipboard    copy the password to the clipboard
    -h, --help         Prints help information

Examples

Get the password for the pets.com record:

$ kbs2 pass pets.com
hunter2

Copy the password for the pets.com record into the clipboard:

$ kbs2 pass -c pets.com

kbs2 env

Usage

get an environment record

USAGE:
    kbs2 env [FLAGS] <label>

ARGS:
    <label>    the record's label

FLAGS:
    -h, --help          Prints help information
    -n, --no-export     print only VAR=val without `export`
    -v, --value-only    print only the environment variable value, not the variable name

Examples

Get an environment record in export-able form:

$ kbs2 env twitter-api
export TWITTER_API=92h2890fn83fb2378fbf283bf73fbxkfnso90

Get just the value in an environment record:

$ kbs2 env -v twitter-api
92h2890fn83fb2378fbf283bf73fbxkfnso90

kbs2 edit

Usage

modify a record with a text editor

USAGE:
    kbs2 edit [FLAGS] <label>

ARGS:
    <label>    the record's label

FLAGS:
    -h, --help                  Prints help information
    -p, --preserve-timestamp    don't update the record's timestamp

Examples

Open the email record for editing:

$ kbs2 edit email

Open the email record for editing with a custom $EDITOR:

$ EDITOR=vim kbs2 edit email

kbs2 generate

Usage

generate secret values using a generator

USAGE:
    kbs2 generate [generator]

ARGS:
    <generator>    the generator to use [default: default]

FLAGS:
    -h, --help    Prints help information

Examples

Generate a secret using the default generator:

$ kbs2 generate
rrayxfky-81x=h6i

Generate a secret using a generator named pwgen:

$ kbs2 generate pwgen
iit4wie6faeL4aiyupheec5Xochosero

kbs2 agent

Usage

run the kbs2 authentication agent

USAGE:
    kbs2 agent [FLAGS] [SUBCOMMAND]

FLAGS:
    -F, --foreground    run the agent in the foreground
    -h, --help          Prints help information

SUBCOMMANDS:
    flush     remove all unwrapped keys from the running agent
    help      Prints this message or the help of the given subcommand(s)
    unwrap    unwrap the current config's key in the running agent

Examples

Run the kbs2 agent in the background, prompting the user to unwrap the current config's key:

$ kbs2 agent

Run the kbs2 agent in the foreground, for debugging purposes:

$ RUST_LOG=debug kbs2 agent --foreground

kbs2 agent flush

Usage

remove all unwrapped keys from the running agent

USAGE:
    kbs2 agent flush [FLAGS]

FLAGS:
    -h, --help       Prints help information
    -q, --quit       quit the agent after flushing

Examples

Remove all keys from the current kbs2 agent:

$ kbs2 agent flush

kbs2 agent unwrap

Usage

unwrap the current config's key in the running agent

USAGE:
    kbs2 agent unwrap

FLAGS:
    -h, --help       Prints help information

Examples

Add the current config's key to the kbs2 agent:

$ kbs2 agent unwrap

Add a custom config's key to the kbs2 agent:

$ kbs2 -c /path/to/config/dir agent unwrap

kbs2 rewrap

Usage

change the master password on a wrapped key

USAGE:
    kbs2 rewrap [FLAGS]

FLAGS:
    -f, --force        overwrite a previous backup, if one exists
    -h, --help         Prints help information
    -n, --no-backup    don't make a backup of the old wrapped key

Examples

Change the password on the wrapped key in the default config:

$ kbs2 rewrap

Change the password on a wrapped key in another config:

$ kbs2 -c /path/to/config/dir rewrap

Change the password on a wrapped key without making a backup of the old wrapped key:

$ kbs2 rewrap -n

Configuration

kbs2 stores its configuration in <config dir>/kbs2/kbs2.conf, where <config dir> is determined by your host system. On Linux, for example, it's ~/.config.

kbs2.conf is TOML-formatted, and might look something like this after a clean start with kbs2 init:

public-key = "age1elujxyndwy0n9j2e2elmk9ns8vtltg69q620dr0sz4nu5fgj95xsl2peea"
keyfile = "/home/william/.config/kbs2/key"
store = "/home/william/.local/share/kbs2"

[commands.pass]
clipboard-duration = 10
clear-after = true
x11-clipboard = "Clipboard"

public-key (default: generated by kbs2 init)

The public-key setting records the public half of the age keypair used by kbs2.

kbs2 init pre-populates this setting; users should not modify it unless also modifying the keyfile setting (e.g., to point to a pre-existing age keypair).

keyfile (default: generated by kbs2 init)

The keyfile setting records the path to the private half of the age keypair used by kbs2.

kbs2 init pre-populates this setting; users should not modify it unless also modifying the public-key setting (e.g., to point to a pre-existing age keypair).

agent-autostart (default: true)

The agent-autostart setting controls whether or not kbs2 attempts to auto-start the authentication agent (kbs2 agent) whenever encryption or decryption operations are requested. By default, kbs2 agent will be started (unless it's already running).

When set to false, kbs2 will report an error if kbs2 agent is not running. In this case, users should configure their system to launch kbs2 agent at login (or some other convenient time).

wrapped (default: true)

The wrapped settings records whether keyfile is a "wrapped" private key, i.e. whether the private key itself is encrypted with a master password.

By default, kbs2 init asks the user for a master password and creates a wrapped key. See the kbs2 init documentation for more information.

store (default: $HOME/.local/share/kbs2)

The store setting records the path to the secret store, i.e. where records are kept.

Users may modify this setting to store their records in a custom directory.

pinentry (default: "pinentry")

The pinentry setting specifies the Pinentry binary to use for passphrase operations (i.e., prompting the user for their master password).

pinentry is a reasonable default for most systems; macOS users may wish to use pinentry-mac instead.

pre-hook (default: None)

The pre-hook setting can be used to run a command before (almost) every kbs2 invocation.

There are currently three cases where the configured pre-hook will not run:

  • kbs2 (i.e., no subcommand)
  • kbs2 agent (and all kbs2 agent subcommands)
  • kbs2 init

All other subcommands, including custom subcommands, will cause the configured pre-hook to run.

Read the Hooks documentation for more details.

post-hook (default: None)

The post-hook setting can be used to run a command after (almost) every kbs2 invocation, on success.

There are currently three cases where the configured post-hook will not run:

  • kbs2 (i.e., no subcommand)
  • kbs2 agent (and all kbs2 agent subcommands)
  • kbs2 init

All other subcommands, including custom subcommands, will cause the configured post-hook to run.

Read the Hooks documentation for more details.

error-hook (default: None)

The error-hook setting can be used to run a command after (almost) every kbs2 invocation, on failure.

There are currently three cases where the configured error-hook will not run:

  • kbs2 (i.e., no subcommand)
  • kbs2 agent (and all kbs2 agent subcommands)
  • kbs2 init

All other subcommands, including custom subcommands, will cause the configured error-hook to run.

The error-hook setting passes a single argument to its hook, which is a string representation of the error that occurred.

Read the Hooks documentation for more details.

reentrant-hooks (default: false)

The reentrant-hooks setting controls whether hooks are run multiple times when a hook itself runs kbs2. By default, hooks are run only for the initial kbs2 invocation.

Read the Reentrancy section of the Hooks documentation for more details.

commands.new.generate-on-empty (default: false)

The commands.new.generate-on-empty setting determines whether or not uses the default generator when the user supplies an empty input for a sensitive field (e.g., a password).

By default, supplying an empty field causes kbs2 to re-prompt for that field. For example:

$ kbs2 new top-secret-login
Username: foobar
Password: [empty]
Password:

with generate-on-empty = true:

$ kbs2 new top-secret-login
Username: foobar
Password: [empty]

$ kbs2 dump top-secret-login
Label: top-secret-login
  Kind: login
  Username: foobar
  Password: 17tlza7b_4}f(m6)

This can be used as a lazy default for when the user forgets to pass --generate to kbs2 new.

commands.new.pre-hook (default: None)

The commands.new.pre-hook setting is like the global pre-hook setting, except that it runs immediately before record creation during kbs2 new (and only kbs2 new).

commands.new.post-hook (default: None)

The commands.new.post-hook setting is like the global post-hook setting, except that it runs immediately after record creation during kbs2 new (and only kbs2 new).

The commands.new.post-hook setting passes a single argument to its hook, which is the label of the record that was just created. For example, the following:

[commands.new]
post-hook = "~/.config/kbs2/hooks/post-new.sh"
# ~/.config/kbs2/hooks/post-new.sh

>&2 echo "[+] created ${1}"

would produce:

$ kbs2 new foo
Username: bar
Password: [hidden]
[+] created foo

commands.pass.clipboard-duration (default: 10)

The commands.pass.clipboard-duration setting determines the duration, in seconds, for persisting a password stored in the clipboard via kbs2 pass -c.

commands.pass.clear-after (default: true)

The commands.pass.clear-after setting determines whether or not the clipboard is cleared at all after kbs2 pass -c.

Setting this to false overrides any duration configured in commands.pass.clipboard-duration.

commands.pass.x11-clipboard (default: "Clipboard")

The commands.pass.x11-clipboard setting determines which clipboard is used on X11.

Valid options are "Clipboard" and "Primary".

commands.pass.pre-hook (default: None)

The command.pass.pre-hook setting is like the global pre-hook setting, except that it runs immediately before record access during kbs2 pass (and only kbs2 pass).

command.pass.post-hook (default: None)

The command.pass.post-hook setting is like the global post-hook setting, except that it runs immediately after record access during kbs2 pass (and only kbs2 pass).

command.pass.clear-hook (default: None)

The command.pass.clear-hook is like the other command.pass hooks, except that it only runs after the password has been cleared from the clipboard.

commands.edit.editor (default: None)

The commands.edit.editor setting controls which editor is used when opening a file with kbs2 edit. This setting takes precedence over the $EDITOR environment variable, which is used as a fallback.

This setting is allowed to contain flags. For example, the following would be split correctly:

[commands.edit]
editor = "subl -w"

commands.edit.post-hook (default: None)

The command.edit.post-hook setting is like the global post-hook setting, except that it runs immediately after record editing during kbs2 edit (and only kbs2 edit).

commands.rm.post-hook (default: None)

The command.rm.post-hook setting is like the global post-hook setting, except that it runs immediately after record removal during kbs2 rm (and only kbs2 rm).

Generators

kbs2 supports generators for producing sensitive values, allowing users to automatically generate passwords and environment variables.

Generators come in two flavors: "command" generators and "internal" generators. Both are configured as entries in [[generators]].

The following configures two generators: a "command" generator named "pwgen" that executes pwgen to get a new secret, and an "internal" generator named "hexonly" that generates a secret from the configured alphabet and length.

[[generators]]
name = "pwgen"
command = "pwgen 16 1"

[[generators]]
name = "hexonly"
alphabet = "0123456789abcdef"
length = 16

These generators can be used with kbs2 new:

# Notice: the user is not prompted for a password
$ kbs2 new -gG hexonly pets.com
Username: catlover2000

Customization

Beyond the configuration above, kbs2 offers several avenues for customization.

Custom commands

kbs2 supports git-style subcommands, allowing you to easily write your own.

For example, running the following:

$ kbs2 frobulate --xyz

will cause kbs2 to run kbs2-frobulate --xyz. Custom commands are allowed to read from and write to the config file under the [commands.<name>] hierarchy.

When run via kbs2, custom commands receive the following environment variables:

  • KBS2_CONFIG_DIR: The path to the configuration directory that kbs2 itself was loaded with. Subcommands can use this path to read the current configuration file or any other content stored in the configuration directory.
  • KBS2_STORE: The path to the secret store.
  • KBS2_SUBCOMMAND: Always set to 1. This can be used to determine whether a subcommand was run via kbs2 (e.g. kbs2 foo) versus directly (e.g. kbs2-foo).
  • KBS2_MAJOR_VERSION, KBS2_MINOR_VERSION, KBS2_PATCH_VERSION: The major, minor, and patch numbers for the version of kbs2 that executed this subcommand. Subcommands can use these numbers to enforce running under a minimum (or maximum) version of kbs2.

The contrib/ext-cmds directory contains several useful external commands.

Hooks

kbs2 exposes hook-points during the lifecycle of an invocation, allowing users to inject additional functionality or perform their own bookkeeping.

The hook API

All hooks, whether pre- or post-, have the following behavior:

  • Hooks do not inherit stdin or stdout from the parent kbs2 process
  • Hooks do inherit stderr from the parent process, and may use it to print anything they please
  • Hooks always run from the store directory
  • Hooks are run with KBS2_HOOK=1 in their environment and with KBS2_CONFIG_DIR set to the configuration directory that the original kbs2 command was loaded with
  • An error exit from a hook (or failure to execute) causes the entire kbs2 command to fail

Hooks may introduce additional behavior, so long as it does not conflict with the above. Any additional hook behavior is documented under that hook's configuration setting.

Reentrancy

kbs2's hooks are non-reentrant by default.

To understand what that means, imagine the following hook setup:

pre-hook = "~/.config/kbs2/hooks/pre.sh"
# ~/.config/kbs2/hooks/pre.sh

kbs2 some-other-command

and then:

$ kbs2 list

In this setting, most users would expect pre.sh to be run exactly once: on kbs2 list.

However, naively, it ought to execute twice: once for kbs2 list, and again for kbs2 some-other-command. In other words, naively, hooks would reenter themselves whenever they use kbs2 internally.

Most users find this confusing and would consider it an impediment to hook writing, so kbs2 does not do this by default. However, should you wish for reentrant hooks, you have two options:

  • You can set reentrant-hooks to true in the configuration. This will make all hooks reentrant — it's all or nothing, intentionally.
  • You can unset or otherwise delete the KBS2_HOOK environment variable in your hook before running kbs2 internally. This allows you to control which hooks cause reentrancy. Beware: KBS2_HOOK is an implementation detail! Unset it at your own risk!

Why another password manager?

No good reason. See the history section.

Technical details

Threat model

kbs2's threat model is similar to that of most password and secret managers. In particular:

  • kbs2 does not attempt to defend against the root user or arbitrary code executed by the current user.
  • kbs2 tries to avoid operations that would result in secret material (i.e. the private key and the decrypted contents of records) being saved or cached on disk, but does not attempt to present the consumers of secret material from doing so.
  • kbs2, by default, attempts to prevent offline private key extraction by encrypting the private key at rest with a master password. kbs2 does not attempt to prevent the user from mishandling their master password.

Cryptography

kbs2 does not implement any cryptography on its own — it uses only the cryptographic primitives supplied by an age implementation. In particular, kbs2 uses the rage implementation of age.

The particulars of kbs2's cryptographic usage are as follows:

  • Every kbs2 configuration file specifies a symmetric keypair. The public key is stored in the public-key configuration setting, while the private key is stored in the file referenced by the keyfile setting.
  • By default, kbs2 "wraps" (i.e. encrypts) the private key with a master password. This makes offline key extraction attacks more difficult (although not impossible) and makes the consequences of wrapped private key disclosure less severe. Users may choose to use a non-wrapped key by passing --insecure-not-wrapped to kbs2 init.

Key unwrapping and persistence

As mentioned under Threat Model and Cryptography, kbs2 uses a wrapped private key by default.

Without any persistence, wrapped key usage would be tedious: the user would have to re-enter their master password on each kbs2 action, defeating the point of having a secret manager.

To avoid this, kbs2 establishes persistence of the unwrapped key with an authentication agent: running kbs2 agent will start a daemon in the background, which subsequent kbs2 invocations can connect to (as needed) via a Unix domain socket. By default, running kbs2 agent will prompt the user for the currently configured key's master password. Users can add additional unwrapped keys to their running agent by invoking kbs2 agent unwrap.

Hacking

Hacking on kbs2 is relatively straightforward. To build a fully functional development copy, just use cargo build in the repository root:

$ cargo build
$ ./target/debug/kbs2 --help

Of note: some functionality in the age crate has pathological performance in debug builds. In particular, decryption and key unwrapping are known to be particularly slow.

To avoid this, use a release build:

$ cargo build --release
$ ./target/release/kbs2 --help

Logging

kbs2 uses log and env_logger for logging. You can past RUST_LOG=debug in your environment to enable debug logging:

$ RUST_LOG=debug ./target/release/kbs2 list -k login

See the env_logger documentation for more possible RUST_LOG values.

History

TL;DR: kbs2 is short for "KBSecret 2".

In 2017, I wrote KBSecret as a general purpose secret manager for the Keybase ecosystem.

KBSecret was written in Ruby and piggybacked off of Keybase + KBFS for encryption, storage, and synchronization. It was also extremely flexible, allowing user-defined record types, secret sharing between users and teams, and a variety of convenient and well-behaved CLI tools for integration into my development ecosystem.

Unfortunately, KBSecret was also extremely slow: it was written in obnoxiously metaprogrammed Ruby, relied heavily on re-entrant CLIs, and was further capped by the latency and raw performance of KBFS itself.

Having a slow secret manager was fine for my purposes, but I no longer trust that Keybase (and KBFS) will continue to receive the work they require. I also no longer have the time to maintain KBSecret's (slowly) deteriorating codebase.

kbs2 is my attempt to reproduce the best parts of KBSecret in a faster language. Apart from the name and some high-level design decisions, it shares nothing in common with the original KBSecret. It's only named kbs2 because I'm used to typing "kbs" in my terminal.