summaryrefslogtreecommitdiffstats
path: root/sq
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2021-04-28 10:34:53 +0200
committerJustus Winter <justus@sequoia-pgp.org>2021-04-28 12:10:23 +0200
commitd29b0918cc60e26c3eea097e1b7fc566530b87f0 (patch)
tree695994310da9fa127ce89e8d3b2c3fd6a30e28ca /sq
parentd1202ff498b234dd4da0c1ff76769e68d53bf8b4 (diff)
sq: Implement sq key password.
Diffstat (limited to 'sq')
-rw-r--r--sq/src/commands/key.rs68
-rw-r--r--sq/src/sq-usage.rs51
-rw-r--r--sq/src/sq_cli.rs38
3 files changed, 157 insertions, 0 deletions
diff --git a/sq/src/commands/key.rs b/sq/src/commands/key.rs
index d6f646e7..c48d574d 100644
--- a/sq/src/commands/key.rs
+++ b/sq/src/commands/key.rs
@@ -27,6 +27,7 @@ use crate::decrypt_key;
pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> {
match m.subcommand() {
("generate", Some(m)) => generate(config, m)?,
+ ("password", Some(m)) => password(config, m)?,
("extract-cert", Some(m)) => extract_cert(config, m)?,
("adopt", Some(m)) => adopt(config, m)?,
("attest-certifications", Some(m)) =>
@@ -205,6 +206,73 @@ fn generate(config: Config, m: &ArgMatches) -> Result<()> {
Ok(())
}
+fn password(config: Config, m: &ArgMatches) -> Result<()> {
+ let input = open_or_stdin(m.value_of("certificate"))?;
+ let key = Cert::from_reader(input)?;
+
+ if ! key.is_tsk() {
+ return Err(anyhow::anyhow!("Certificate has no secrets"));
+ }
+
+ // First, decrypt all secrets.
+ let passwords = &mut Vec::new();
+ let mut decrypted: Vec<Packet> = Vec::new();
+ decrypted.push(decrypt_key(
+ key.primary_key().key().clone().parts_into_secret()?,
+ passwords)?.into());
+ for ka in key.keys().subkeys().secret() {
+ decrypted.push(decrypt_key(
+ ka.key().clone().parts_into_secret()?,
+ passwords)?.into());
+ }
+ let mut key = key.insert_packets(decrypted)?;
+ assert_eq!(key.keys().secret().count(),
+ key.keys().unencrypted_secret().count());
+
+ let new_password = if m.is_present("clear") {
+ None
+ } else {
+ let prompt_0 =
+ rpassword::read_password_from_tty(Some("New password: "))
+ .context("Error reading password")?;
+ let prompt_1 =
+ rpassword::read_password_from_tty(Some("Repeat new password: "))
+ .context("Error reading password")?;
+
+ if prompt_0 != prompt_1 {
+ return Err(anyhow::anyhow!("Passwords do not match"));
+ }
+
+ if prompt_0.len() == 0 {
+ // Empty password means no password.
+ None
+ } else {
+ Some(prompt_0.into())
+ }
+ };
+
+ if let Some(new) = new_password {
+ let mut encrypted: Vec<Packet> = Vec::new();
+ encrypted.push(
+ key.primary_key().key().clone().parts_into_secret()?
+ .encrypt_secret(&new)?.into());
+ for ka in key.keys().subkeys().unencrypted_secret() {
+ encrypted.push(
+ ka.key().clone().parts_into_secret()?
+ .encrypt_secret(&new)?.into());
+ }
+ key = key.insert_packets(encrypted)?;
+ }
+
+ let mut output = config.create_or_stdout_safe(m.value_of("output"))?;
+ if m.is_present("binary") {
+ key.as_tsk().serialize(&mut output)?;
+ } else {
+ key.as_tsk().armored().serialize(&mut output)?;
+ }
+ Ok(())
+}
+
fn extract_cert(config: Config, m: &ArgMatches) -> Result<()> {
let input = open_or_stdin(m.value_of("input"))?;
let mut output = config.create_or_stdout_safe(m.value_of("output"))?;
diff --git a/sq/src/sq-usage.rs b/sq/src/sq-usage.rs
index 94938f4e..54da6705 100644
--- a/sq/src/sq-usage.rs
+++ b/sq/src/sq-usage.rs
@@ -348,6 +348,7 @@
//!
//! SUBCOMMANDS:
//! generate Generates a new key
+//! password Changes password protecting secrets
//! extract-cert Converts a key to a cert
//! attest-certifications Attests to third-party certifications
//! adopt Binds keys from one certificate to another
@@ -436,6 +437,56 @@
//! $ sq key generate --userid "<juliet@example.org>" --userid "Juliet Capulet"
//! ```
//!
+//! ### Subcommand key password
+//!
+//! ```text
+//! Changes password protecting secrets
+//!
+//! Secret key material in keys can be protected by a password. This
+//! subcommand changes or clears this encryption password.
+//!
+//! To emit the key with unencrypted secrets, either use `--clear` or
+//! supply a zero-length password when prompted for the new password.
+//!
+//! USAGE:
+//! sq key password [FLAGS] [OPTIONS] [FILE]
+//!
+//! FLAGS:
+//! -B, --binary
+//! Emits binary data
+//!
+//! --clear
+//! Emit a key with unencrypted secrets
+//!
+//! -h, --help
+//! Prints help information
+//!
+//! -V, --version
+//! Prints version information
+//!
+//!
+//! OPTIONS:
+//! -o, --output <FILE>
+//! Writes to FILE or stdout if omitted
+//!
+//!
+//! ARGS:
+//! <FILE>
+//! Reads from FILE or stdin if omitted
+//!
+//!
+//! EXAMPLES:
+//!
+//! # First, generate a key
+//! $ sq key generate --userid "<juliet@example.org>" --export juliet.key.pgp
+//!
+//! # Then, encrypt the secrets in the key with a password.
+//! $ sq key password < juliet.key.pgp > juliet.encrypted_key.pgp
+//!
+//! # And remove the password again.
+//! $ sq key password --clear < juliet.encrypted_key.pgp > juliet.decrypted_key.pgp
+//! ```
+//!
//! ### Subcommand key extract-cert
//!
//! ```text
diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs
index 0948363c..b0ce93d7 100644
--- a/sq/src/sq_cli.rs
+++ b/sq/src/sq_cli.rs
@@ -590,6 +590,44 @@ $ sq key generate --userid \"<juliet@example.org>\" --userid \"Juliet Capulet\"
mandatory if OUTFILE is \"-\". \
[default: <OUTFILE>.rev]"))
)
+ .subcommand(
+ SubCommand::with_name("password")
+ .display_order(105)
+ .about("Changes password protecting secrets")
+ .long_about(
+"Changes password protecting secrets
+
+Secret key material in keys can be protected by a password. This
+subcommand changes or clears this encryption password.
+
+To emit the key with unencrypted secrets, either use `--clear` or
+supply a zero-length password when prompted for the new password.
+")
+ .after_help(
+"EXAMPLES:
+
+# First, generate a key
+$ sq key generate --userid \"<juliet@example.org>\" --export juliet.key.pgp
+
+# Then, encrypt the secrets in the key with a password.
+$ sq key password < juliet.key.pgp > juliet.encrypted_key.pgp
+
+# And remove the password again.
+$ sq key password --clear < juliet.encrypted_key.pgp > juliet.decrypted_key.pgp
+")
+ .arg(Arg::with_name("clear")
+ .long("clear")
+ .help("Emit a key with unencrypted secrets"))
+ .arg(Arg::with_name("output")
+ .short("o").long("output").value_name("FILE")
+ .help("Writes to FILE or stdout if omitted"))
+ .arg(Arg::with_name("binary")
+ .short("B").long("binary")
+ .help("Emits binary data"))
+ .arg(Arg::with_name("key")
+ .value_name("FILE")
+ .help("Reads from FILE or stdin if omitted"))
+ )
.subcommand(SubCommand::with_name("extract-cert")
.display_order(110)
.about("Converts a key to a cert")