diff --git a/CHANGELOG.md b/CHANGELOG.md index f6ba9c9..1fd70ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ to 0.3.0 are beta releases. - MSRV is now 1.60.0. - The YubiKey PIV PIN and touch caches are now preserved across processes in most cases. See [README.md](README.md#agent-support) for exceptions. This has - several usability effects: + several usability effects (not applicable to YubiKey 4 series): - If a YubiKey's PIN is cached by an agent like `yubikey-agent`, and then `age-plugin-yubikey` is run (either directly or as a plugin), the agent won't request a PIN entry on its next use. diff --git a/README.md b/README.md index 4c764da..2d874f2 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,10 @@ age client as normal (e.g. `rage -d -i yubikey-identity.txt`). ### Agent support `age-plugin-yubikey` does not provide or interact with an agent for decryption. -It does however preserve the PIN cache by not soft-resetting the YubiKey after a -decryption or read-only operation, which enables YubiKey identities configured -with a PIN policy of `once` to not prompt for the PIN on every decryption. +It does however attempt to preserve the PIN cache by not soft-resetting the +YubiKey after a decryption or read-only operation, which enables YubiKey +identities configured with a PIN policy of `once` to not prompt for the PIN on +every decryption. **This does not work for YubiKey 4 series.** The session that corresponds to the `once` policy can be ended in several ways, not all of which are necessarily intuitive: @@ -133,6 +134,8 @@ not all of which are necessarily intuitive: - Unplugging the YubiKey (the obvious way). - Using a different applet (e.g. FIDO2). This causes the PIV applet to be closed which clears its state. + - This is why the YubiKey 4 series does not support PIN cache preservation: + their serial can only be obtained by switching to the OTP applet. - Generating a new age identity via `age-plugin-yubikey --generate` or the CLI interface. This is to avoid leaving the YubiKey authenticated with the management key. diff --git a/i18n/en-US/age_plugin_yubikey.ftl b/i18n/en-US/age_plugin_yubikey.ftl index 8783093..22ad778 100644 --- a/i18n/en-US/age_plugin_yubikey.ftl +++ b/i18n/en-US/age_plugin_yubikey.ftl @@ -76,6 +76,14 @@ cli-setup-name-identity = 📛 Name this identity cli-setup-select-pin-policy = 🔤 Select a PIN policy cli-setup-select-touch-policy = 👆 Select a touch policy +cli-setup-yk4-pin-policy = + ⚠️ Your {-yubikey} is a {-yubikey} 4 series. With ephemeral applications like + {-age-plugin-yubikey}, a PIN policy of "Once" behaves like a PIN policy of + "Always", and your PIN will be requested for every decryption. However, you + might still benefit from a PIN policy of "Once" in long-running applications + like agents. +cli-setup-yk4-pin-policy-confirm = Use PIN policy of "Once" with {-yubikey} 4? + cli-setup-generate-new = Generate new identity in slot {$slot_index}? cli-setup-use-existing = Use existing identity in slot {$slot_index}? diff --git a/src/main.rs b/src/main.rs index f6cd93d..757cab6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -487,29 +487,54 @@ fn main() -> Result<(), Error> { .report(true) .interact_text()?; - let pin_policy = match Select::new() - .with_prompt(fl!("cli-setup-select-pin-policy")) - .items(&[ - fl!("pin-policy-always"), - fl!("pin-policy-once"), - fl!("pin-policy-never"), - ]) - .default( - [PinPolicy::Always, PinPolicy::Once, PinPolicy::Never] - .iter() - .position(|p| { - p == &flags.pin_policy.unwrap_or(builder::DEFAULT_PIN_POLICY) - }) - .unwrap(), - ) - .report(true) - .interact_opt()? - { - Some(0) => PinPolicy::Always, - Some(1) => PinPolicy::Once, - Some(2) => PinPolicy::Never, - Some(_) => unreachable!(), - None => return Ok(()), + let mut displayed_yk4_warning = false; + let pin_policy = loop { + let pin_policy = match Select::new() + .with_prompt(fl!("cli-setup-select-pin-policy")) + .items(&[ + fl!("pin-policy-always"), + fl!("pin-policy-once"), + fl!("pin-policy-never"), + ]) + .default( + [PinPolicy::Always, PinPolicy::Once, PinPolicy::Never] + .iter() + .position(|p| { + p == &flags.pin_policy.unwrap_or(builder::DEFAULT_PIN_POLICY) + }) + .unwrap(), + ) + .report(true) + .interact_opt()? + { + Some(0) => PinPolicy::Always, + Some(1) => PinPolicy::Once, + Some(2) => PinPolicy::Never, + Some(_) => unreachable!(), + None => return Ok(()), + }; + + // We can't preserve the PIN cache for YubiKey 4 series, because to + // retrieve the serial we switch to the OTP applet. + match (pin_policy, yubikey.version().major) { + (PinPolicy::Once, 4) => { + if !displayed_yk4_warning { + eprintln!(); + eprintln!("{}", fl!("cli-setup-yk4-pin-policy")); + eprintln!(); + displayed_yk4_warning = true; + } + + if Confirm::new() + .with_prompt(fl!("cli-setup-yk4-pin-policy-confirm")) + .report(true) + .interact()? + { + break pin_policy; + } + } + _ => break pin_policy, + } }; let touch_policy = match Select::new()