diff --git a/Cargo.lock b/Cargo.lock index c415853..4ba00bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,27 +20,27 @@ dependencies = [ [[package]] name = "age-core" version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70afa630ef12a4fc666277713efbe6da2bc87bb3f3af0f1149415b701362c615" +source = "git+https://github.com/str4d/rage.git?rev=fdb518c6d802a3618b47d959d56af6e60c668627#fdb518c6d802a3618b47d959d56af6e60c668627" dependencies = [ "base64", "chacha20poly1305", "cookie-factory", "hkdf", + "io_tee", "nom", "rand", "secrecy", - "sha2", + "sha2 0.10.2", "tempfile", ] [[package]] name = "age-plugin" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74049e94d591e0b96128880bb9dcbc5f27432b3089725524d020616e1dc36e2b" +source = "git+https://github.com/str4d/rage.git?rev=fdb518c6d802a3618b47d959d56af6e60c668627#fdb518c6d802a3618b47d959d56af6e60c668627" dependencies = [ "age-core", + "base64", "bech32", "chrono", ] @@ -68,7 +68,7 @@ dependencies = [ "pcsc", "rand", "rust-embed", - "sha2", + "sha2 0.9.9", "which", "x509", "x509-parser", @@ -149,6 +149,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -265,6 +274,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-mac" version = "0.11.1" @@ -357,6 +376,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", + "subtle", +] + [[package]] name = "ecdsa" version = "0.12.4" @@ -365,7 +395,7 @@ checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372" dependencies = [ "der", "elliptic-curve", - "hmac", + "hmac 0.11.0", "signature", ] @@ -563,12 +593,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" -version = "0.11.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" dependencies = [ - "digest", - "hmac", + "hmac 0.12.1", ] [[package]] @@ -578,7 +607,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ "crypto-mac", - "digest", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.3", ] [[package]] @@ -685,6 +723,12 @@ dependencies = [ "unic-langid", ] +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + [[package]] name = "lazy_static" version = "1.4.0" @@ -917,7 +961,7 @@ checksum = "d053368e1bae4c8a672953397bd1bd7183dde1c72b0b7612a15719173148d186" dependencies = [ "ecdsa", "elliptic-curve", - "sha2", + "sha2 0.9.9", ] [[package]] @@ -1156,7 +1200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d" dependencies = [ "byteorder", - "digest", + "digest 0.9.0", "lazy_static", "num-bigint-dig", "num-integer", @@ -1199,7 +1243,7 @@ version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756feca3afcbb1487a1d01f4ecd94cf8ec98ea074c55a69e7136d29fb6166029" dependencies = [ - "sha2", + "sha2 0.9.9", "walkdir", ] @@ -1274,10 +1318,10 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] @@ -1287,20 +1331,31 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "signature" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" dependencies = [ - "digest", + "digest 0.9.0", "rand_core", ] @@ -1658,7 +1713,7 @@ dependencies = [ "der-parser", "des", "elliptic-curve", - "hmac", + "hmac 0.11.0", "log", "nom", "num-bigint-dig", @@ -1672,7 +1727,7 @@ dependencies = [ "rsa", "secrecy", "sha-1", - "sha2", + "sha2 0.9.9", "subtle", "subtle-encoding", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 49ef1b8..0858976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,3 +50,7 @@ rust-embed = "6" [dev-dependencies] flate2 = "1" man = "0.3" + +[patch.crates-io] +age-core = { git = "https://github.com/str4d/rage.git", rev = "fdb518c6d802a3618b47d959d56af6e60c668627" } +age-plugin = { git = "https://github.com/str4d/rage.git", rev = "fdb518c6d802a3618b47d959d56af6e60c668627" } diff --git a/i18n/en-US/age_plugin_yubikey.ftl b/i18n/en-US/age_plugin_yubikey.ftl index d924626..a680bef 100644 --- a/i18n/en-US/age_plugin_yubikey.ftl +++ b/i18n/en-US/age_plugin_yubikey.ftl @@ -151,6 +151,9 @@ plugin-err-invalid-stanza = Invalid {-yubikey} stanza plugin-err-decryption-failed = Failed to decrypt {-yubikey} stanza plugin-insert-yk = Please insert {-yubikey} with serial {$yubikey_serial} +plugin-yk-is-plugged-in = {-yubikey} is plugged in +plugin-skip-this-yk = Skip this {-yubikey} +plugin-insert-yk-retry = Could not open {-yubikey}. Please insert {-yubikey} with serial {$yubikey_serial} plugin-err-yk-not-found = Could not find {-yubikey} with serial {$yubikey_serial} plugin-err-yk-opening = Could not open {-yubikey} with serial {$yubikey_serial} plugin-err-yk-timed-out = Timed out while waiting for {-yubikey} with serial {$yubikey_serial} to be inserted diff --git a/src/key.rs b/src/key.rs index ab1dabb..6883d20 100644 --- a/src/key.rs +++ b/src/key.rs @@ -199,7 +199,7 @@ pub struct Stub { pub(crate) serial: Serial, pub(crate) slot: RetiredSlotId, pub(crate) tag: [u8; TAG_BYTES], - identity_index: usize, + pub(crate) identity_index: usize, } impl fmt::Display for Stub { @@ -260,38 +260,53 @@ impl Stub { self.tag == line.tag } + /// Returns: + /// - `Ok(Ok(Some(connection)))` if we successfully connected to this YubiKey. + /// - `Ok(Ok(None))` if the user told us to skip this YubiKey. + /// - `Ok(Err(_))` if we encountered an error while trying to connect to the YubiKey. + /// - `Err(_)` on communication errors with the age client. pub(crate) fn connect( &self, callbacks: &mut dyn Callbacks, - ) -> io::Result> { + ) -> io::Result, identity::Error>> { let mut yubikey = match YubiKey::open_by_serial(self.serial) { Ok(yk) => yk, Err(yubikey::Error::NotFound) => { - if callbacks - .message(&i18n_embed_fl::fl!( - crate::LANGUAGE_LOADER, - "plugin-insert-yk", - yubikey_serial = self.serial.to_string(), - ))? - .is_err() - { - return Ok(Err(identity::Error::Identity { - index: self.identity_index, - message: i18n_embed_fl::fl!( - crate::LANGUAGE_LOADER, - "plugin-err-yk-not-found", - yubikey_serial = self.serial.to_string(), - ), - })); - } + let mut message = i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-insert-yk", + yubikey_serial = self.serial.to_string(), + ); - // Start a 15-second timer waiting for the YubiKey to be inserted - let start = SystemTime::now(); - loop { - match YubiKey::open_by_serial(self.serial) { - Ok(yubikey) => break yubikey, - Err(yubikey::Error::NotFound) => (), - Err(_) => { + // If the `confirm` command is available, we loop until either the YubiKey + // we want is inserted, or the used explicitly skips. + let yubikey = loop { + match callbacks.confirm( + &message, + &fl!("plugin-yk-is-plugged-in"), + Some(&fl!("plugin-skip-this-yk")), + )? { + // `confirm` command is not available. + Err(age_core::plugin::Error::Unsupported) => break None, + // User told us to skip this key. + Ok(false) => return Ok(Ok(None)), + // User said they plugged it in; try it. + Ok(true) => match YubiKey::open_by_serial(self.serial) { + Ok(yubikey) => break Some(yubikey), + Err(yubikey::Error::NotFound) => (), + Err(_) => { + return Ok(Err(identity::Error::Identity { + index: self.identity_index, + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-yk-opening", + yubikey_serial = self.serial.to_string(), + ), + })); + } + }, + // We can't communicate with the user. + Err(age_core::plugin::Error::Fail) => { return Ok(Err(identity::Error::Identity { index: self.identity_index, message: i18n_embed_fl::fl!( @@ -299,22 +314,65 @@ impl Stub { "plugin-err-yk-opening", yubikey_serial = self.serial.to_string(), ), - })); + })) } } - match SystemTime::now().duration_since(start) { - Ok(end) if end >= FIFTEEN_SECONDS => { - return Ok(Err(identity::Error::Identity { - index: self.identity_index, - message: i18n_embed_fl::fl!( - crate::LANGUAGE_LOADER, - "plugin-err-yk-timed-out", - yubikey_serial = self.serial.to_string(), - ), - })) + // We're going to loop around, meaning that the first attempt failed. + // Change the message to indicate this to the user. + message = i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-insert-yk-retry", + yubikey_serial = self.serial.to_string(), + ); + }; + + if let Some(yk) = yubikey { + yk + } else { + // `confirm` is not available; fall back to `message` with a timeout. + if callbacks.message(&message)?.is_err() { + return Ok(Err(identity::Error::Identity { + index: self.identity_index, + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-yk-not-found", + yubikey_serial = self.serial.to_string(), + ), + })); + } + + // Start a 15-second timer waiting for the YubiKey to be inserted + let start = SystemTime::now(); + loop { + match YubiKey::open_by_serial(self.serial) { + Ok(yubikey) => break yubikey, + Err(yubikey::Error::NotFound) => (), + Err(_) => { + return Ok(Err(identity::Error::Identity { + index: self.identity_index, + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-yk-opening", + yubikey_serial = self.serial.to_string(), + ), + })); + } + } + + match SystemTime::now().duration_since(start) { + Ok(end) if end >= FIFTEEN_SECONDS => { + return Ok(Err(identity::Error::Identity { + index: self.identity_index, + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-yk-timed-out", + yubikey_serial = self.serial.to_string(), + ), + })) + } + _ => sleep(ONE_SECOND), } - _ => sleep(ONE_SECOND), } } } @@ -348,7 +406,7 @@ impl Stub { } }; - Ok(Ok(Connection { + Ok(Ok(Some(Connection { yubikey, cert, pk, @@ -357,7 +415,7 @@ impl Stub { identity_index: self.identity_index, cached_metadata: None, last_touch: None, - })) + }))) } } diff --git a/src/plugin.rs b/src/plugin.rs index 965733e..2ccb3f8 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -68,7 +68,15 @@ impl RecipientPluginV1 for RecipientPlugin { let mut yk_errors = vec![]; for stub in &self.yubikeys { match stub.connect(&mut callbacks)? { - Ok(conn) => yk_recipients.push(conn.recipient().clone()), + Ok(Some(conn)) => yk_recipients.push(conn.recipient().clone()), + Ok(None) => yk_errors.push(recipient::Error::Identity { + index: stub.identity_index, + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-yk-opening", + yubikey_serial = stub.serial.to_string(), + ), + }), Err(e) => yk_errors.push(match e { identity::Error::Identity { index, message } => { recipient::Error::Identity { index, message } @@ -203,7 +211,11 @@ impl IdentityPluginV1 for IdentityPlugin { for (stub, files) in candidate_stanzas.iter() { let mut conn = match stub.connect(&mut callbacks)? { - Ok(conn) => conn, + // The user skipped this YubiKey. + Ok(None) => continue, + // We connected to this YubiKey. + Ok(Some(conn)) => conn, + // We failed to connect to this YubiKey. Err(e) => { callbacks.error(e)?.unwrap(); continue;