diff --git a/Cargo.lock b/Cargo.lock index d63f587a..b63e1209 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -857,6 +857,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1980,7 +1989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -3140,37 +3149,50 @@ dependencies = [ ] [[package]] -name = "sequoia-gpg-agent" -version = "0.3.1" +name = "sequoia-directories" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29f0e04d9d161c65384a02282380ceaf1b506e768cab95837a428439c2596a4" +checksum = "b01dd48960c5cf8617ab77e5c9f8ebeb55a1d694e3eabf830fa70453ffa637d5" +dependencies = [ + "anyhow", + "directories", + "same-file", + "tempfile", + "thiserror", +] + +[[package]] +name = "sequoia-gpg-agent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c929d572dee98c48d286cef43e2ade4201962f3454c015f52bf43b5a8e40d42" dependencies = [ "anyhow", "chrono", "futures", + "lalrpop", + "lalrpop-util", "libc", - "sequoia-cert-store", "sequoia-ipc", "sequoia-openpgp", "stfu8", + "tempfile", "thiserror", "tokio", ] [[package]] name = "sequoia-ipc" -version = "0.34.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d67c3c28da4a3483fa969e6a6cde9e5487fe44632b5f285224d79df05bd99dd4" +checksum = "b4a7e644ec9e1055fde8dcdaa65c58fa4636c615b5e955a9b1942444145e308a" dependencies = [ "anyhow", "buffered-reader", "capnp-rpc", - "crossbeam-utils", "ctor", "dirs", "fs2", - "futures", "lalrpop", "lalrpop-util", "lazy_static", @@ -3188,9 +3210,9 @@ dependencies = [ [[package]] name = "sequoia-keystore" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5dd7c49aa034452f70772ecb86fc4ecff3bd41b7a92590fe1925fc5cceea2d9" +checksum = "76eaefe78ca373001382f2434ca021145d9b702ba52c04cebc398b8e5956d28a" dependencies = [ "anyhow", "capnp", @@ -3200,6 +3222,7 @@ dependencies = [ "lazy_static", "log", "paste", + "sequoia-directories", "sequoia-ipc", "sequoia-keystore-backend", "sequoia-keystore-gpg-agent", @@ -3212,9 +3235,9 @@ dependencies = [ [[package]] name = "sequoia-keystore-backend" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36e37e531a03fb52e8f1281e0b221ddbf2f1df2d51c3d4d9bca5551fd99f2d3" +checksum = "5ab69a90e3455e15aa0ff47d676e84bf1a085716691b72156badc50d0a01dab1" dependencies = [ "anyhow", "async-trait", @@ -3230,9 +3253,9 @@ dependencies = [ [[package]] name = "sequoia-keystore-gpg-agent" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d888cad1e8b0f1de24c53aa9e3f0829703bfe2bf7597e8d31c0fabc1c2241458" +checksum = "454e8d580617e07d595b8df718d7fa3e26cdc58f35d1ad89f9fecc78ef0d55a7" dependencies = [ "anyhow", "async-trait", @@ -3249,9 +3272,9 @@ dependencies = [ [[package]] name = "sequoia-keystore-softkeys" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3441d9708124eb5e2b310f1d8337687ddbbab8d5bda2ffa227165b888d8be10" +checksum = "e1affc41cb24e491cd38a0a47928bc221b7db4318f0e4b17c49131c9a4a13eb1" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index d974176e..2957fcb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ indicatif = "0.17" itertools = ">=0.10, <0.13" once_cell = "1.17" sequoia-cert-store = "0.5.3" -sequoia-keystore = { version = "0.3" } +sequoia-keystore = { version = "0.4" } sequoia-wot = { version = "0.11", default-features = false } tempfile = "3.1" thiserror = "1" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 7cd770be..b07f2e6f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -232,8 +232,8 @@ key store." long_help = "\ A key store server manages and protects secret key material. By default, `sq` connects to the key store server listening on -`$XDG_DATA_HOME/sequoia`. If no key store server is running, one is -started. +`$XDG_DATA_HOME/sequoia/keystore`. If no key store server is running, +one is started. This option causes `sq` to use an alternate key store server. If necessary, a key store server is started, and configured to look for diff --git a/src/commands/encrypt.rs b/src/commands/encrypt.rs index d5190cc9..7358bb46 100644 --- a/src/commands/encrypt.rs +++ b/src/commands/encrypt.rs @@ -24,6 +24,8 @@ use openpgp::serialize::stream::padding::Padder; use openpgp::types::CompressionAlgorithm; use openpgp::types::KeyFlags; +use sequoia_keystore::Protection; + use crate::best_effort_primary_uid; use crate::cli; use crate::cli::types::EncryptPurpose; @@ -156,7 +158,7 @@ pub fn encrypt<'a, 'b: 'a>( let mut key = keys.into_iter().next().expect("checked for one"); match key.locked() { - Ok(true) => { + Ok(Protection::Password(msg)) => { let fpr = key.fingerprint(); let cert = config.lookup_one( &KeyHandle::from(&fpr), None, true); @@ -173,6 +175,9 @@ pub fn encrypt<'a, 'b: 'a>( }; let keyid = KeyID::from(&fpr); + if let Some(msg) = msg { + eprintln!("{}", msg); + } loop { let password = Password::from(rpassword::prompt_password( format!("Enter password to unlock {}{}: ", @@ -185,9 +190,21 @@ pub fn encrypt<'a, 'b: 'a>( } } } - Ok(false) => { + Ok(Protection::Unlocked) => { // Already unlocked, nothing to do. } + Ok(Protection::UnknownProtection(msg)) + | Ok(Protection::ExternalPassword(msg)) + | Ok(Protection::ExternalTouch(msg)) + | Ok(Protection::ExternalOther(msg)) => + { + // Locked. + eprint!("Key is locked"); + if let Some(msg) = msg { + eprint!(": {}", msg); + } + eprintln!(); + } Err(err) => { // Failed to get the key's locked status. Just print // a warning now. We'll (probably) fail more later. diff --git a/src/commands/key/list.rs b/src/commands/key/list.rs index 29b88992..5acf07dc 100644 --- a/src/commands/key/list.rs +++ b/src/commands/key/list.rs @@ -1,6 +1,9 @@ use sequoia_openpgp as openpgp; use openpgp::KeyHandle; +use sequoia_keystore as keystore; +use keystore::Protection; + use crate::best_effort_primary_uid; use crate::cli; use crate::Config; @@ -55,10 +58,10 @@ pub fn list(config: Config, _command: cli::key::ListCommand) -> Result<()> { } else { "not available" }, - if key.locked().unwrap_or(false) { - "locked" - } else { - "not locked" + match key.locked() { + Ok(Protection::Unlocked) => "unlocked", + Ok(_) => "locked", + Err(_) => "unknown protection", }, match (signing_capable, decryption_capable) { (true, true) => { diff --git a/src/commands/sign.rs b/src/commands/sign.rs index b78378dc..e76c6e79 100644 --- a/src/commands/sign.rs +++ b/src/commands/sign.rs @@ -23,6 +23,8 @@ use openpgp::serialize::stream::{ }; use openpgp::types::SignatureType; +use sequoia_keystore::Protection; + use crate::best_effort_primary_uid; use crate::Config; use crate::load_certs; @@ -201,7 +203,7 @@ fn sign_data<'a, 'store, 'rstore>( let mut key = keys.into_iter().next().expect("checked for one"); match key.locked() { - Ok(true) => { + Ok(Protection::Password(msg)) => { let fpr = key.fingerprint(); let cert = config.lookup_one( &KeyHandle::from(&fpr), None, true); @@ -218,6 +220,9 @@ fn sign_data<'a, 'store, 'rstore>( }; let keyid = KeyID::from(&fpr); + if let Some(msg) = msg { + eprintln!("{}", msg); + } loop { let password = Password::from(rpassword::prompt_password( format!("Enter password to unlock {}{}: ", @@ -230,9 +235,21 @@ fn sign_data<'a, 'store, 'rstore>( } } } - Ok(false) => { + Ok(Protection::Unlocked) => { // Already unlocked, nothing to do. } + Ok(Protection::UnknownProtection(msg)) + | Ok(Protection::ExternalPassword(msg)) + | Ok(Protection::ExternalTouch(msg)) + | Ok(Protection::ExternalOther(msg)) => + { + // Locked. + eprint!("Key is locked"); + if let Some(msg) = msg { + eprint!(": {}", msg); + } + eprintln!(); + } Err(err) => { // Failed to get the key's locked status. Just print // a warning now. We'll (probably) fail more later. diff --git a/src/sq.rs b/src/sq.rs index 3216ef09..3d314cb9 100644 --- a/src/sq.rs +++ b/src/sq.rs @@ -11,7 +11,6 @@ use anyhow::Context as _; use std::borrow::Borrow; use std::collections::btree_map::{BTreeMap, Entry}; use std::fmt; -use std::fs::File; use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -440,15 +439,26 @@ pub struct Config<'store, 'rstore> /// Whether a cert or key was freshly imported, updated, or unchanged. /// /// Returned by [`Config::import_key`]. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] enum ImportStatus { + /// The certificate or key is unchanged. + Unchanged, + /// The certificate or key is new. New, /// The certificate or key has been updated. Updated, +} - /// The certificate or key is unchanged. - Unchanged, +impl From for ImportStatus { + fn from(status: keystore::ImportStatus) -> ImportStatus { + match status { + keystore::ImportStatus::Unchanged => ImportStatus::Unchanged, + keystore::ImportStatus::New => ImportStatus::New, + keystore::ImportStatus::Updated => ImportStatus::Updated, + } + } } impl<'store: 'rstore, 'rstore> Config<'store, 'rstore> { @@ -685,7 +695,7 @@ impl<'store: 'rstore, 'rstore> Config<'store, 'rstore> { } else if let Some(dir) = self.key_store_path.as_ref() { Ok(Some(dir.clone())) } else if let Some(dir) = dirs::data_dir() { - Ok(Some(dir.join("sequoia"))) + Ok(Some(dir.join("sequoia/keystore"))) } else { Err(anyhow::anyhow!( "No key store, the XDG data directory is not defined")) @@ -1163,72 +1173,44 @@ impl<'store: 'rstore, 'rstore> Config<'store, 'rstore> { /// /// On success, returns whether the key was imported, updated, or /// unchanged. - fn import_key(&self, mut cert: Cert) -> Result { + fn import_key(&self, cert: Cert) -> Result { if ! cert.is_tsk() { return Err(anyhow::anyhow!( "Certificate does not contain any secret key material")); } - let softkeys = self.key_store_path_or_else()? - .join("keystore").join("softkeys"); + let keystore = self.key_store_or_else()?; + let mut keystore = keystore.lock().unwrap(); - std::fs::create_dir_all(&softkeys)?; - - let fpr = cert.fingerprint(); - - let filename = softkeys.join(format!("{}.pgp", fpr)); - - let mut update = false; - match Cert::from_file(&filename) { - Ok(old) => { - if old.fingerprint() != fpr { - return Err(anyhow::anyhow!( - "{} contains {}, but expected {}", - filename.display(), - old.fingerprint(), - fpr)); - } - - update = true; - - // Prefer secret key material from `cert`. - cert = old.clone().merge_public_and_secret(cert.clone())?; - - if cert == old { - return Ok(ImportStatus::Unchanged); - } - } - Err(err) => { - // If the file doesn't exist yet, it's not an - // error: it just means that we don't have to - // merge. - if let Some(ioerr) = err.downcast_ref::() { - if ioerr.kind() == std::io::ErrorKind::NotFound { - // Not found. No problem. - } else { - return Err(err); - } - } else { - return Err(err); - } + let mut softkeys = None; + for mut backend in keystore.backends()?.into_iter() { + if backend.id()? == "softkeys" { + softkeys = Some(backend); + break; } } - // We write to a temporary file and then move it into - // place. This doesn't eliminate races, but it does - // prevent a partial update from destroying the existing - // data. - let mut tmp_filename = filename.clone(); - tmp_filename.set_extension("pgp~"); + drop(keystore); - let mut f = File::create(&tmp_filename)?; - cert.as_tsk().serialize(&mut f)?; + let mut softkeys = if let Some(softkeys) = softkeys { + softkeys + } else { + return Err(anyhow::anyhow!("softkeys backend is not configured.")); + }; - std::fs::rename(&tmp_filename, &filename)?; + let mut import_status = ImportStatus::Unchanged; + for (s, key) in softkeys.import(&cert)? { + self.info(format_args!( + "Importing {} into key store: {:?}", + key.fingerprint(), s)); + + import_status = import_status.max(s.into()); + } // Also insert the certificate into the certificate store. // If we can't, we don't fail. This allows, in // particular, `sq --no-cert-store key import` to work. + let fpr = cert.fingerprint(); match self.cert_store_or_else() { Ok(cert_store) => { if let Err(err) = cert_store.update( @@ -1246,11 +1228,7 @@ impl<'store: 'rstore, 'rstore> Config<'store, 'rstore> { } } - if update { - return Ok(ImportStatus::Updated); - } else { - return Ok(ImportStatus::New); - } + Ok(import_status) } /// Prints additional information in verbose mode.