Dec 6, 2020 Tags: devblog, kbs2, programming, rust
As of yesterday and beginning with 0.2.1,
kbs2 is officially in its second beta series.
Like with the 0.1.x series, I’ll use this post to explain the features that made it into the 0.2.x tree, as well as important changes and breakages. I’ll also use this post to revise the 1.0.0 roadmap.
If you’re looking for a brief explanation of what
read this first.
As documented in the “cryptography” section,
kbs2 protects the private key used for record encryption by having the user encrypt it (i.e., the
keys itself) with a master password. This encrypted key is called the “wrapped key.”
The additional protection conferred by a wrapped key needs to be balanced against usability: without
a caching or persistence mechanism for the unwrapped key, users would be forced to provide
their master password on each
Before the 0.2.x series,
kbs2 established persistence of the unwrapped key using a
POSIX shared memory object:
mmapthe object and read the unwrapped key
This had a few nice properties (like persistence without running a separate process), but the downsides rapidly became obvious:
Despite being a POSIX API, shared memory is woefully underspecified. In particular, Linux’s implementation is significantly better behaved than macOS’s, leading to a collection of ugly hacks in the name of portability.
POSIX shared memory objects have unintuitive lifetimes: because they’re tied to the kernel,
they outlive the user’s login session and can be accessed in subsequent logins.
kbs2 lock was
supplied so that users could configure their systems to remove any
kbs2 object(s) on logout
(or screensaver activation, or any other event), but I wasn’t a fan of this.
Using shared memory objects meant extensive use of native APIs in Rust, which in turn meant
extensive use of
kbs2 has switched to a more traditional daemon agent architecture:
kbs2 agent(more on that below) runs as a daemon, listening on a Unix domain socket
kbs2invocations act as clients to the agent, querying for keys and requesting that keys be unwrapped
Switching to a separate daemon requires users to do slightly more setup (in that they now
kbs2 agent on login or at some point before they intend to use
kbs2), but this may be
mitigated in a future patch release by having
kbs2 auto-spawn the agent.
As mentioned above, the
kbs2 CLI has grown a new subcommand for running and interacting with
the agent daemon:
To run the agent itself:
1 2 3 4 $ kbs2 agent # alternatively, in the foreground with debugging logs $ RUST_LOG=debug kbs2 agent --foreground
To request that the agent unwrap a key:
1 2 3 4 5 # unwrap the default config's wrapped key $ kbs2 agent unwrap # unwrap specified config's wrapped key $ kbs2 -c /path/to/config/dir agent unwrap
Note: In normal use, you should never have to call
kbs2 agent unwrap — all
commands will perform the equivalent operations internally and will prompt you for the master
password as necessary.
To request that the agent flush all keys from memory:
1 2 3 4 $ kbs2 agent flush # optionally, ask the agent to additionally quit $ kbs2 agent flush --quit
Like the (now removed)
kbs2 lock, this is primarily useful for integration
with screensavers or other system events where the user would prefer that the unwrapped
keys not be present in memory.
kbs2 rewrap is a new utility command for handling a maintenance task: changing the master password
on a wrapped key. Instead of running the manual
rage commands themselves, users can
now simply run:
1 $ kbs2 rewrap
…and they’ll be prompted to enter both their old master password (for unwrapping) and a new
master password (for rewrapping). By default,
kbs2 rewrap also makes a backup of the old wrapped
key, making it harder to accidentally flush your encrypted passwords down the drain.
kbs2 lock and
kbs2 unlock were the primary subcommands for interacting with the POSIX shared
memory mechanism that older versions of
kbs2 used for key persistence. That mechanism has been
removed, so the subcommands were also removed.
kbs2 0.2.x introduces
error-hook, a new top-level execution hook (alongside
error-hook can be used to run a program whenever a
kbs2 subcommand fails, including external
subcommands. It passes a message describing the error to the hook, allowing the hook
to log or display it. An example of this has been added to the contrib section of the
Using it is as simple as setting
error-hook in your
1 error-hook = "/path/to/error-hook-notify"
kbs2 has and continues to use Pinentry (via the excellent
pinentry-rs) for its default password prompts. Before
kbs2 would always select the “default” Pinentry binary, hardcoded as
This is almost always a reasonable choice on Linux systems (where it tends to be a symlink to
the user’s preferred Pinentry), but not on macOS (where
pinentry is usually the
version and not a graphical version like
To remedy this, the 0.2.x series supports the
pinentry config setting. In your config:
1 pinentry = "pinentry-mac"
…will switch all
kbs2 prompts to
pinentry-mac, if available. On failure to locate a
kbs2 will continue to fall back to the (TTY-based)
In terms of core functionality,
kbs2 is roughly where it was with the 0.1.x series —
I’d like to add a couple more convenient features, but there’s nothing fundamentally blocking
a stable 1.0.0 release.
With that being said, here’s what you’re likely to see before a 1.0.0 release:
Ratcheting down of the agent trust model: the agent daemon currently only accepts connections from processes with the same effective UID; future versions may mandate that the connection is coming from a trusted process1.
An additional subcommand,
kbs2 reroll or similar, for changing the wrapped key entirely
(and re-encrypting all records with the new key).
Support for auto-starting the
kbs2 agent daemon, so that users don’t have to configure
kbs2 agent to run at login.
Additional configurable hooks.
Detailed CHANGELOG entries.
This is a little finnicky, but essentially: once we have the client’s PID, we can get the executable that it was spawned from. This should always match (in contents) the path that
kbs2 agent was spawned from, so any mismatch indicates an untrustworthy client. This can be circumvented by an attacker (at least on macOS), but it’s a little more difficult than just running a malicious process as the same user. ↩