Skip to content

Commit

Permalink
kbs2 rekey (#184)
Browse files Browse the repository at this point in the history
* kbs2: `kbs2 rekey`

Adds support for rekeying.

Closes #163.

* README: update docs

* CHANGELOG: record `kbs2 rekey`

* README: Tweak language

* kbs2/command: clippy fix
  • Loading branch information
woodruffw authored Apr 14, 2021
1 parent 2f6d357 commit d240a51
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ All versions prior to 0.2.1 are untracked.

## [Unreleased] - ReleaseDate

### Added

* CLI: `kbs2 rekey` enables users to rekey their entire secret store, re-encrypting
all records with a new secret key. `kbs2 rekey` also handles the chore work of
updating the user's config and related files for the new key.

### Changed

* Contrib: The `kbs2-dmenu-pass` and `kbs2-choose-pass` commands now understand the
Expand Down
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ Quick links:
* [`kbs2 agent flush`](#kbs2-agent)
* [`kbs2 agent unwrap`](#kbs2-agent)
* [`kbs2 rewrap`](#kbs2-rewrap)
* [`kbs2 rekey`](#kbs2-rekey)
* [Configuration](#configuration)
* [Generators](#generators)
* [Customization](#customization)
* [Custom commands](#custom-commands)
* [Hooks](#hooks)
* [Managing your key and master password](#managing-your-key-and-master-password)
* [Why another password manager?](#why-another-password-manager)
* [Technical details](#technical-details)
* [Hacking](#hacking)
Expand Down Expand Up @@ -615,6 +617,41 @@ Change the password on a wrapped key without making a backup of the old wrapped
$ 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:

```bash
$ kbs2 rekey
```

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

```bash
$ kbs2 rekey --no-backup
```

Re-key a different configuration and store:

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

## Configuration

`kbs2` stores its configuration in `<config dir>/kbs2/kbs2.conf`, where `<config dir>` is determined
Expand Down Expand Up @@ -900,6 +937,11 @@ $ 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.

**IMPORTANT**: In a future version of `kbs2`, custom commands will be required to
use the `[commands.ext.<name>]` hierarchy instead of `[commands.<name>]`. Failure to
use this reserved hierarchy may result in loss of custom external command configuration
when using `kbs2` commands that rewrite the config (like `kbs2 rekey`) .

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.
Expand Down Expand Up @@ -972,6 +1014,47 @@ reentrant &mdash; it's all or nothing, intentionally.
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`](#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`](#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, or any fields that aren't
explicitly part of `kbs2`'s internal representation of the config (like external command configs).
Users should be mindful of this when rekeying, and should perform the appropriate manual copies
from the config backup made by `kbs2 rekey`.

* `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](#history).
Expand Down
2 changes: 1 addition & 1 deletion src/kbs2/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ impl Client {

/// Ask the agent whether it has an unwrapped key for the given keyfile.
pub fn query_key(&self, keyfile: &str) -> Result<bool> {
log::debug!("query_key: asking whether client has key for {}", keyfile);
log::debug!("query_key: asking whether agent has key for {}", keyfile);

let req = Request::QueryUnwrappedKey(keyfile.into());
let resp = self.request(&req)?;
Expand Down
2 changes: 0 additions & 2 deletions src/kbs2/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ pub trait Backend {
/// given path.
///
/// NOTE: Like `create_keypair`, this writes an ASCII-armored private component.
/// It also prompts the user to enter a password for encrypting the generated
/// private key.
fn create_wrapped_keypair<P: AsRef<Path>>(path: P, password: SecretString) -> Result<String>;

/// Unwraps the given `keyfile` using `password`, returning the unwrapped contents.
Expand Down
128 changes: 128 additions & 0 deletions src/kbs2/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use atty::Stream;
use clap::ArgMatches;
use clipboard::{ClipboardContext, ClipboardProvider};
use daemonize::Daemonize;
use dialoguer::Confirm;
use nix::unistd::{fork, ForkResult};
use secrecy::{ExposeSecret, Secret};

use std::convert::TryInto;
use std::env;
Expand Down Expand Up @@ -547,3 +549,129 @@ pub fn rewrap(matches: &ArgMatches, config: &config::Config) -> Result<()> {

backend::RageLib::rewrap_keyfile(&config.keyfile, old, new)
}

/// Implements the `kbs2 rekey` command.
pub fn rekey(matches: &ArgMatches, config: &config::Config) -> Result<()> {
log::debug!("attempting to rekey the store");

// This is an artificial limitation; bare keys should never be used outside of testing,
// so support for them is unnecessary here.
if !config.wrapped {
return Err(anyhow!("rekeying is only supported on wrapped keys"));
}

let session: Session = config.try_into()?;

println!(
"This subcommand REKEYS your entire store ({}) and REWRITES your config",
session.config.store
);

if !Confirm::new()
.default(false)
.with_prompt("Are you SURE you want to continue?")
.interact()?
{
return Ok(());
}

if !matches.is_present("no-backup") {
// First, back up the keyfile.
let keyfile_backup: PathBuf = format!("{}.old", &config.keyfile).into();
if keyfile_backup.exists() {
return Err(anyhow!(
"refusing to overwrite a previous key backup during rekeying; resolve manually"
));
}

std::fs::copy(&config.keyfile, &keyfile_backup)?;
println!(
"Backup of the OLD wrapped keyfile saved to: {:?}",
keyfile_backup
);

// Next, the config itself.
let config_backup: PathBuf =
Path::new(&config.config_dir).join(format!("{}.old", config::CONFIG_BASENAME));
if config_backup.exists() {
return Err(anyhow!(
"refusing to overwrite a previous config backup during rekeying; resolve manually"
));
}

std::fs::copy(
Path::new(&config.config_dir).join(config::CONFIG_BASENAME),
&config_backup,
)?;
println!("Backup of the OLD config saved to: {:?}", config_backup);

// Finally, every record in the store.
let store_backup: PathBuf = format!("{}.old", &config.store).into();
if store_backup.exists() {
return Err(anyhow!(
"refusing to overwrite a previous store backup during rekeying; resolve manually"
));
}

std::fs::create_dir_all(&store_backup)?;
for label in session.record_labels()? {
std::fs::copy(
Path::new(&config.store).join(&label),
store_backup.join(&label),
)?;
}
println!("Backup of the OLD store saved to: {:?}", &store_backup);
}

// Decrypt and collect all records.
let records: Vec<Secret<record::Record>> = {
let records: Result<Vec<record::Record>> = session
.record_labels()?
.iter()
.map(|l| session.get_record(&l))
.collect();

records?.into_iter().map(Secret::new).collect()
};

// Get a new master password.
let new_password = util::get_password(Some("NEW master password: "), &config.pinentry)?;

// Use it to generate a new wrapped keypair, overwriting the previous keypair.
let public_key =
backend::RageLib::create_wrapped_keypair(&config.keyfile, new_password.clone())?;

// Dupe the current config, update only the public key field, and write it back.
let config = config::Config {
public_key,
..config.clone()
};
std::fs::write(
Path::new(&config.config_dir).join(config::CONFIG_BASENAME),
toml::to_string(&config)?,
)?;

// Flush the stale key from the active agent, and add the new key to the agent.
// NOTE(ww): This scope is essential: we need to drop this client before we
// create the new session below. Why? Because the session contains its
// own agent client, and the current agent implementation only allows a
// single client at a time. Clients yield their access by closing their
// underlying socket, so we need to drop here to prevent a deadlock.
{
let client = agent::Client::new()?;
client.flush_keys()?;
client.add_key(&config.keyfile, new_password)?;
}

// Create a new session from the new config and use it to re-encrypt each record.
println!("Re-encrypting all records, be patient...");
let session: Session = (&config).try_into()?;
for record in records {
log::debug!("re-encrypting {}", record.expose_secret().label);
session.add_record(record.expose_secret())?;
}

println!("All done.");

Ok(())
}
20 changes: 10 additions & 10 deletions src/kbs2/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub static STORE_BASEDIR: &str = "kbs2";
/// The main kbs2 configuration structure.
/// The fields of this structure correspond directly to the fields
/// loaded from the configuration file.
#[derive(Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Config {
/// The path to the directory that this configuration was loaded from.
///
Expand Down Expand Up @@ -148,7 +148,7 @@ impl Config {
}

/// A newtype wrapper around a `String`, used to provide a sensible default for `Config.pinentry`.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Pinentry(String);

impl Default for Pinentry {
Expand All @@ -164,7 +164,7 @@ impl AsRef<OsStr> for Pinentry {
}

/// The different types of generators known to `kbs2`.
#[derive(Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum GeneratorConfig {
Command(GeneratorCommandConfig),
Expand All @@ -181,7 +181,7 @@ impl GeneratorConfig {
}

/// The configuration settings for a "command" generator.
#[derive(Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct GeneratorCommandConfig {
/// The name of the generator.
pub name: String,
Expand All @@ -191,7 +191,7 @@ pub struct GeneratorCommandConfig {
}

/// The configuration settings for an "internal" generator.
#[derive(Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct GeneratorInternalConfig {
/// The name of the generator.
pub name: String,
Expand All @@ -216,7 +216,7 @@ impl Default for GeneratorInternalConfig {
}

/// The per-command configuration settings known to `kbs2`.
#[derive(Default, Debug, Deserialize, Serialize)]
#[derive(Clone, Default, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct CommandConfigs {
/// Settings for `kbs2 new`.
Expand All @@ -233,7 +233,7 @@ pub struct CommandConfigs {
}

/// Configuration settings for `kbs2 new`.
#[derive(Debug, Default, Deserialize, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct NewConfig {
#[serde(rename = "generate-on-empty")]
Expand All @@ -248,7 +248,7 @@ pub struct NewConfig {
}

/// Configuration settings for `kbs2 pass`.
#[derive(Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct PassConfig {
#[serde(rename = "clipboard-duration")]
Expand Down Expand Up @@ -288,7 +288,7 @@ impl Default for PassConfig {
}

/// Configuration settings for `kbs2 edit`.
#[derive(Debug, Default, Deserialize, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct EditConfig {
pub editor: Option<String>,
Expand All @@ -298,7 +298,7 @@ pub struct EditConfig {
}

/// Configuration settings for `kbs2 rm`.
#[derive(Debug, Default, Deserialize, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct RmConfig {
#[serde(deserialize_with = "deserialize_optional_with_tilde")]
Expand Down
Loading

0 comments on commit d240a51

Please sign in to comment.