73 releases

0.7.3-rc.1 Aug 4, 2023
0.7.2 Mar 5, 2023
0.7.1 Feb 24, 2023
0.6.0 Jun 29, 2022
0.1.3 Jul 18, 2020

#221 in Cryptography

MIT license

165KB
2.5K SLoC

kbs2

CI Crates.io Packaging status

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

kbs2 is available via a variety of official and community-supplied packages.

See the matrix below for a list of repositories containing kbs2.

Packaging status

These packages are the recommended way to install kbs2 if you are not developing it.

Debian/Ubuntu

This is an official package.

If you're running a Debian or Ubuntu distribution on AMD64, you can use the .deb packages attached to the latest release.

By way of example:

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

Arch Linux

This is a community-maintained package.

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!

Nix

This is a community-maintained package.

kbs2 can be installed through Nix:

$ nix-env -iA nixpkgs.kbs2

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
    -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? **********

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]
[Press [enter] to auto-generate]

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

$ kbs2 new pets.com
? Username? catlover1312
? Password?
[Press [enter] to auto-generate]

Entering nothing in the password prompt will cause kbs2 to generate a password using the "default" generator. You can use the --generator option to specify a different generator, if you have another one configured.

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

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

When in "terse" mode, kbs2 expects fields to be separated by \x01 (ASCII SOH) characters.

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 one or more records

USAGE:
    kbs2 rm <label>...

ARGS:
    <label>...    the labels of the records to remove

FLAGS:
    -h, --help    Prints help information

Examples

Remove the foobar record:

$ kbs2 rm foobar

kbs2 rename

Usage

rename a record

Usage: kbs2 rename [OPTIONS] <old-label> <new-label>

Arguments:
  <old-label>  the record's current label
  <new-label>  the new record label

Options:
  -f, --force  overwrite, if already present
  -h, --help   Print help

Examples

Rename the foo record to bar:

$ kbs2 rename foo bar

Rename foo to bar, even if bar already exists:

$ kbs2 rename --force foo bar

kbs2 dump

Usage

dump one or more records

USAGE:
    kbs2 dump [FLAGS] <label>...

ARGS:
    <label>...    the labels of the records to dump

FLAGS:
    -h, --help    Prints help information
    -j, --json    dump in JSON format (JSONL when multiple)

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"
   }
}

Dump multiple records, demonstrating JSONL:

$ kbs2 dump -j carthage roma
{"timestamp":1590363392,"label":"bepis","body":{"kind":"Login","fields":{"username":"hamilcar","password":"ihatecato"}}}
{"timestamp":1590363392,"label":"conk","body":{"kind":"Login","fields":{"username":"cato","password":"carthagodelendaest"}}}

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 query

Usage

ask the current agent whether it has the current config's key

USAGE:
    kbs2 agent query

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

kbs2 agent query exits with a few discrete codes to signal the query status:

  • 0: query succeeded, agent is running and has a keypair for the config's public key
  • 1: query failed, agent is running but does not have the queried keypair
  • 2: query failed, agent is running but the keypair isn't managed by the agent (i.e., it's an unwrapped keypair)
  • 3: query failed, agent is not running

All other error codes should be treated as an unspecified error that prevented a query.

Examples

Query the agent for the current config:

$ kbs2 agent query && echo "success" || echo "failure"

Query the agent for another config's keypair:

$ kbs2 -c /some/other/config agent query

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

kbs2 rekey

Usage

re-encrypt the entire store with a new keypair and master password

USAGE:
    kbs2 rekey [FLAGS]

FLAGS:
    -h, --help         Prints help information
    -n, --no-backup    don't make a backup of the old wrapped key, config, or store

Examples

Re-key the default config and its store:

$ kbs2 rekey

Re-key without making backups of the original keyfile, config, and store (not recommended):

$ kbs2 rekey --no-backup

Re-key a different configuration and store:

$ kbs2 -c /some/other/kbs2/conf/dir rekey

kbs2 config

Usage

interact with kbs2's configuration file

USAGE:
    kbs2 config <SUBCOMMAND>

OPTIONS:
    -h, --help    Print help information

SUBCOMMANDS:
    dump    dump the active configuration file as JSON
    help    Print this message or the help of the given subcommand(s)

kbs2 config dump

Usage

dump the active configuration file as JSON

USAGE:
    kbs2 config dump [OPTIONS]

OPTIONS:
    -h, --help      Print help information
    -p, --pretty    pretty-print the JSON

Examples

Dump the current configuration as JSON:

$ kbs2 config dump

# pretty-print the dumped JSON
$ kbs2 config dump --pretty

Configuration

kbs2 stores its configuration in <config dir>/kbs2/config.toml, where <config dir> is determined by the the XDG basedir specification. On Linux, it's probably ~/.config/kbs2.

NOTE: If config.toml isn't found in a configuration directory, kbs2 attempts to use kbs2.conf in the same directory. This is for backwards compatibility, and will be removed once kbs2 has its first stable release.

config.toml 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

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.default-username (default: None)

The commands.new.default-username setting allows the user to specify a default username for logins created with kbs2 new.

When specified, kbs2 new's username prompt will fill in the default when the user presses only [enter].

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? **********
[+] 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.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).

The label of each record removed by kbs2 rm is passed as a separate argument to the post-hook.

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

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

The record's old and new names are passed as separate arguments to the post-hook, in that order.

Generators

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

Generators are configured as entries in [[generators]].

The following configures an generator named "hexonly" that generates a secret from the configured alphabet and length.

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

By default, kbs2's configuration includes a default generator that looks something like this:

[[generators]]
name = "default"
# kbs2 samples from each alphabet, to ensure a good distribution of symbols
alphabets = [
    "abcdefghijklmnopqrstuvwxyz",
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    "0123456789",
    "(){}[]-_+=",
]
length = 16

These generators can be used with kbs2 new. For example, the following will use the hexonly generator when the user presses [enter] instead of manually entering a password.

$ kbs2 new -G hexonly pets.com
? Username? catlover2000
? Password?
[Press [enter] to auto-generate]

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.ext.<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.
    • NOTE: Subcommands are encouraged to use kbs2 config dump to read the configuration state instead of attempting to find the correct file manually.
  • 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!

Managing your key and master password

Rewrapping and rekeying

kbs2 supports two basic options for managing the (wrapped) key that encrypts all records in the secret store: rewrapping and rekeying.

Rewrapping means changing the password on your wrapped key. Rewrapping does not modify the underlying key itself, which means that your individual records in the store do not change. Rewrapping is done with the kbs2 rewrap command.

You should rewrap under the following (non-exhaustive) conditions:

  • You're doing a routine update of your master password
  • You believe that your master password has been disclosed, but not the underlying wrapped key

Rekeying means changing the wrapped key itself, and consequently re-encrypting every record with the new wrapped key. When rekeying you can choose the same master password as the old key. However, you should choose a new password. Unlike rewrapping, rekeying does change the individual records in your store, and makes them no longer decryptable with your previous key. Rekeying is done with the kbs2 rekey command.

You should rekey under the following (non-exhaustive) conditions:

  • You believe that your underlying wrapped key has been disclosed
  • You're sharing a kbs2 to a new device, and you'd like that device to have its own wrapped key

Rekeying is a more drastic operation than rewrapping: it involves rewriting the keypair, the kbs2 config, and every record in the store. This means it comes with some technical caveats:

  • kbs2 rekey does not preserve the layout of your config file. Users should be mindful of this when rekeying.

  • kbs2 rekey makes a backup of the secret store by copying each record in the store to a backup folder. Anything in the secret store that is not a record (like a metadata or revision control directory, or a hidden file) is not copied during backup. Rekeying causes kbs2 to write the newly encrypted records into the same store, so any non-record members of the store will remain unmodified.

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.

Dependencies

~21–35MB
~529K SLoC