From c4fe3f6b1a38759d131f603e131300a62a90d994 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 1 May 2022 11:49:06 +0000 Subject: [PATCH 1/2] Add support for translations --- Cargo.lock | 459 ++++++++++++++++++++++++++++++ Cargo.toml | 6 + i18n.toml | 4 + i18n/en-US/age_plugin_yubikey.ftl | 13 + src/main.rs | 29 ++ 5 files changed, 511 insertions(+) create mode 100644 i18n.toml create mode 100644 i18n/en-US/age_plugin_yubikey.ftl diff --git a/Cargo.lock b/Cargo.lock index 10cf781..c415853 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,11 +59,15 @@ dependencies = [ "flate2", "gumdrop", "hex", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", "log", "man", "p256", "pcsc", "rand", + "rust-embed", "sha2", "which", "x509", @@ -130,6 +134,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.9.0" @@ -265,6 +275,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "dashmap" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8858831f7781322e539ea39e72449c46b059638250c14344fec8d0aa6e539c" +dependencies = [ + "cfg-if", + "num_cpus", + "parking_lot", +] + [[package]] name = "data-encoding" version = "2.3.2" @@ -408,6 +429,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml", +] + [[package]] name = "flate2" version = "1.0.22" @@ -420,6 +450,50 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78" +dependencies = [ + "thiserror", +] + [[package]] name = "generic-array" version = "0.14.5" @@ -513,6 +587,75 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "i18n-config" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62affcd43abfb51f3cbd8736f9407908dc5b44fc558a9be07460bbfd104d983" +dependencies = [ + "log", + "serde", + "serde_derive", + "thiserror", + "toml", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f21ed76e44de8ac3dfa36bb37ab2e6480be0dc75c612474949be1f3cb2c253" +dependencies = [ + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "lazy_static", + "locale_config", + "log", + "parking_lot", + "rust-embed", + "thiserror", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420a9718ef9d0ab727840a398e25408ea0daff9ba3c681707ba05485face98e" +dependencies = [ + "dashmap", + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "strsim", + "syn", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0db2330e035808eb064afb67e6743ddce353763af3e0f2bdfc2476e00ce76136" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "instant" version = "0.1.12" @@ -522,6 +665,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "intl-memoizer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18f988384267d7066cc2be425e6faf352900652c046b6971d2e228d3b1c5ecf" +dependencies = [ + "tinystr", + "unic-langid", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -543,6 +706,29 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg 1.1.0", + "scopeguard", +] + [[package]] name = "log" version = "0.4.14" @@ -552,6 +738,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "man" version = "0.3.0" @@ -654,6 +849,45 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "oid-registry" version = "0.2.0" @@ -695,6 +929,29 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "pbkdf2" version = "0.9.0" @@ -779,6 +1036,30 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -888,6 +1169,46 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a17e5ac65b318f397182ae94e532da0ba56b88dd1200b774715d36c4943b1c3" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e763e24ba2bf0c72bc6be883f967f794a019fafd1b86ba1daff9c91a7edd30" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756feca3afcbb1487a1d01f4ecd94cf8ec98ea074c55a69e7136d29fb6166029" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -897,6 +1218,21 @@ dependencies = [ "nom", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "secrecy" version = "0.8.0" @@ -906,11 +1242,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "self_cell" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af" + [[package]] name = "serde" version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "sha-1" @@ -969,6 +1325,12 @@ dependencies = [ "der", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" @@ -1070,12 +1432,55 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinystr" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29738eedb4388d9ea620eeab9384884fc3f06f586a2eddb56bedc5885126c7c1" + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "type-map" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "unic-langid" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73328fcd730a030bdb19ddf23e192187a6b01cd98be6d3140622a89129459ce5" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a4a8eeaf0494862c1404c95ec2f4c33a2acff5076f64314b465e3ddae1b934d" +dependencies = [ + "serde", + "tinystr", +] + [[package]] name = "unicode-width" version = "0.1.9" @@ -1113,6 +1518,17 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" @@ -1161,6 +1577,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "x509" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 83df6b9..49ef1b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,12 @@ x509 = "0.2" x509-parser = "0.12" yubikey = { version = "0.5", features = ["untested"] } +# Translations +i18n-embed = { version = "0.13", features = ["desktop-requester", "fluent-system"] } +i18n-embed-fl = "0.6" +lazy_static = "1" +rust-embed = "6" + [dev-dependencies] flate2 = "1" man = "0.3" diff --git a/i18n.toml b/i18n.toml new file mode 100644 index 0000000..40d065c --- /dev/null +++ b/i18n.toml @@ -0,0 +1,4 @@ +fallback_language = "en-US" + +[fluent] +assets_dir = "i18n" diff --git a/i18n/en-US/age_plugin_yubikey.ftl b/i18n/en-US/age_plugin_yubikey.ftl new file mode 100644 index 0000000..0512a6d --- /dev/null +++ b/i18n/en-US/age_plugin_yubikey.ftl @@ -0,0 +1,13 @@ +# Copyright 2022 Jack Grigg +# +# Licensed under the Apache License, Version 2.0 or the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +### Localization for strings in age-plugin-yubikey + +-age = age +-yubikey = YubiKey +-age-plugin-yubikey = age-plugin-yubikey diff --git a/src/main.rs b/src/main.rs index 743cc56..6da5c38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,12 @@ use std::io::{self, Write}; use age_plugin::run_state_machine; use dialoguer::{Confirm, Input, Select}; use gumdrop::Options; +use i18n_embed::{ + fluent::{fluent_language_loader, FluentLanguageLoader}, + DesktopLanguageRequester, +}; +use lazy_static::lazy_static; +use rust_embed::RustEmbed; use yubikey::{ certificate::PublicKeyInfo, piv::{RetiredSlotId, SlotId}, @@ -52,6 +58,23 @@ const USABLE_SLOTS: [RetiredSlotId; 20] = [ RetiredSlotId::R20, ]; +#[derive(RustEmbed)] +#[folder = "i18n"] +struct Translations; + +const TRANSLATIONS: Translations = Translations {}; + +lazy_static! { + static ref LANGUAGE_LOADER: FluentLanguageLoader = fluent_language_loader!(); +} + +#[macro_export] +macro_rules! fl { + ($message_id:literal) => {{ + i18n_embed_fl::fl!($crate::LANGUAGE_LOADER, $message_id) + }}; +} + #[derive(Debug, Options)] struct PluginOptions { #[options(help = "Print this help message and exit.")] @@ -301,6 +324,12 @@ fn main() -> Result<(), Error> { .parse_default_env() .init(); + let requested_languages = DesktopLanguageRequester::requested_languages(); + i18n_embed::select(&*LANGUAGE_LOADER, &TRANSLATIONS, &requested_languages).unwrap(); + // Unfortunately the common Windows terminals don't support Unicode Directionality + // Isolation Marks, so we disable them for now. + LANGUAGE_LOADER.set_use_isolating(false); + let opts = PluginOptions::parse_args_default_or_exit(); if [opts.generate, opts.identity, opts.list, opts.list_all] From a92a843e14e64459925a92efe87aa51929c601d5 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 1 May 2022 12:55:48 +0000 Subject: [PATCH 2/2] 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(), + ) + ); }