diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c039a5..06f59e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to Rust's notion of to 0.3.0 are beta releases. ## [Unreleased] +### Changed +- If a "sharing violation" error is encountered while opening a connection to a + YubiKey, and `scdaemon` is running (which can hold exclusive access to a + YubiKey indefinitely), `age-plugin-yubikey` now attempts to stop `scdaemon` by + interrupting it (or killing it on Windows), and then tries again to open the + connection. ## [0.3.0] - 2022-05-02 First non-beta release! diff --git a/Cargo.lock b/Cargo.lock index 10eaf3f..d7ebc95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,7 @@ dependencies = [ "rand", "rust-embed", "sha2 0.9.9", + "sysinfo", "which", "x509", "x509-parser", @@ -246,6 +247,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.2" @@ -264,6 +271,49 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg 1.1.0", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-bigint" version = "0.2.11" @@ -808,6 +858,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -834,6 +893,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -1154,6 +1222,28 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.11" @@ -1426,6 +1516,21 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "sysinfo" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49086f670c15221b510c3f8c47e04e49714c56820d5ca78e2f58419e9fbb0f1b" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + [[package]] name = "tempfile" version = "3.3.0" diff --git a/Cargo.toml b/Cargo.toml index 92f893a..b7442f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,9 @@ i18n-embed-fl = "0.6" lazy_static = "1" rust-embed = "6" +# GnuPG coexistence +sysinfo = ">=0.26, <0.26.4" + [dev-dependencies] flate2 = "1" man = "0.3" diff --git a/src/key.rs b/src/key.rs index 01b5209..aa7bab6 100644 --- a/src/key.rs +++ b/src/key.rs @@ -8,7 +8,7 @@ use age_core::{ use age_plugin::{identity, Callbacks}; use bech32::{ToBase32, Variant}; use dialoguer::Password; -use log::warn; +use log::{debug, warn}; use std::fmt; use std::io; use std::iter; @@ -77,6 +77,71 @@ pub(crate) fn wait_for_readers() -> Result { } } +/// Stops `scdaemon` if it is running. +/// +/// Returns `true` if `scdaemon` was running and was successfully interrupted (or killed +/// if the platform doesn't support interrupts). +fn stop_scdaemon() -> bool { + debug!("Sharing violation encountered, looking for scdaemon processes to stop"); + + use sysinfo::{ + Process, ProcessExt, ProcessRefreshKind, RefreshKind, Signal, System, SystemExt, + }; + + let mut interrupted = false; + + let sys = + System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::new())); + + for process in sys + .processes() + .values() + .filter(|val: &&Process| ["scdaemon", "scdaemon.exe"].contains(&val.name())) + { + if process + .kill_with(Signal::Interrupt) + .unwrap_or_else(|| process.kill()) + { + debug!("Stopped scdaemon (PID {})", process.pid()); + interrupted = true; + } + } + + // If we did interrupt `scdaemon`, pause briefly to allow it to exit. + if interrupted { + sleep(Duration::from_millis(100)); + } + + interrupted +} + +fn open_sesame( + op: impl Fn() -> Result, +) -> Result { + op().or_else(|e| match e { + yubikey::Error::PcscError { + inner: Some(pcsc::Error::SharingViolation), + } if stop_scdaemon() => op(), + _ => Err(e), + }) +} + +/// Opens a connection to this reader, returning a `YubiKey` if successful. +/// +/// This is equivalent to [`Reader::open`], but additionally handles the presence of +/// `scdaemon` (which can indefinitely hold exclusive access to a YubiKey). +pub(crate) fn open_connection(reader: &Reader) -> Result { + open_sesame(|| reader.open()) +} + +/// Opens a YubiKey with a specific serial number. +/// +/// This is equivalent to [`YubiKey::open_by_serial`], but additionally handles the +/// presence of `scdaemon` (which can indefinitely hold exclusive access to a YubiKey). +fn open_by_serial(serial: Serial) -> Result { + open_sesame(|| YubiKey::open_by_serial(serial)) +} + pub(crate) fn open(serial: Option) -> Result { if !Context::open()?.iter()?.any(is_connected) { if let Some(serial) = serial { @@ -99,9 +164,9 @@ pub(crate) fn open(serial: Option) -> Result { // connected, an error is returned. let yubikey = match (readers_iter.next(), readers_iter.next(), serial) { (None, _, _) => unreachable!(), - (Some(reader), None, None) => reader.open()?, + (Some(reader), None, None) => open_connection(&reader)?, (Some(reader), None, Some(serial)) => { - let yubikey = reader.open()?; + let yubikey = open_connection(&reader)?; if yubikey.serial() != serial { return Err(Error::NoMatchingSerial(serial)); } @@ -112,12 +177,12 @@ pub(crate) fn open(serial: Option) -> Result { .chain(Some(a)) .chain(Some(b)) .chain(readers_iter) - .find(|reader| match reader.open() { + .find(|reader| match open_connection(reader) { Ok(yk) => yk.serial() == serial, _ => false, }) .ok_or(Error::NoMatchingSerial(serial))?; - reader.open()? + open_connection(&reader)? } (Some(_), Some(_), None) => return Err(Error::MultipleYubiKeys), }; @@ -272,7 +337,7 @@ impl Stub { &self, callbacks: &mut dyn Callbacks, ) -> io::Result, identity::Error>> { - let mut yubikey = match YubiKey::open_by_serial(self.serial) { + let mut yubikey = match open_by_serial(self.serial) { Ok(yk) => yk, Err(yubikey::Error::NotFound) => { let mut message = i18n_embed_fl::fl!( @@ -294,7 +359,7 @@ impl Stub { // 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(true) => match open_by_serial(self.serial) { Ok(yubikey) => break Some(yubikey), Err(yubikey::Error::NotFound) => (), Err(_) => { @@ -348,7 +413,7 @@ impl Stub { // Start a 15-second timer waiting for the YubiKey to be inserted let start = SystemTime::now(); loop { - match YubiKey::open_by_serial(self.serial) { + match open_by_serial(self.serial) { Ok(yubikey) => break yubikey, Err(yubikey::Error::NotFound) => (), Err(_) => { diff --git a/src/main.rs b/src/main.rs index 9ab2c48..9a0d03c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -229,7 +229,7 @@ fn print_multiple( let mut printed = 0; for reader in readers.iter()?.filter(key::filter_connected) { - let mut yubikey = reader.open()?; + let mut yubikey = key::open_connection(&reader)?; if let Some(serial) = serial { if yubikey.serial() != serial { continue; @@ -401,7 +401,7 @@ fn main() -> Result<(), Error> { let reader_names = readers_list .iter() .map(|reader| { - reader.open().map(|yk| { + key::open_connection(reader).map(|yk| { i18n_embed_fl::fl!( LANGUAGE_LOADER, "cli-setup-yk-name",