Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kbs2 rekey #184

Merged
merged 5 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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