From a92a843e14e64459925a92efe87aa51929c601d5 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 1 May 2022 12:55:48 +0000 Subject: [PATCH] Tag all strings for translation --- i18n/en-US/age_plugin_yubikey.ftl | 182 +++++++++++++++++++++++++ src/error.rs | 149 +++++++++++++------- src/key.rs | 143 ++++++++++++-------- src/main.rs | 218 ++++++++++++++++++------------ src/plugin.rs | 12 +- src/util.rs | 67 +++++---- 6 files changed, 541 insertions(+), 230 deletions(-) diff --git a/i18n/en-US/age_plugin_yubikey.ftl b/i18n/en-US/age_plugin_yubikey.ftl index 0512a6d..ac7c70c 100644 --- a/i18n/en-US/age_plugin_yubikey.ftl +++ b/i18n/en-US/age_plugin_yubikey.ftl @@ -10,4 +10,186 @@ -age = age -yubikey = YubiKey +-yubikeys = YubiKeys -age-plugin-yubikey = age-plugin-yubikey + +## CLI commands and flags + +-cmd-generate = --generate +-cmd-identity = --identity +-cmd-list = --list +-cmd-list-all = --list-all + +-flag-force = --force +-flag-serial = --serial +-flag-slot = --slot + +## YubiKey metadata + +pin-policy-always = Always (A PIN is required for every decryption, if set) +pin-policy-once = Once (A PIN is required once per session, if set) +pin-policy-never = Never (A PIN is NOT required to decrypt) + +touch-policy-always = Always (A physical touch is required for every decryption) +touch-policy-cached = Cached (A physical touch is required for decryption, and is cached for 15 seconds) +touch-policy-never = Never (A physical touch is NOT required to decrypt) + +unknown-policy = Unknown + +yubikey-metadata = + # Serial: {$serial}, Slot: {$slot} + # Name: {$name} + # Created: {$created} + # PIN policy: {$pin_policy} + # Touch policy: {$touch_policy} +yubikey-identity = + {$yubikey_metadata} + # Recipient: {$recipient} + {$identity} + +## CLI setup via text interface + +cli-setup-intro = + ✨ Let's get your {-yubikey} set up for {-age}! ✨ + + This tool can create a new {-age} identity in a free slot of your {-yubikey}. + It will generate an identity file that you can use with an {-age} client, + along with the corresponding recipient. You can also do this directly + with: + {" "}{$generate_usage} + + If you are already using a {-yubikey} with {-age}, you can select an existing + slot to recreate its corresponding identity file and recipient. + + When asked below to select an option, use the up/down arrow keys to + make your choice, or press [Esc] or [q] to quit. + +cli-setup-insert-yk = ⏳ Please insert the {-yubikey} you want to set up. +cli-setup-yk-name = {$yubikey_name} (Serial: {$yubikey_serial}) +cli-setup-select-yk = 🔑 Select a {-yubikey} +cli-setup-slot-usable = Slot {$slot_index} ({$slot_name}) +cli-setup-slot-unusable = Slot {$slot_index} (Unusable) +cli-setup-slot-empty = Slot {$slot_index} (Empty) +cli-setup-select-slot = 🕳️ Select a slot for your {-age} identity +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-generate-new = Generate new identity in slot {$slot_index}? +cli-setup-use-existing = Use existing identity in slot {$slot_index}? + +cli-setup-identity-file-name = 📝 File name to write this identity to +cli-setup-identity-file-exists = File exists. Overwrite it? + +cli-setup-finished = + ✅ Done! This {-yubikey} identity is ready to go. + + 🔑 { $is_new -> + [true] Here's your shiny new {-yubikey} recipient: + *[false] Here's the corresponding {-yubikey} recipient: + } + {" "}{$recipient} + + Here are some example things you can do with it: + + - Encrypt a file to this identity: + {" "}{$encrypt_usage} + + - Decrypt a file with this identity: + {" "}{$decrypt_usage} + + - Recreate the identity file: + {" "}{$identity_usage} + + - Recreate the recipient: + {" "}{$recipient_usage} + + 💭 Remember: everything breaks, have a backup plan for when this {-yubikey} does. + +## Programmatic usage + +open-yk-with-serial = ⏳ Please insert the {-yubikey} with serial {$yubikey_serial}. +open-yk-without-serial = ⏳ Please insert the {-yubikey}. +warn-yk-not-connected = Ignoring {$yubikey_name}: not connected + +print-recipient = Recipient: {$recipient} + +printed-kind-identities = identities +printed-kind-recipients = recipients +printed-multiple = Generated {$kind} for {$count} slots. If you intended to select a slot, use {-flag-slot}. + +## YubiKey management + +mgr-enter-pin = Enter PIN for {-yubikey} with serial {$yubikey_serial} (default is {$default_pin}) + +mgr-change-default-pin = + ✨ Your {-yubikey} is using the default PIN. Let's change it! + ✨ We'll also set the PUK equal to the PIN. + + 🔐 The PIN is up to 8 numbers, letters, or symbols. Not just numbers! + ❌ Your keys will be lost if the PIN and PUK are locked after 3 incorrect tries. + +mgr-enter-current-puk = Enter current PUK (default is {$default_puk}) +mgr-choose-new-pin = Choose a new PIN/PUK +mgr-repeat-new-pin = Repeat the PIN/PUK +mgr-pin-mismatch = PINs don't match + +mgr-changing-mgmt-key = + ✨ Your {-yubikey} is using the default management key. + ✨ We'll migrate it to a PIN-protected management key. +mgr-changing-mgmt-key-error = + An error occurred while setting the new management key. + ⚠️ SAVE THIS MANAGEMENT KEY - YOU MAY NEED IT TO MANAGE YOUR {-yubikey}! ⚠️ + {" "}{$management_key} +mgr-changing-mgmt-key-success = Success! + +## Plugin usage + +plugin-err-invalid-recipient = Invalid recipient +plugin-err-invalid-identity = Invalid {-yubikey} stub +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-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 +plugin-err-yk-stub-mismatch = A {-yubikey} stub did not match the {-yubikey} + +plugin-err-yk-invalid-pin-policy = Certificate for {-yubikey} identity contains an invalid PIN policy + +plugin-enter-pin = Enter PIN for {-yubikey} with serial {$yubikey_serial} +plugin-err-accidental-touch = Did you touch the {-yubikey} by accident? +plugin-err-pin-too-short = PIN was too short. +plugin-err-pin-too-long = PIN was too long. +plugin-err-pin-required = A PIN is required for {-yubikey} with serial {$yubikey_serial} + +plugin-touch-yk = 👆 Please touch the {-yubikey} + +## Errors + +err-custom-mgmt-key = Custom unprotected management keys are not supported. +err-invalid-flag-command = Flag '{$flag}' cannot be used with '{$command}'. +err-invalid-flag-tui = Flag '{$flag}' cannot be used with the interactive interface. +err-invalid-pin-length = The PIN needs to be 1-8 characters. +err-invalid-pin-policy = Invalid PIN policy '{$policy}' (expected [{$expected}]). +err-invalid-slot = Invalid slot '{$slot}' (expected number between 1 and 20). +err-invalid-touch-policy = Invalid touch policy '{$policy}' (expected [{$expected}]). +err-io = Failed to set up {-yubikey}: {$err} +err-multiple-commands = Only one of {-cmd-generate}, {-cmd-identity}, {-cmd-list}, {-cmd-list-all} can be specified. +err-multiple-yubikeys = Multiple {-yubikeys} are plugged in. Use {-flag-serial} to select a single {-yubikey}. +err-no-empty-slots = {-yubikey} with serial {$serial} has no empty slots. +err-no-matching-serial = Could not find {-yubikey} with serial {$serial}. +err-slot-has-no-identity = Slot {$slot} does not contain an {-age} identity or compatible key. +err-slot-is-not-empty = Slot {$slot} is not empty. Use {-flag-force} to overwrite the slot. +err-timed-out = Timed out while waiting for a {-yubikey} to be inserted. +err-use-list-for-single = Use {-cmd-list} to print the recipient for a single slot. +err-yk-not-found = Please insert the {-yubikey} you want to set up +err-yk-wrong-pin = Invalid PIN ({$tries} tries remaining before it is blocked) +err-yk-general = Error while communicating with {-yubikey}: {$err} +err-yk-general-cause = Cause: {$inner_err} + +err-ux-A = Did this not do what you expected? Could an error be more useful? +err-ux-B = Tell us +# Put (len(A) - len(B) - 46) spaces here. +err-ux-C = {" "} diff --git a/src/error.rs b/src/error.rs index 892a063..0bc4719 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,16 @@ +use i18n_embed_fl::fl; use std::fmt; use std::io; use yubikey::{piv::RetiredSlotId, Serial}; use crate::util::slot_to_ui; +macro_rules! wlnfl { + ($f:ident, $message_id:literal) => { + writeln!($f, "{}", $crate::fl!($message_id)) + }; +} + pub enum Error { CustomManagementKey, InvalidFlagCommand(String, String), @@ -41,90 +48,136 @@ impl From for Error { impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Error::CustomManagementKey => { - writeln!(f, "Custom unprotected management keys are not supported.")? - } - Error::InvalidFlagCommand(flag, command) => { - writeln!(f, "Flag '{}' cannot be used with '{}'.", flag, command)? - } + Error::CustomManagementKey => wlnfl!(f, "err-custom-mgmt-key")?, + Error::InvalidFlagCommand(flag, command) => writeln!( + f, + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-invalid-flag-command", + flag = flag.as_str(), + command = command.as_str(), + ), + )?, Error::InvalidFlagTui(flag) => writeln!( f, - "Flag '{}' cannot be used with the interactive interface.", - flag + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-invalid-flag-tui", + flag = flag.as_str(), + ), )?, - Error::InvalidPinLength => writeln!(f, "The PIN needs to be 1-8 characters.")?, + Error::InvalidPinLength => wlnfl!(f, "err-invalid-pin-length")?, Error::InvalidPinPolicy(s) => writeln!( f, - "Invalid PIN policy '{}' (expected [always, once, never]).", - s + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-invalid-pin-policy", + policy = s.as_str(), + expected = "always, once, never", + ), )?, Error::InvalidSlot(slot) => writeln!( f, - "Invalid slot '{}' (expected number between 1 and 20).", - slot + "{}", + fl!(crate::LANGUAGE_LOADER, "err-invalid-slot", slot = slot), )?, Error::InvalidTouchPolicy(s) => writeln!( f, - "Invalid touch policy '{}' (expected [always, cached, never]).", - s + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-invalid-touch-policy", + policy = s.as_str(), + expected = "always, cached, never", + ), )?, - Error::Io(e) => writeln!(f, "Failed to set up YubiKey: {}", e)?, - Error::MultipleCommands => writeln!( + Error::Io(e) => writeln!( f, - "Only one of --generate, --identity, --list, --list-all can be specified." + "{}", + fl!(crate::LANGUAGE_LOADER, "err-io", err = e.to_string()), )?, - Error::MultipleYubiKeys => writeln!( + Error::MultipleCommands => wlnfl!(f, "err-multiple-commands")?, + Error::MultipleYubiKeys => wlnfl!(f, "err-multiple-yubikeys")?, + Error::NoEmptySlots(serial) => writeln!( f, - "Multiple YubiKeys are plugged in. Use --serial to select a single YubiKey." + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-no-empty-slots", + serial = serial.to_string(), + ), + )?, + Error::NoMatchingSerial(serial) => writeln!( + f, + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-no-matching-serial", + serial = serial.to_string(), + ), )?, - Error::NoEmptySlots(serial) => { - writeln!(f, "YubiKey with serial {} has no empty slots.", serial)? - } - Error::NoMatchingSerial(serial) => { - writeln!(f, "Could not find YubiKey with serial {}.", serial)? - } Error::SlotHasNoIdentity(slot) => writeln!( f, - "Slot {} does not contain an age identity or compatible key.", - slot_to_ui(slot) + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-slot-has-no-identity", + slot = slot_to_ui(slot), + ), )?, Error::SlotIsNotEmpty(slot) => writeln!( f, - "Slot {} is not empty. Use --force to overwrite the slot.", - slot_to_ui(slot) + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-slot-is-not-empty", + slot = slot_to_ui(slot), + ), )?, - Error::TimedOut => { - writeln!(f, "Timed out while waiting for a YubiKey to be inserted.")? - } - Error::UseListForSingleSlot => { - writeln!(f, "Use --list to print the recipient for a single slot.")? - } + Error::TimedOut => wlnfl!(f, "err-timed-out")?, + Error::UseListForSingleSlot => wlnfl!(f, "err-use-list-for-single")?, Error::YubiKey(e) => match e { - yubikey::Error::NotFound => { - writeln!(f, "Please insert the YubiKey you want to set up")? - } + yubikey::Error::NotFound => wlnfl!(f, "err-yk-not-found")?, yubikey::Error::WrongPin { tries } => writeln!( f, - "Invalid PIN ({} tries remaining before it is blocked)", - tries + "{}", + fl!(crate::LANGUAGE_LOADER, "err-yk-wrong-pin", tries = tries), )?, e => { - writeln!(f, "Error while communicating with YubiKey: {}", e)?; + writeln!( + f, + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-yk-general", + err = e.to_string(), + ), + )?; use std::error::Error; if let Some(inner) = e.source() { - writeln!(f, "Cause: {}", inner)?; + writeln!( + f, + "{}", + fl!( + crate::LANGUAGE_LOADER, + "err-yk-general-cause", + inner_err = inner.to_string(), + ), + )?; } } }, } writeln!(f)?; - writeln!( - f, - "[ Did this not do what you expected? Could an error be more useful? ]" - )?; + writeln!(f, "[ {} ]", crate::fl!("err-ux-A"))?; write!( f, - "[ Tell us: https://str4d.xyz/age-plugin-yubikey/report ]" + "[ {}: https://str4d.xyz/age-plugin-yubikey/report {} ]", + crate::fl!("err-ux-B"), + crate::fl!("err-ux-C") ) } } diff --git a/src/key.rs b/src/key.rs index d44ecf4..f95a39d 100644 --- a/src/key.rs +++ b/src/key.rs @@ -23,6 +23,7 @@ use yubikey::{ use crate::{ error::Error, + fl, format::{RecipientLine, STANZA_KEY_LABEL}, p256::{Recipient, TAG_BYTES}, util::{otp_serial_prefix, Metadata}, @@ -44,7 +45,14 @@ pub(crate) fn filter_connected(reader: &Reader) -> bool { if let Some(pcsc::Error::RemovedCard) = e.source().and_then(|inner| inner.downcast_ref()) { - warn!("Ignoring {}: not connected", reader.name()); + warn!( + "{}", + i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "warn-yk-not-connected", + yubikey_name = reader.name(), + ) + ); false } else { true @@ -72,9 +80,16 @@ pub(crate) fn wait_for_readers() -> Result { pub(crate) fn open(serial: Option) -> Result { if !Context::open()?.iter()?.any(is_connected) { if let Some(serial) = serial { - eprintln!("⏳ Please insert the YubiKey with serial {}.", serial); + eprintln!( + "{}", + i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "open-yk-with-serial", + yubikey_serial = serial.to_string(), + ) + ); } else { - eprintln!("⏳ Please insert the YubiKey."); + eprintln!("{}", fl!("open-yk-without-serial")); } } let mut readers = wait_for_readers()?; @@ -111,32 +126,35 @@ pub(crate) fn open(serial: Option) -> Result { } pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> { + const DEFAULT_PIN: &str = "123456"; + const DEFAULT_PUK: &str = "12345678"; + eprintln!(); let pin = Password::new() - .with_prompt(&format!( - "Enter PIN for YubiKey with serial {} (default is 123456)", - yubikey.serial(), + .with_prompt(i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "mgr-enter-pin", + yubikey_serial = yubikey.serial().to_string(), + default_pin = DEFAULT_PIN, )) .interact()?; yubikey.verify_pin(pin.as_bytes())?; // If the user is using the default PIN, help them to change it. - if pin == "123456" { + if pin == DEFAULT_PIN { eprintln!(); - eprintln!("✨ Your YubiKey is using the default PIN. Let's change it!"); - eprintln!("✨ We'll also set the PUK equal to the PIN."); - eprintln!(); - eprintln!("🔐 The PIN is up to 8 numbers, letters, or symbols. Not just numbers!"); - eprintln!( - "❌ Your keys will be lost if the PIN and PUK are locked after 3 incorrect tries." - ); + eprintln!("{}", fl!("mgr-change-default-pin")); eprintln!(); let current_puk = Password::new() - .with_prompt("Enter current PUK (default is 12345678)") + .with_prompt(i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "mgr-enter-current-puk", + default_puk = DEFAULT_PUK, + )) .interact()?; let new_pin = Password::new() - .with_prompt("Choose a new PIN/PUK") - .with_confirmation("Repeat the PIN/PUK", "PINs don't match") + .with_prompt(fl!("mgr-choose-new-pin")) + .with_confirmation(fl!("mgr-repeat-new-pin"), fl!("mgr-pin-mismatch")) .interact()?; if new_pin.len() > 8 { return Err(Error::InvalidPinLength); @@ -156,16 +174,20 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> { // Migrate to a PIN-protected management key. let mgm_key = MgmKey::generate(); eprintln!(); - eprintln!("✨ Your YubiKey is using the default management key."); - eprintln!("✨ We'll migrate it to a PIN-protected management key."); + eprintln!("{}", fl!("mgr-changing-mgmt-key")); eprint!("... "); mgm_key.set_protected(yubikey).map_err(|e| { - eprintln!("An error occurred while setting the new management key."); - eprintln!("⚠️ SAVE THIS MANAGEMENT KEY - YOU MAY NEED IT TO MANAGE YOUR YubiKey! ⚠️"); - eprintln!(" {}", hex::encode(mgm_key.as_ref())); + eprintln!( + "{}", + i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "mgr-changing-mgmt-key-error", + management_key = hex::encode(mgm_key.as_ref()), + ) + ); e })?; - eprintln!("Success!"); + eprintln!("{}", fl!("mgr-changing-mgmt-key-success")); } Ok(()) @@ -246,15 +268,20 @@ impl Stub { Ok(yk) => yk, Err(yubikey::Error::NotFound) => { if callbacks - .message(&format!( - "Please insert YubiKey with serial {}", - self.serial + .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: format!("Could not find YubiKey with serial {}", self.serial), + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-yk-not-found", + yubikey_serial = self.serial.to_string(), + ), })); } @@ -267,9 +294,10 @@ impl Stub { Err(_) => { return Ok(Err(identity::Error::Identity { index: self.identity_index, - message: format!( - "Could not open YubiKey with serial {}", - self.serial + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-yk-opening", + yubikey_serial = self.serial.to_string(), ), })); } @@ -279,10 +307,11 @@ impl Stub { Ok(end) if end >= FIFTEEN_SECONDS => { return Ok(Err(identity::Error::Identity { index: self.identity_index, - message: format!( - "Timed out while waiting for YubiKey with serial {} to be inserted", - self.serial - ), + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-yk-timed-out", + yubikey_serial = self.serial.to_string(), + ), })) } _ => sleep(ONE_SECOND), @@ -292,7 +321,11 @@ impl Stub { Err(_) => { return Ok(Err(identity::Error::Identity { index: self.identity_index, - message: format!("Could not open YubiKey with serial {}", self.serial), + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-yk-opening", + yubikey_serial = self.serial.to_string(), + ), })) } }; @@ -310,7 +343,7 @@ impl Stub { None => { return Ok(Err(identity::Error::Identity { index: self.identity_index, - message: "A YubiKey stub did not match the YubiKey".to_owned(), + message: fl!("plugin-err-yk-stub-mismatch"), })) } }; @@ -356,9 +389,7 @@ impl Connection { None => { return Ok(Err(identity::Error::Identity { index: self.identity_index, - message: - "Certificate for YubiKey identity contains an invalid PIN policy" - .to_string(), + message: fl!("plugin-err-yk-invalid-pin-policy"), })) } metadata => metadata, @@ -371,10 +402,12 @@ impl Connection { // The policy requires a PIN, so request it. // Note that we can't distinguish between PinPolicy::Once and PinPolicy::Always // because this plugin is ephemeral, so we always request the PIN. - let mut message = format!( - "Enter PIN for YubiKey with serial {}", - self.yubikey.serial() + let enter_pin_msg = i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-enter-pin", + yubikey_serial = self.yubikey.serial().to_string(), ); + let mut message = enter_pin_msg.clone(); let pin = loop { message = match callbacks.request_secret(&message)? { Ok(pin) => match pin.expose_secret().len() { @@ -387,27 +420,19 @@ impl Connection { .expose_secret() .starts_with(&otp_serial_prefix(self.yubikey.serial())) => { - format!( - "Did you touch the YubiKey by accident? Enter PIN for YubiKey with serial {}", - self.yubikey.serial() - ) + format!("{} {}", fl!("plugin-err-accidental-touch"), enter_pin_msg) } // Otherwise, the PIN is either too short or too long. - 0..=5 => format!( - "PIN was too short. Enter PIN for YubiKey with serial {}", - self.yubikey.serial() - ), - _ => format!( - "PIN was too long. Enter PIN for YubiKey with serial {}", - self.yubikey.serial() - ), + 0..=5 => format!("{} {}", fl!("plugin-err-pin-too-short"), enter_pin_msg), + _ => format!("{} {}", fl!("plugin-err-pin-too-long"), enter_pin_msg), }, Err(_) => { return Ok(Err(identity::Error::Identity { index: self.identity_index, - message: format!( - "A PIN is required for YubiKey with serial {}", - self.yubikey.serial() + message: i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "plugin-err-pin-required", + yubikey_serial = self.yubikey.serial().to_string(), ), })) } @@ -435,11 +460,11 @@ impl Connection { self.last_touch, ) { (Some(TouchPolicy::Always), _) | (Some(TouchPolicy::Cached), None) => { - callbacks.message("👆 Please touch the YubiKey")?.unwrap(); + callbacks.message(&fl!("plugin-touch-yk"))?.unwrap(); true } (Some(TouchPolicy::Cached), Some(last)) if last.elapsed() >= FIFTEEN_SECONDS => { - callbacks.message("👆 Please touch the YubiKey")?.unwrap(); + callbacks.message(&fl!("plugin-touch-yk"))?.unwrap(); true } _ => false, diff --git a/src/main.rs b/src/main.rs index 6da5c38..9ab2c48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -269,8 +269,13 @@ fn print_multiple( } if printed > 1 { eprintln!( - "Generated {} for {} slots. If you intended to select a slot, use --slot.", - kind, printed, + "{}", + i18n_embed_fl::fl!( + LANGUAGE_LOADER, + "printed-multiple", + kind = kind, + count = printed, + ) ); } @@ -297,7 +302,12 @@ fn identity(flags: PluginFlags) -> Result<(), Error> { "--identity".into(), )); } - print_details("identities", flags, false, util::print_identity) + print_details( + &fl!("printed-kind-identities"), + flags, + false, + util::print_identity, + ) } fn list(flags: PluginFlags, all: bool) -> Result<(), Error> { @@ -311,10 +321,15 @@ fn list(flags: PluginFlags, all: bool) -> Result<(), Error> { )); } - print_details("recipients", flags, all, |_, recipient, metadata| { - println!("{}", metadata); - println!("{}", recipient.to_string()); - }) + print_details( + &fl!("printed-kind-recipients"), + flags, + all, + |_, recipient, metadata| { + println!("{}", metadata); + println!("{}", recipient.to_string()); + }, + ) } fn main() -> Result<(), Error> { @@ -365,23 +380,18 @@ fn main() -> Result<(), Error> { } let flags: PluginFlags = opts.try_into()?; - eprintln!("✨ Let's get your YubiKey set up for age! ✨"); - eprintln!(); - eprintln!("This tool can create a new age identity in a free slot of your YubiKey."); - eprintln!("It will generate an identity file that you can use with an age client,"); - eprintln!("along with the corresponding recipient. You can also do this directly"); - eprintln!("with:"); - eprintln!(" age-plugin-yubikey --generate"); - eprintln!(); - eprintln!("If you are already using a YubiKey with age, you can select an existing"); - eprintln!("slot to recreate its corresponding identity file and recipient."); - eprintln!(); - eprintln!("When asked below to select an option, use the up/down arrow keys to"); - eprintln!("make your choice, or press [Esc] or [q] to quit."); + eprintln!( + "{}", + i18n_embed_fl::fl!( + LANGUAGE_LOADER, + "cli-setup-intro", + generate_usage = "age-plugin-yubikey --generate", + ) + ); eprintln!(); if !Context::open()?.iter()?.any(key::is_connected) { - eprintln!("⏳ Please insert the YubiKey you want to set up."); + eprintln!("{}", fl!("cli-setup-insert-yk")); }; let mut readers = key::wait_for_readers()?; @@ -391,13 +401,18 @@ fn main() -> Result<(), Error> { let reader_names = readers_list .iter() .map(|reader| { - reader - .open() - .map(|yk| format!("{} (Serial: {})", reader.name(), yk.serial())) + reader.open().map(|yk| { + i18n_embed_fl::fl!( + LANGUAGE_LOADER, + "cli-setup-yk-name", + yubikey_name = reader.name(), + yubikey_serial = yk.serial().to_string(), + ) + }) }) .collect::, _>>()?; let mut yubikey = match Select::new() - .with_prompt("🔑 Select a YubiKey") + .with_prompt(fl!("cli-setup-select-yk")) .items(&reader_names) .default(0) .interact_opt()? @@ -440,9 +455,20 @@ fn main() -> Result<(), Error> { let i = i + 1; match occupied { - Some(Some(name)) => format!("Slot {} ({})", i, name), - Some(None) => format!("Slot {} (Unusable)", i), - None => format!("Slot {} (Empty)", i), + Some(Some(name)) => i18n_embed_fl::fl!( + LANGUAGE_LOADER, + "cli-setup-slot-usable", + slot_index = i, + slot_name = name.as_str(), + ), + Some(None) => i18n_embed_fl::fl!( + LANGUAGE_LOADER, + "cli-setup-slot-unusable", + slot_index = i, + ), + None => { + i18n_embed_fl::fl!(LANGUAGE_LOADER, "cli-setup-slot-empty", slot_index = i) + } } }) .collect(); @@ -450,7 +476,7 @@ fn main() -> Result<(), Error> { let ((stub, recipient, metadata), is_new) = { let (slot_index, slot) = loop { match Select::new() - .with_prompt("🕳️ Select a slot for your age identity") + .with_prompt(fl!("cli-setup-select-slot")) .items(&slots) .default(0) .interact_opt()? @@ -474,7 +500,11 @@ fn main() -> Result<(), Error> { }; if Confirm::new() - .with_prompt(&format!("Use existing identity in slot {}?", slot_index)) + .with_prompt(i18n_embed_fl::fl!( + LANGUAGE_LOADER, + "cli-setup-use-existing", + slot_index = slot_index, + )) .interact()? { let stub = key::Stub::new(yubikey.serial(), slot, &recipient); @@ -490,18 +520,19 @@ fn main() -> Result<(), Error> { } else { let name = Input::::new() .with_prompt(format!( - "📛 Name this identity [{}]", + "{} [{}]", + fl!("cli-setup-name-identity"), flags.name.as_deref().unwrap_or("age identity TAG_HEX") )) .allow_empty(true) .interact_text()?; let pin_policy = match Select::new() - .with_prompt("🔤 Select a PIN policy") + .with_prompt(fl!("cli-setup-select-pin-policy")) .items(&[ - "Always (A PIN is required for every decryption, if set)", - "Once (A PIN is required once per session, if set)", - "Never (A PIN is NOT required to decrypt)", + fl!("pin-policy-always"), + fl!("pin-policy-once"), + fl!("pin-policy-never"), ]) .default( [PinPolicy::Always, PinPolicy::Once, PinPolicy::Never] @@ -521,30 +552,35 @@ fn main() -> Result<(), Error> { }; let touch_policy = match Select::new() - .with_prompt("👆 Select a touch policy") - .items(&[ - "Always (A physical touch is required for every decryption)", - "Cached (A physical touch is required for decryption, and is cached for 15 seconds)", - "Never (A physical touch is NOT required to decrypt)", - ]) - .default( - [TouchPolicy::Always, TouchPolicy::Cached, TouchPolicy::Never] - .iter() - .position(|p| p == &flags - .touch_policy.unwrap_or(builder::DEFAULT_TOUCH_POLICY)) - .unwrap(), - ) - .interact_opt()? - { - Some(0) => TouchPolicy::Always, - Some(1) => TouchPolicy::Cached, - Some(2) => TouchPolicy::Never, - Some(_) => unreachable!(), - None => return Ok(()), - }; + .with_prompt(fl!("cli-setup-select-touch-policy")) + .items(&[ + fl!("touch-policy-always"), + fl!("touch-policy-cached"), + fl!("touch-policy-never"), + ]) + .default( + [TouchPolicy::Always, TouchPolicy::Cached, TouchPolicy::Never] + .iter() + .position(|p| { + p == &flags.touch_policy.unwrap_or(builder::DEFAULT_TOUCH_POLICY) + }) + .unwrap(), + ) + .interact_opt()? + { + Some(0) => TouchPolicy::Always, + Some(1) => TouchPolicy::Cached, + Some(2) => TouchPolicy::Never, + Some(_) => unreachable!(), + None => return Ok(()), + }; if Confirm::new() - .with_prompt(&format!("Generate new identity in slot {}?", slot_index)) + .with_prompt(i18n_embed_fl::fl!( + LANGUAGE_LOADER, + "cli-setup-generate-new", + slot_index = slot_index, + )) .interact()? { eprintln!(); @@ -567,7 +603,7 @@ fn main() -> Result<(), Error> { eprintln!(); let file_name = Input::::new() - .with_prompt("📝 File name to write this identity to") + .with_prompt(fl!("cli-setup-identity-file-name")) .default(format!( "age-yubikey-identity-{}.txt", hex::encode(stub.tag) @@ -582,7 +618,7 @@ fn main() -> Result<(), Error> { Ok(file) => file, Err(e) if e.kind() == io::ErrorKind::AlreadyExists => { if Confirm::new() - .with_prompt("File exists. Overwrite it?") + .with_prompt(fl!("cli-setup-identity-file-exists")) .interact()? { File::create(&file_name)? @@ -593,54 +629,56 @@ fn main() -> Result<(), Error> { Err(e) => return Err(e.into()), }; - writeln!(file, "{}", metadata)?; - writeln!(file, "# Recipient: {}", recipient)?; - writeln!(file, "{}", stub.to_string())?; + writeln!( + file, + "{}", + i18n_embed_fl::fl!( + LANGUAGE_LOADER, + "yubikey-identity", + yubikey_metadata = metadata.to_string(), + recipient = recipient.to_string(), + identity = stub.to_string(), + ) + )?; file.sync_data()?; // If `rage` binary is installed, use it in examples. Otherwise default to `age`. let age_binary = which::which("rage").map(|_| "rage").unwrap_or("age"); - eprintln!(); - eprintln!("✅ Done! This YubiKey identity is ready to go."); - eprintln!(); - if is_new { - eprintln!("🔑 Here's your shiny new YubiKey recipient:"); - } else { - eprintln!("🔑 Here's the corresponding YubiKey recipient:"); - } - eprintln!(" {}", recipient); - eprintln!(); - eprintln!("Here are some example things you can do with it:"); - eprintln!(); - eprintln!("- Encrypt a file to this identity:"); - eprintln!( - " $ cat foo.txt | {} -r {} -o foo.txt.age", + let encrypt_usage = format!( + "$ cat foo.txt | {} -r {} -o foo.txt.age", age_binary, recipient ); - eprintln!(); - eprintln!("- Decrypt a file with this identity:"); - eprintln!( - " $ cat foo.txt.age | {} -d -i {} > foo.txt", + let decrypt_usage = format!( + "$ cat foo.txt.age | {} -d -i {} > foo.txt", age_binary, file_name ); - eprintln!(); - eprintln!("- Recreate the identity file:"); - eprintln!( - " $ age-plugin-yubikey -i --serial {} --slot {} > {}", + let identity_usage = format!( + "$ age-plugin-yubikey -i --serial {} --slot {} > {}", stub.serial, util::slot_to_ui(&stub.slot), file_name, ); - eprintln!(); - eprintln!("- Recreate the recipient:"); - eprintln!( - " $ age-plugin-yubikey -l --serial {} --slot {}", + let recipient_usage = format!( + "$ age-plugin-yubikey -l --serial {} --slot {}", stub.serial, util::slot_to_ui(&stub.slot), ); + eprintln!(); - eprintln!("💭 Remember: everything breaks, have a backup plan for when this YubiKey does."); + eprintln!( + "{}", + i18n_embed_fl::fl!( + LANGUAGE_LOADER, + "cli-setup-finished", + is_new = if is_new { "true" } else { "false" }, + recipient = recipient.to_string(), + encrypt_usage = encrypt_usage, + decrypt_usage = decrypt_usage, + identity_usage = identity_usage, + recipient_usage = recipient_usage, + ) + ); Ok(()) } diff --git a/src/plugin.rs b/src/plugin.rs index 8df54ea..e6671d1 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -7,7 +7,7 @@ use age_plugin::{ use std::collections::HashMap; use std::io; -use crate::{format, key, p256::Recipient, PLUGIN_NAME}; +use crate::{fl, format, key, p256::Recipient, PLUGIN_NAME}; #[derive(Debug, Default)] pub(crate) struct RecipientPlugin { @@ -32,7 +32,7 @@ impl RecipientPluginV1 for RecipientPlugin { } else { Err(recipient::Error::Recipient { index, - message: "Invalid recipient".to_owned(), + message: fl!("plugin-err-invalid-recipient"), }) } } @@ -53,7 +53,7 @@ impl RecipientPluginV1 for RecipientPlugin { } else { Err(recipient::Error::Identity { index, - message: "Invalid Yubikey stub".to_owned(), + message: fl!("plugin-err-invalid-identity"), }) } } @@ -120,7 +120,7 @@ impl IdentityPluginV1 for IdentityPlugin { } else { Err(identity::Error::Identity { index, - message: "Invalid Yubikey stub".to_owned(), + message: fl!("plugin-err-invalid-identity"), }) } } @@ -146,7 +146,7 @@ impl IdentityPluginV1 for IdentityPlugin { res.map_err(|_| identity::Error::Stanza { file_index: file, stanza_index, - message: "Invalid yubikey stanza".to_owned(), + message: fl!("plugin-err-invalid-stanza"), }) }), file_keys.contains_key(&file), @@ -232,7 +232,7 @@ impl IdentityPluginV1 for IdentityPlugin { .error(identity::Error::Stanza { file_index, stanza_index, - message: "Failed to decrypt YubiKey stanza".to_owned(), + message: fl!("plugin-err-decryption-failed"), })? .unwrap(), } diff --git a/src/util.rs b/src/util.rs index 5a07158..5e04f6b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -7,6 +7,7 @@ use yubikey::{ PinPolicy, Serial, TouchPolicy, YubiKey, }; +use crate::fl; use crate::{error::Error, key::Stub, p256::Recipient, BINARY_NAME, USABLE_SLOTS}; pub(crate) const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8]; @@ -42,23 +43,21 @@ pub(crate) fn touch_policy_from_string(s: String) -> Result } } -pub(crate) fn pin_policy_to_str(policy: Option) -> &'static str { +pub(crate) fn pin_policy_to_str(policy: Option) -> String { match policy { - Some(PinPolicy::Always) => "Always (A PIN is required for every decryption, if set)", - Some(PinPolicy::Once) => "Once (A PIN is required once per session, if set)", - Some(PinPolicy::Never) => "Never (A PIN is NOT required to decrypt)", - _ => "Unknown", + Some(PinPolicy::Always) => fl!("pin-policy-always"), + Some(PinPolicy::Once) => fl!("pin-policy-once"), + Some(PinPolicy::Never) => fl!("pin-policy-never"), + _ => fl!("unknown-policy"), } } -pub(crate) fn touch_policy_to_str(policy: Option) -> &'static str { +pub(crate) fn touch_policy_to_str(policy: Option) -> String { match policy { - Some(TouchPolicy::Always) => "Always (A physical touch is required for every decryption)", - Some(TouchPolicy::Cached) => { - "Cached (A physical touch is required for decryption, and is cached for 15 seconds)" - } - Some(TouchPolicy::Never) => "Never (A physical touch is NOT required to decrypt)", - _ => "Unknown", + Some(TouchPolicy::Always) => fl!("touch-policy-always"), + Some(TouchPolicy::Cached) => fl!("touch-policy-cached"), + Some(TouchPolicy::Never) => fl!("touch-policy-never"), + _ => fl!("unknown-policy"), } } @@ -178,19 +177,19 @@ impl Metadata { impl fmt::Display for Metadata { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!( - f, - "# Serial: {}, Slot: {}", - self.serial, - slot_to_ui(&self.slot) - )?; - writeln!(f, "# Name: {}", self.name)?; - writeln!(f, "# Created: {}", self.created)?; - writeln!(f, "# PIN policy: {}", pin_policy_to_str(self.pin_policy))?; write!( f, - "# Touch policy: {}", - touch_policy_to_str(self.touch_policy) + "{}", + i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "yubikey-metadata", + serial = self.serial.to_string(), + slot = slot_to_ui(&self.slot), + name = self.name.as_str(), + created = self.created.as_str(), + pin_policy = pin_policy_to_str(self.pin_policy), + touch_policy = touch_policy_to_str(self.touch_policy), + ) ) } } @@ -198,10 +197,24 @@ impl fmt::Display for Metadata { pub(crate) fn print_identity(stub: Stub, recipient: Recipient, metadata: Metadata) { let recipient = recipient.to_string(); if !console::user_attended() { - eprintln!("Recipient: {}", recipient); + eprintln!( + "{}", + i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "print-recipient", + recipient = recipient.as_str(), + ) + ); } - println!("{}", metadata); - println!("# Recipient: {}", recipient); - println!("{}", stub.to_string()); + println!( + "{}", + i18n_embed_fl::fl!( + crate::LANGUAGE_LOADER, + "yubikey-identity", + yubikey_metadata = metadata.to_string(), + recipient = recipient, + identity = stub.to_string(), + ) + ); }