diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0902c8..be0c914 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,7 +86,7 @@ jobs: if: matrix.name == 'windows' - name: Upload archive to release - uses: svenstaro/upload-release-action@2.6.1 + uses: svenstaro/upload-release-action@2.9.0 with: file: ${{ matrix.archive_name }} asset_name: age-plugin-yubikey-$tag-${{ matrix.asset_suffix }} @@ -144,7 +144,7 @@ jobs: args: --package age-plugin-yubikey --no-build --target ${{ matrix.target }} - name: Upload Debian package to release - uses: svenstaro/upload-release-action@2.6.1 + uses: svenstaro/upload-release-action@2.9.0 with: file: target/${{ matrix.target }}/debian/*.deb file_glob: true diff --git a/CHANGELOG.md b/CHANGELOG.md index a3049a6..6b3068a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ to 0.3.0 are beta releases. ## [Unreleased] +### Added +- Support for the native non-hybrid tagged recipient type (`age1tag1..`). + - Encryption requires making the `age-plugin-yubikey` binary available on the + `PATH` as `age-plugin-tag`, or upgrading to a client version that builds in + support for this new native recipient type. + +### Changed +- MSRV is now 1.70.0. +- Encryption to an identity now uses the preferred recipient type supported for + that identity. +- `age-plugin-yubikey` now prints `age1tag1..` recipients in its CLI and + identity files instead of `age1yubikey1..` recipients. The latter is now only + shown in comments for identities generated with `age-plugin-yubikey 0.5.0` or + earlier. + ## [0.3.4], [0.4.1], [0.5.1] - 2026-04-08 ### Fixed - `age-plugin-yubikey` now completely ignores any identity that has unrecognised diff --git a/Cargo.lock b/Cargo.lock index 13464e4..e0d9b55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aead" @@ -28,31 +28,56 @@ dependencies = [ ] [[package]] -name = "age-core" -version = "0.10.0" +name = "aes" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5f11899bc2bbddd135edbc30c36b1924fa59d0746bb45beb5933fafe3fe509b" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ - "base64 0.21.7", + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "age-core" +version = "0.11.0" +source = "git+https://github.com/str4d/rage.git?rev=e08c450aa5d7b1cc5706094080c0042ddd60aaf7#e08c450aa5d7b1cc5706094080c0042ddd60aaf7" +dependencies = [ + "base64 0.22.1", + "bech32", "chacha20poly1305", "cookie-factory", "hkdf", + "hpke", "io_tee", - "nom", + "nom 8.0.0", "rand", - "secrecy", + "secrecy 0.10.3", "sha2", "tempfile", ] [[package]] name = "age-plugin" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04740d993aac0e06eaf4cfbf8484a8a23a6a7f950cf5d53bdf2d6ea3f429eb9d" +version = "0.6.1" +source = "git+https://github.com/str4d/rage.git?rev=e08c450aa5d7b1cc5706094080c0042ddd60aaf7#e08c450aa5d7b1cc5706094080c0042ddd60aaf7" dependencies = [ "age-core", - "base64 0.21.7", + "base64 0.22.1", "bech32", "chrono", ] @@ -63,7 +88,7 @@ version = "0.5.1" dependencies = [ "age-core", "age-plugin", - "base64 0.21.7", + "base64 0.22.1", "bech32", "console", "dialoguer", @@ -71,6 +96,8 @@ dependencies = [ "flate2", "gumdrop", "hex", + "hkdf", + "hpke", "i18n-embed", "i18n-embed-fl", "lazy_static", @@ -129,7 +156,7 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror", @@ -161,23 +188,23 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -198,6 +225,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -205,10 +238,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] -name = "bech32" -version = "0.9.1" +name = "basic-toml" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bitflags" @@ -261,15 +303,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.1.7" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -366,15 +411,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -432,17 +477,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] [[package]] -name = "dashmap" -version = "5.5.3" +name = "ctr" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", - "hashbrown", + "crossbeam-utils", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -473,7 +529,7 @@ checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" dependencies = [ "asn1-rs", "displaydoc", - "nom", + "nom 7.1.3", "num-bigint", "num-traits", "rusticata-macros", @@ -529,7 +585,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.86", ] [[package]] @@ -581,9 +637,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -619,9 +675,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "ff" @@ -639,14 +695,14 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" dependencies = [ - "toml 0.5.11", + "toml", ] [[package]] name = "flate2" -version = "1.0.31" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -728,9 +784,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -743,9 +799,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -753,15 +809,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -770,38 +826,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.86", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -838,10 +894,20 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.29.0" +name = "ghash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "group" @@ -899,12 +965,24 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -938,6 +1016,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "hpke" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4917627a14198c3603282c5158b815ad5534795451d3c074b53cf3cee0960b11" +dependencies = [ + "aead", + "aes-gcm", + "chacha20poly1305", + "digest", + "generic-array", + "hkdf", + "hmac", + "p256", + "rand_core", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "http" version = "0.2.12" @@ -962,9 +1060,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -980,9 +1078,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -995,7 +1093,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -1017,23 +1115,23 @@ dependencies = [ [[package]] name = "i18n-config" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ce3c48cbc21fd5b22b9331f32b5b51f6ad85d969b99e793427332e76e7640" +checksum = "8e88074831c0be5b89181b05e6748c4915f77769ecc9a4c372f88b169a8509c9" dependencies = [ + "basic-toml", "log", "serde", "serde_derive", "thiserror", - "toml 0.8.19", "unic-langid", ] [[package]] name = "i18n-embed" -version = "0.14.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94205d95764f5bb9db9ea98fa77f89653365ca748e27161f5bbea2ffd50e459c" +checksum = "a7839d8c7bb8da7bd58c1112d3a1aeb7f178ff3df4ae87783e758ca3bfb750b7" dependencies = [ "arc-swap", "fluent", @@ -1053,9 +1151,9 @@ dependencies = [ [[package]] name = "i18n-embed-fl" -version = "0.8.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8241a781f49e923415e106fcd1f89c3fab92cc9f699a521c56e95dee273903d3" +checksum = "f6e9571c3cba9eba538eaa5ee40031b26debe76f0c7e17bafc97ea57a76cd82e" dependencies = [ "dashmap", "find-crate", @@ -1064,32 +1162,32 @@ dependencies = [ "i18n-config", "i18n-embed", "lazy_static", - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", "strsim", - "syn 2.0.72", + "syn 2.0.86", "unic-langid", ] [[package]] name = "i18n-embed-impl" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81093c4701672f59416582fe3145676126fd23ba5db910acad0793c1108aaa58" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" dependencies = [ "find-crate", "i18n-config", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.86", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1120,12 +1218,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.3.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.0", ] [[package]] @@ -1164,17 +1262,17 @@ checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] @@ -1187,9 +1285,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -1205,15 +1303,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "linux-raw-sys" @@ -1288,11 +1386,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -1333,6 +1431,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -1412,7 +1519,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] @@ -1447,9 +1554,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.2" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] @@ -1465,9 +1572,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "opaque-debug" @@ -1477,9 +1584,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -1498,7 +1605,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.86", ] [[package]] @@ -1509,9 +1616,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -1612,9 +1719,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -1656,9 +1763,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "poly1305" @@ -1671,6 +1778,18 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1720,19 +1839,41 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.86" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.86", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -1789,18 +1930,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -1810,9 +1951,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -1821,9 +1962,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -1921,7 +2062,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.72", + "syn 2.0.86", "walkdir", ] @@ -1953,14 +2094,14 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags 2.6.0", "errno", @@ -1995,11 +2136,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2031,6 +2172,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2046,9 +2196,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -2071,29 +2221,29 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.86", ] [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -2101,15 +2251,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2150,6 +2291,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signature" version = "2.2.0" @@ -2213,9 +2360,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" @@ -2236,9 +2383,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" dependencies = [ "proc-macro2", "quote", @@ -2301,15 +2448,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.11.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2335,7 +2482,7 @@ dependencies = [ "quote", "regex", "reqwest", - "syn 2.0.72", + "syn 2.0.86", "sysinfo", "users", "which", @@ -2343,22 +2490,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.86", ] [[package]] @@ -2463,45 +2610,11 @@ dependencies = [ "serde", ] -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -2564,36 +2677,36 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" @@ -2634,9 +2747,9 @@ checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", ] @@ -2680,34 +2793,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.86", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -2717,9 +2831,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2727,28 +2841,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.86", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -2789,7 +2903,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -2825,6 +2939,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2946,15 +3069,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.50.0" @@ -2986,7 +3100,7 @@ dependencies = [ "data-encoding", "der-parser", "lazy_static", - "nom", + "nom 7.1.3", "oid-registry", "rusticata-macros", "thiserror", @@ -3007,7 +3121,7 @@ dependencies = [ "elliptic-curve", "hmac", "log", - "nom", + "nom 7.1.3", "num-bigint-dig", "num-integer", "num-traits", @@ -3017,7 +3131,7 @@ dependencies = [ "pcsc", "rand_core", "rsa", - "secrecy", + "secrecy 0.8.0", "sha1", "sha2", "subtle", @@ -3045,7 +3159,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.86", ] [[package]] @@ -3053,3 +3167,17 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] diff --git a/Cargo.toml b/Cargo.toml index b4661ad..22564e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ keywords = ["age", "cli", "encryption", "yubikey"] categories = ["command-line-utilities", "cryptography"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.67" # MSRV +rust-version = "1.70" # MSRV [package.metadata.deb] extended-description = """\ @@ -22,15 +22,17 @@ assets = [ ] [dependencies] -age-core = "0.10" -age-plugin = "0.5" -base64 = "0.21" -bech32 = "0.9" +age-core = "0.11" +age-plugin = "0.6" +base64 = "0.22" +bech32 = "0.11" console = { version = "0.15", default-features = false } dialoguer = { version = "0.11", default-features = false, features = ["password"] } env_logger = "0.10" gumdrop = "0.8" hex = "0.4" +hkdf = "0.12" +hpke = { version = "0.12", default-features = false, features = ["alloc", "p256"] } log = "0.4" p256 = { version = "0.13", features = ["ecdh"] } pcsc = "2.4" @@ -42,8 +44,8 @@ x509-parser = "0.14" yubikey = { version = "=0.8.0-pre.0", features = ["untested"] } # Translations -i18n-embed = { version = "0.14", features = ["desktop-requester", "fluent-system"] } -i18n-embed-fl = "0.8" +i18n-embed = { version = "0.15", features = ["desktop-requester", "fluent-system"] } +i18n-embed-fl = "0.9" lazy_static = "1" rust-embed = "8" @@ -56,3 +58,7 @@ man = "0.3" tempfile = "3" test-with = "0.11" which = "5" + +[patch.crates-io] +age-core = { git = "https://github.com/str4d/rage.git", rev = "e08c450aa5d7b1cc5706094080c0042ddd60aaf7" } +age-plugin = { git = "https://github.com/str4d/rage.git", rev = "e08c450aa5d7b1cc5706094080c0042ddd60aaf7" } diff --git a/README.md b/README.md index daf6f61..5235051 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ which enables files to be encrypted to age identities stored on YubiKeys. | Environment | CLI command | |-------------|-------------| -| Cargo (Rust 1.67+) | `cargo install age-plugin-yubikey` | +| Cargo (Rust 1.70+) | `cargo install age-plugin-yubikey` | | Homebrew (macOS or Linux) | `brew install age-plugin-yubikey` | | Arch Linux | `pacman -S age-plugin-yubikey` | | Debian | [Debian package](https://github.com/str4d/age-plugin-yubikey/releases) | @@ -108,15 +108,17 @@ standard output: $ age-plugin-yubikey --list ``` -To encrypt files to these YubiKey recipients, ensure that `age-plugin-yubikey` -is accessible in your `PATH`, and then use the recipients with an age client as -normal (e.g. `rage -r age1yubikey1...`). +To encrypt files to these YubiKey recipients, ensure you have a recent version +of an age client, and then use the recipients with it as normal (e.g. +`rage -r age1tag1...`). If this does not work, make `age-plugin-yubikey` +accessible in your `PATH` with the name `age-plugin-tag` and try again. The output of the `--list` command can also be used directly to encrypt files to all recipients (e.g. `age -R filename.txt`). -To decrypt files encrypted to a YubiKey identity, pass the identity file to the -age client as normal (e.g. `rage -d -i yubikey-identity.txt`). +To decrypt files encrypted to a YubiKey identity, ensure that +`age-plugin-yubikey` is accessible in your `PATH`, and then pass the identity +file to the age client as normal (e.g. `rage -d -i yubikey-identity.txt`). ## Advanced topics diff --git a/i18n/en-US/age_plugin_yubikey.ftl b/i18n/en-US/age_plugin_yubikey.ftl index 2174b6c..0baf801 100644 --- a/i18n/en-US/age_plugin_yubikey.ftl +++ b/i18n/en-US/age_plugin_yubikey.ftl @@ -43,6 +43,8 @@ yubikey-metadata = # Created: {$created} # PIN policy: {$pin_policy} # Touch policy: {$touch_policy} +yubikey-legacy-recipient = + # Legacy recipient: {$recipient} yubikey-identity = {$yubikey_metadata} # Recipient: {$recipient} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4219db3..5299106 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.67.0" +channel = "1.70.0" components = ["clippy", "rustfmt"] diff --git a/src/builder.rs b/src/builder.rs index 653789e..aa4a534 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,9 +11,9 @@ use crate::{ error::Error, fl, key::{self, Stub}, - p256::Recipient, + native::p256tag, util::{Metadata, POLICY_EXTENSION_OID}, - BINARY_NAME, USABLE_SLOTS, + Recipient, BINARY_NAME, USABLE_SLOTS, }; pub(crate) const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once; @@ -104,7 +104,9 @@ impl IdentityBuilder { touch_policy, )?; - let recipient = Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey"); + let recipient = Recipient::P256Tag( + p256tag::Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey"), + ); let stub = Stub::new(yubikey.serial(), slot, &recipient); eprintln!(); diff --git a/src/key.rs b/src/key.rs index 9f0df81..b5d5e28 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,12 +1,8 @@ //! Structs for handling YubiKeys. -use age_core::{ - format::{FileKey, FILE_KEY_BYTES}, - primitives::{aead_decrypt, hkdf}, - secrecy::{ExposeSecret, SecretString}, -}; +use age_core::primitives::bech32_encode; +use age_core::secrecy::{ExposeSecret, SecretString}; use age_plugin::{identity, Callbacks}; -use bech32::{ToBase32, Variant}; use dialoguer::Password; use log::{debug, error, warn}; use std::convert::Infallible; @@ -26,10 +22,10 @@ use yubikey::{ use crate::{ error::Error, fl, - format::{RecipientLine, STANZA_KEY_LABEL}, - p256::{Recipient, TAG_BYTES}, + native::p256tag, + recipient::TAG_BYTES, util::{otp_serial_prefix, Metadata, POLICY_EXTENSION_OID}, - IDENTITY_PREFIX, + Recipient, IDENTITY_PREFIX, }; const ONE_SECOND: Duration = Duration::from_secs(1); @@ -336,7 +332,7 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> { .with_prompt(fl!("mgr-choose-new-pin")) .with_confirmation(fl!("mgr-repeat-new-pin"), fl!("mgr-pin-mismatch")) .interact() - .map(|pin| Result::<_, Infallible>::Ok(SecretString::new(pin))) + .map(|pin| Result::<_, Infallible>::Ok(SecretString::from(pin))) }, yubikey.serial(), )? @@ -413,7 +409,7 @@ pub(crate) fn identify_recipient(cert: &Certificate) -> Option { return None; } - Recipient::from_certificate(cert) + p256tag::Recipient::from_certificate(cert).map(Recipient::P256Tag) } /// Returns an iterator of keys that are occupying plugin-compatible slots, along with the @@ -453,14 +449,9 @@ pub struct Stub { impl fmt::Display for Stub { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str( - bech32::encode( - IDENTITY_PREFIX, - self.to_bytes().to_base32(), - Variant::Bech32, - ) - .expect("HRP is valid") - .to_uppercase() - .as_str(), + bech32_encode(IDENTITY_PREFIX, &self.to_bytes()) + .to_uppercase() + .as_str(), ) } } @@ -480,7 +471,7 @@ impl Stub { Stub { serial, slot, - tag: recipient.tag(), + tag: recipient.static_tag(), identity_index: 0, } } @@ -507,10 +498,6 @@ impl Stub { bytes } - pub(crate) fn matches(&self, line: &RecipientLine) -> bool { - self.tag == line.tag - } - /// Returns: /// - `Ok(Ok(Some(connection)))` if we successfully connected to this YubiKey. /// - `Ok(Ok(None))` if the user told us to skip this YubiKey. @@ -632,8 +619,9 @@ impl Stub { let (cert, pk) = match Certificate::read(&mut yubikey, SlotId::Retired(self.slot)) .ok() .and_then(|cert| { + // Parse as the preferred recipient for each identity type. identify_recipient(&cert) - .filter(|recipient| recipient.tag() == self.tag) + .filter(|recipient| recipient.static_tag() == self.tag) .map(|r| (cert, r)) }) { Some(pk) => pk, @@ -650,7 +638,6 @@ impl Stub { cert, pk, slot: self.slot, - tag: self.tag, identity_index: self.identity_index, cached_metadata: None, last_touch: None, @@ -663,17 +650,21 @@ pub(crate) struct Connection { cert: Certificate, pk: Recipient, slot: RetiredSlotId, - tag: [u8; 4], identity_index: usize, cached_metadata: Option, last_touch: Option, } impl Connection { + /// Returns the preferred recipient for encrypting to this identity. pub(crate) fn recipient(&self) -> &Recipient { &self.pk } + pub(crate) fn stub(&self) -> Stub { + Stub::new(self.yubikey.serial(), self.slot, &self.pk) + } + pub(crate) fn request_pin_if_necessary( &mut self, callbacks: &mut dyn Callbacks, @@ -732,8 +723,10 @@ impl Connection { Ok(Ok(())) } - pub(crate) fn unwrap_file_key(&mut self, line: &RecipientLine) -> Result { - assert_eq!(self.tag, line.tag); + pub(crate) fn p256_ecdh(&mut self, epk_bytes: &[u8]) -> Result { + // The YubiKey API for performing scalar multiplication takes the point in its + // uncompressed SEC-1 encoding. + assert_eq!(epk_bytes.len(), 65); // Check if the touch policy requires a touch. let needs_touch = match ( @@ -745,11 +738,9 @@ impl Connection { _ => false, }; - // The YubiKey API for performing scalar multiplication takes the point in its - // uncompressed SEC-1 encoding. let shared_secret = match decrypt_data( &mut self.yubikey, - line.epk_bytes.decompress().as_bytes(), + epk_bytes, AlgorithmId::EccP256, SlotId::Retired(self.slot), ) { @@ -766,20 +757,7 @@ impl Connection { } } - let mut salt = vec![]; - salt.extend_from_slice(line.epk_bytes.as_bytes()); - salt.extend_from_slice(self.pk.to_encoded().as_bytes()); - - let enc_key = hkdf(&salt, STANZA_KEY_LABEL, shared_secret.as_ref()); - - // A failure to decrypt is fatal, because we assume that we won't - // encounter 32-bit collisions on the key tag embedded in the header. - match aead_decrypt(&enc_key, FILE_KEY_BYTES, &line.encrypted_file_key) { - Ok(pt) => Ok(TryInto::<[u8; FILE_KEY_BYTES]>::try_into(&pt[..]) - .unwrap() - .into()), - Err(_) => Err(()), - } + Ok(shared_secret) } /// Close this connection without resetting the YubiKey. diff --git a/src/main.rs b/src/main.rs index 0bbb5f4..24d7c0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,19 +16,20 @@ use yubikey::{piv::RetiredSlotId, reader::Context, PinPolicy, Serial, TouchPolic mod builder; mod error; -mod format; mod key; -mod p256; +mod native; +mod piv_p256; mod plugin; mod util; +mod recipient; +use recipient::Recipient; + use error::Error; const PLUGIN_NAME: &str = "yubikey"; const BINARY_NAME: &str = "age-plugin-yubikey"; -const RECIPIENT_PREFIX: &str = "age1yubikey"; -const IDENTITY_PREFIX: &str = "age-plugin-yubikey-"; -const STANZA_TAG: &str = "piv-p256"; +const IDENTITY_PREFIX: bech32::Hrp = bech32::Hrp::parse_unchecked("AGE-PLUGIN-YUBIKEY-"); const USABLE_SLOTS: [RetiredSlotId; 20] = [ RetiredSlotId::R1, @@ -194,7 +195,7 @@ fn generate(flags: PluginFlags) -> Result<(), Error> { fn print_single( serial: Option, slot: RetiredSlotId, - printer: impl Fn(key::Stub, p256::Recipient, util::Metadata), + printer: impl Fn(key::Stub, Recipient, util::Metadata), ) -> Result<(), Error> { let mut yubikey = key::open(serial)?; @@ -216,7 +217,7 @@ fn print_multiple( kind: &str, serial: Option, all: bool, - printer: impl Fn(key::Stub, p256::Recipient, util::Metadata), + printer: impl Fn(key::Stub, Recipient, util::Metadata), ) -> Result<(), Error> { let mut readers = Context::open()?; @@ -256,7 +257,7 @@ fn print_details( kind: &str, flags: PluginFlags, all: bool, - printer: impl Fn(key::Stub, p256::Recipient, util::Metadata), + printer: impl Fn(key::Stub, Recipient, util::Metadata), ) -> Result<(), Error> { if let Some(slot) = flags.slot { print_single(flags.serial, slot, printer) @@ -297,6 +298,12 @@ fn list(flags: PluginFlags, all: bool) -> Result<(), Error> { all, |_, recipient, metadata| { println!("{metadata}"); + if let Some(legacy_recipient) = recipient.legacy_recipient(&metadata) { + println!( + "{}", + fl!("yubikey-legacy-recipient", recipient = legacy_recipient) + ); + } println!("{recipient}"); }, ) @@ -327,11 +334,7 @@ fn main() -> Result<(), Error> { } if let Some(state_machine) = opts.age_plugin { - run_state_machine( - &state_machine, - Some(plugin::RecipientPlugin::default), - Some(plugin::IdentityPlugin::default), - )?; + run_state_machine(&state_machine, plugin::Handler)?; Ok(()) } else if opts.version { println!("age-plugin-yubikey {}", env!("CARGO_PKG_VERSION")); @@ -406,7 +409,7 @@ fn main() -> Result<(), Error> { let (_, cert) = x509_parser::parse_x509_certificate(key.certificate().as_ref()) .unwrap(); - let (name, _) = util::extract_name(&cert, true).unwrap(); + let (name, _) = util::extract_name_and_version(&cert, true).unwrap(); let created = cert .validity() .not_before @@ -616,6 +619,15 @@ fn main() -> Result<(), Error> { Err(e) => return Err(e.into()), }; + let identity = if let Some(legacy_recipient) = recipient.legacy_recipient(&metadata) { + format!( + "{}\n{stub}", + fl!("yubikey-legacy-recipient", recipient = legacy_recipient), + ) + } else { + stub.to_string() + }; + writeln!( file, "{}", @@ -623,7 +635,7 @@ fn main() -> Result<(), Error> { "yubikey-identity", yubikey_metadata = metadata.to_string(), recipient = recipient.to_string(), - identity = stub.to_string(), + identity = identity, ) )?; file.sync_data()?; diff --git a/src/native.rs b/src/native.rs new file mode 100644 index 0000000..f7484a2 --- /dev/null +++ b/src/native.rs @@ -0,0 +1,59 @@ +use std::marker::PhantomData; +use std::rc::Rc; +use std::sync::RwLock; + +use hkdf::Hkdf; +use sha2::Sha256; + +use crate::key::Connection; + +pub(crate) mod p256tag; + +/// Derives a tag for the tagged age recipient formats. +fn stanza_tag(ikm: &[u8], salt: &str) -> [u8; 4] { + let (tag, _) = Hkdf::::extract(Some(salt.as_bytes()), ikm); + tag[..4].try_into().expect("correct length") +} + +/// Pretend that a YubiKey connection is a KEM private key. +struct YubiKeyKemPrivateKey<'a, Kem> { + conn: Rc>, + _kem: PhantomData, +} + +impl<'a, Kem> YubiKeyKemPrivateKey<'a, Kem> { + fn new(conn: &'a mut Connection) -> Self { + Self { + conn: Rc::new(RwLock::new(conn)), + _kem: PhantomData::default(), + } + } +} + +impl<'a, Kem> Clone for YubiKeyKemPrivateKey<'a, Kem> { + fn clone(&self) -> Self { + Self { + conn: self.conn.clone(), + _kem: PhantomData::default(), + } + } +} + +impl<'a, Kem> PartialEq for YubiKeyKemPrivateKey<'a, Kem> { + fn eq(&self, other: &Self) -> bool { + self.conn.read().unwrap().stub() == other.conn.read().unwrap().stub() + } +} +impl<'a, Kem> Eq for YubiKeyKemPrivateKey<'a, Kem> {} + +impl<'a, Kem: hpke::Kem> hpke::Serializable for YubiKeyKemPrivateKey<'a, Kem> { + type OutputSize = ::OutputSize; + fn write_exact(&self, _: &mut [u8]) { + unreachable!("Never called") + } +} +impl<'a, Kem: hpke::Kem> hpke::Deserializable for YubiKeyKemPrivateKey<'a, Kem> { + fn from_bytes(_: &[u8]) -> Result { + unreachable!("Never called") + } +} diff --git a/src/native/p256tag.rs b/src/native/p256tag.rs new file mode 100644 index 0000000..ad94675 --- /dev/null +++ b/src/native/p256tag.rs @@ -0,0 +1,292 @@ +use std::fmt; +use std::marker::PhantomData; + +use age_core::{ + format::{FileKey, Stanza}, + primitives::{bech32_encode_to_fmt, hpke_open, hpke_seal}, + secrecy::{zeroize::Zeroize, ExposeSecret}, +}; +use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine}; +use hpke::{Deserializable, Serializable}; +use p256::{ + elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}, + EncodedPoint, +}; +use rand::rngs::OsRng; +use yubikey::{certificate::PublicKeyInfo, Certificate}; + +use super::{stanza_tag, YubiKeyKemPrivateKey}; +use crate::{ + key::{self, Connection}, + recipient::static_tag, + util::base64_arg, +}; + +pub(crate) const PLUGIN_NAME: &str = "tag"; +const RECIPIENT_PREFIX: bech32::Hrp = bech32::Hrp::parse_unchecked("age1tag"); + +const P256TAG_RECIPIENT_TAG: &str = "p256tag"; +const P256TAG_SALT: &str = "age-encryption.org/p256tag"; + +const TAG_BYTES: usize = 4; +/// Per [RFC 9180 section 7.1.1]: +/// > For P-256, P-384, and P-521, the `SerializePublicKey()` function of the KEM performs +/// > the uncompressed Elliptic-Curve-Point-to-Octet-String conversion according to [SECG]. +/// +/// [RFC 9180 section 7.1.1]: https://www.rfc-editor.org/rfc/rfc9180.html#section-7.1.1 +/// [SECG]: https://secg.org/sec1-v2.pdf +const ENC_BYTES: usize = 65; + +type Kem = hpke::kem::DhP256HkdfSha256; + +/// The non-hybrid tagged age recipient type, designed for hardware keys where decryption +/// potentially requires user presence. +/// +/// With knowledge of the recipient, it is possible to check if a stanza was addressed to +/// a specific recipient before attempting decryption. This offers less privacy than the +/// untagged recipient types. +#[derive(Clone, PartialEq, Eq)] +pub(crate) struct Recipient { + /// Compressed encoding of the recipient public key. + compressed: EncodedPoint, + /// Cached in-memory representation, for HPKE. + pk_recip: ::PublicKey, +} + +impl fmt::Display for Recipient { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + bech32_encode_to_fmt(f, RECIPIENT_PREFIX, self.compressed.as_bytes()) + } +} + +impl fmt::Debug for Recipient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl Recipient { + /// Attempts to parse a valid p256tag recipient from its compressed SEC-1 byte encoding. + pub(crate) fn from_bytes(bytes: &[u8]) -> Option { + let encoded = p256::EncodedPoint::from_bytes(bytes).ok()?; + if !encoded.is_compressed() { + return None; + } + + let point = p256::PublicKey::from_encoded_point(&encoded).into_option()?; + + let pk_recip = + ::PublicKey::from_bytes(point.to_encoded_point(false).as_bytes()) + .expect("valid"); + + Some(Self { + compressed: encoded, + pk_recip, + }) + } + + pub(crate) fn from_certificate(cert: &Certificate) -> Option { + Self::from_spki(cert.subject_pki()) + } + + pub(crate) fn from_spki(spki: &PublicKeyInfo) -> Option { + let encoded = match spki { + PublicKeyInfo::EcP256(pubkey) => Some(pubkey), + _ => None, + }?; + + // Check that the certificate encoding is uncompressed. + let pk_recip = ::PublicKey::from_bytes(encoded.as_bytes()).ok()?; + + let point = p256::PublicKey::from_encoded_point(encoded).into_option()?; + let compressed = point.to_encoded_point(true); + + Some(Self { + compressed, + pk_recip, + }) + } + + /// Returns the compressed SEC-1 encoding of this recipient. + pub(crate) fn to_compressed(&self) -> p256::EncodedPoint { + self.compressed + } + + pub(crate) fn static_tag(&self) -> [u8; TAG_BYTES] { + static_tag(self.compressed.as_bytes()) + } + + pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> RecipientLine { + let (enc, ct) = hpke_seal::( + &self.pk_recip, + P256TAG_SALT.as_bytes(), + file_key.expose_secret(), + &mut OsRng, + ); + + RecipientLine { + tag: tag(&enc, self.static_tag()), + enc, + ct, + } + } +} + +fn tag(enc: &::EncappedKey, static_tag: [u8; TAG_BYTES]) -> [u8; TAG_BYTES] { + let ikm = enc + .to_bytes() + .into_iter() + .chain(static_tag) + .collect::>(); + + stanza_tag(&ikm, P256TAG_SALT) +} + +pub(crate) struct RecipientLine { + tag: [u8; TAG_BYTES], + enc: ::EncappedKey, + ct: Vec, +} + +impl From for Stanza { + fn from(r: RecipientLine) -> Self { + Stanza { + tag: P256TAG_RECIPIENT_TAG.to_owned(), + args: vec![ + BASE64_STANDARD_NO_PAD.encode(r.tag), + BASE64_STANDARD_NO_PAD.encode(r.enc.to_bytes()), + ], + body: r.ct, + } + } +} + +impl RecipientLine { + pub(crate) fn from_stanza(s: Stanza) -> Option> { + if s.tag != P256TAG_RECIPIENT_TAG { + return None; + } + + let (tag, enc) = match &s.args[..] { + [encoded_tag, encoded_enc] => ( + base64_arg(encoded_tag, [0; TAG_BYTES]), + base64_arg(encoded_enc, [0; ENC_BYTES]) + .and_then(|bytes| ::EncappedKey::from_bytes(&bytes[..]).ok()), + ), + _ => (None, None), + }; + + Some(match (tag, enc) { + (Some(tag), Some(epk_bytes)) => Ok(RecipientLine { + tag, + enc: epk_bytes, + ct: s.body, + }), + // Anything else indicates a structurally-invalid stanza. + _ => Err(()), + }) + } + + pub(crate) fn matches_stub(&self, stub: &key::Stub) -> bool { + self.tag == tag(&self.enc, stub.tag) + } + + pub(crate) fn unwrap_file_key(&self, conn: &mut Connection) -> Result { + // > The identity implementation [...] MUST check that the body length is exactly + // > 32 bytes before attempting to decrypt it, to mitigate partitioning oracle + // > attacks. + if self.ct.len() != 32 { + return Err(()); + } + + let sk_recip = YubiKeyKemPrivateKey::new(conn); + + // A failure to decrypt is fatal, because we assume that we won't + // encounter 32-bit collisions on the key tag embedded in the header. + hpke_open::( + &self.enc, + &sk_recip, + P256TAG_SALT.as_bytes(), + &self.ct, + ) + .map_err(|_| ()) + .map(|mut pt| { + FileKey::init_with_mut(|file_key| { + file_key.copy_from_slice(&pt); + pt.zeroize(); + }) + }) + } +} + +/// A decap-only version of [`Kem`] where the private key is stored on a YubiKey. +struct YubiKeyDhP256HkdfSha256<'a>(PhantomData<&'a ()>); + +impl<'a> hpke::Kem for YubiKeyDhP256HkdfSha256<'a> { + type PublicKey = ::PublicKey; + type PrivateKey = YubiKeyKemPrivateKey<'a, Kem>; + + fn sk_to_pk(_: &Self::PrivateKey) -> Self::PublicKey { + unreachable!("Never called") + } + + type EncappedKey = ::EncappedKey; + type NSecret = ::NSecret; + const KEM_ID: u16 = ::KEM_ID; + + fn derive_keypair(_: &[u8]) -> (Self::PrivateKey, Self::PublicKey) { + unreachable!("Never called") + } + + fn decap( + sk_recip: &Self::PrivateKey, + pk_sender_id: Option<&Self::PublicKey>, + encapped_key: &Self::EncappedKey, + ) -> Result, hpke::HpkeError> { + let mut sk_recip = sk_recip.conn.write().unwrap(); + + // Put together the binding context used for all KDF operations + let suite_id = b"KEM\x00\x10"; + + // Compute the shared secret from the ephemeral inputs + let kex_res_eph = sk_recip + .p256_ecdh(&encapped_key.to_bytes()) + .map_err(|_| hpke::HpkeError::DecapError)?; + + // Compute the sender's pubkey from their privkey + let pk_recip = match sk_recip.recipient() { + crate::recipient::Recipient::P256Tag(recipient) => &recipient.pk_recip, + _ => panic!("should have been filtered out earlier"), + }; + + assert!(pk_sender_id.is_none()); + + // kem_context = encapped_key || pk_recip || pk_sender_id + let kem_context = [encapped_key.to_bytes(), pk_recip.to_bytes()] + .into_iter() + .flatten() + .collect::>(); + + // The "unauthed shared secret" is derived from just the KEX of the ephemeral + // input with the recipient pubkey. The HKDF-Expand call only errors if the + // output values are 255x the digest size of the hash function. Since these + // values are fixed at compile time, we don't worry about it. + let mut shared_secret = as Default>::default(); + hpke::kdf::extract_and_expand::( + &kex_res_eph, + suite_id, + &kem_context, + &mut shared_secret.0, + ) + .expect("shared secret is way too big"); + Ok(shared_secret) + } + + fn encap( + _: &Self::PublicKey, + _: Option<(&Self::PrivateKey, &Self::PublicKey)>, + _: &mut R, + ) -> Result<(hpke::kem::SharedSecret, Self::EncappedKey), hpke::HpkeError> { + unreachable!("Never called") + } +} diff --git a/src/format.rs b/src/piv_p256.rs similarity index 62% rename from src/format.rs rename to src/piv_p256.rs index fb9ac4c..26873d4 100644 --- a/src/format.rs +++ b/src/piv_p256.rs @@ -1,7 +1,7 @@ use age_core::{ - format::{FileKey, Stanza}, - primitives::aead_encrypt, - secrecy::ExposeSecret, + format::{FileKey, Stanza, FILE_KEY_BYTES}, + primitives::{aead_decrypt, aead_encrypt, hkdf}, + secrecy::{zeroize::Zeroize, ExposeSecret}, }; use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine}; use p256::{ @@ -11,11 +11,14 @@ use p256::{ use rand::rngs::OsRng; use sha2::Sha256; -use crate::{p256::Recipient, STANZA_TAG}; +use crate::{key::Connection, recipient::TAG_BYTES, util::base64_arg}; +mod recipient; +pub(crate) use recipient::Recipient; + +const STANZA_TAG: &str = "piv-p256"; pub(crate) const STANZA_KEY_LABEL: &[u8] = b"piv-p256"; -const TAG_BYTES: usize = 4; const EPK_BYTES: usize = 33; const ENCRYPTED_FILE_KEY_BYTES: usize = 32; @@ -80,17 +83,6 @@ impl RecipientLine { return None; } - fn base64_arg, B: AsMut<[u8]>>(arg: &A, mut buf: B) -> Option { - if arg.as_ref().len() != ((4 * buf.as_mut().len()) + 2) / 3 { - return None; - } - - BASE64_STANDARD_NO_PAD - .decode_slice_unchecked(arg, buf.as_mut()) - .ok() - .and_then(|len| (len == buf.as_mut().len()).then_some(buf)) - } - let (tag, epk_bytes) = match &s.args[..] { [tag, epk_bytes] => ( base64_arg(tag, [0; TAG_BYTES]), @@ -109,17 +101,17 @@ impl RecipientLine { _ => Err(()), }) } +} - pub(crate) fn wrap_file_key(file_key: &FileKey, pk: &Recipient) -> Self { +impl Recipient { + pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> RecipientLine { let esk = EphemeralSecret::random(&mut OsRng); let epk = esk.public_key(); let epk_bytes = EphemeralKeyBytes::from_public_key(&epk); - let shared_secret = esk.diffie_hellman(pk.public_key()); + let shared_secret = esk.diffie_hellman(self.public_key()); - let mut salt = vec![]; - salt.extend_from_slice(epk_bytes.as_bytes()); - salt.extend_from_slice(pk.to_encoded().as_bytes()); + let salt = salt(&epk_bytes, self.to_encoded()); let enc_key = { let mut okm = [0; 32]; @@ -137,9 +129,50 @@ impl RecipientLine { }; RecipientLine { - tag: pk.tag(), + tag: self.tag(), epk_bytes, encrypted_file_key, } } } + +impl RecipientLine { + pub(crate) fn unwrap_file_key(&self, conn: &mut Connection) -> Result { + let (static_tag, pk) = match conn.recipient() { + crate::recipient::Recipient::PivP256(recipient) => { + (recipient.tag(), recipient.to_encoded()) + } + crate::recipient::Recipient::P256Tag(recipient) => { + (recipient.static_tag(), recipient.to_compressed()) + } + }; + assert_eq!(self.tag, static_tag); + + let salt = salt(&self.epk_bytes, pk); + + // The YubiKey API for performing scalar multiplication takes the point in its + // uncompressed SEC-1 encoding. + let shared_secret = conn.p256_ecdh(self.epk_bytes.decompress().as_bytes())?; + + let enc_key = hkdf(&salt, STANZA_KEY_LABEL, shared_secret.as_ref()); + + // A failure to decrypt is fatal, because we assume that we won't + // encounter 32-bit collisions on the key tag embedded in the header. + aead_decrypt(&enc_key, FILE_KEY_BYTES, &self.encrypted_file_key) + .map_err(|_| ()) + .map(|mut pt| { + FileKey::init_with_mut(|file_key| { + file_key.copy_from_slice(&pt); + pt.zeroize(); + }) + }) + } +} + +fn salt(epk_bytes: &EphemeralKeyBytes, pk: p256::EncodedPoint) -> Vec { + assert!(pk.is_compressed()); + let mut salt = vec![]; + salt.extend_from_slice(epk_bytes.as_bytes()); + salt.extend_from_slice(pk.as_bytes()); + salt +} diff --git a/src/p256.rs b/src/piv_p256/recipient.rs similarity index 63% rename from src/p256.rs rename to src/piv_p256/recipient.rs index 1f4c607..0b41efe 100644 --- a/src/p256.rs +++ b/src/piv_p256/recipient.rs @@ -1,13 +1,11 @@ -use bech32::{ToBase32, Variant}; +use age_core::primitives::bech32_encode_to_fmt; use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; -use sha2::{Digest, Sha256}; -use yubikey::{certificate::PublicKeyInfo, Certificate}; use std::fmt; -use crate::RECIPIENT_PREFIX; +use crate::recipient::{static_tag, TAG_BYTES}; -pub(crate) const TAG_BYTES: usize = 4; +const RECIPIENT_PREFIX: bech32::Hrp = bech32::Hrp::parse_unchecked("age1yubikey"); /// Wrapper around a compressed secp256r1 curve point. #[derive(Clone)] @@ -21,15 +19,7 @@ impl fmt::Debug for Recipient { impl fmt::Display for Recipient { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str( - bech32::encode( - RECIPIENT_PREFIX, - self.to_encoded().as_bytes().to_base32(), - Variant::Bech32, - ) - .expect("HRP is valid") - .as_str(), - ) + bech32_encode_to_fmt(f, RECIPIENT_PREFIX, self.to_encoded().as_bytes()) } } @@ -44,17 +34,6 @@ impl Recipient { } } - pub(crate) fn from_certificate(cert: &Certificate) -> Option { - Self::from_spki(cert.subject_pki()) - } - - pub(crate) fn from_spki(spki: &PublicKeyInfo) -> Option { - match spki { - PublicKeyInfo::EcP256(pubkey) => Self::from_encoded(pubkey), - _ => None, - } - } - /// Attempts to parse a valid YubiKey recipient from its SEC-1 encoding. /// /// This accepts both compressed (as used by the plugin) and uncompressed (as used in @@ -69,8 +48,7 @@ impl Recipient { } pub(crate) fn tag(&self) -> [u8; TAG_BYTES] { - let tag = Sha256::digest(self.to_encoded().as_bytes()); - (&tag[0..TAG_BYTES]).try_into().expect("length is correct") + static_tag(self.to_encoded().as_bytes()) } /// Exposes the wrapped public key. diff --git a/src/plugin.rs b/src/plugin.rs index c0cd18b..910a554 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -2,12 +2,27 @@ use age_core::format::{FileKey, Stanza}; use age_plugin::{ identity::{self, IdentityPluginV1}, recipient::{self, RecipientPluginV1}, - Callbacks, + Callbacks, PluginHandler, }; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::io; -use crate::{fl, format, key, p256::Recipient, PLUGIN_NAME}; +use crate::{fl, key, native::p256tag, piv_p256, Recipient, PLUGIN_NAME}; + +pub(crate) struct Handler; + +impl PluginHandler for Handler { + type RecipientV1 = RecipientPlugin; + type IdentityV1 = IdentityPlugin; + + fn recipient_v1(self) -> io::Result { + Ok(RecipientPlugin::default()) + } + + fn identity_v1(self) -> io::Result { + Ok(IdentityPlugin::default()) + } +} #[derive(Debug, Default)] pub(crate) struct RecipientPlugin { @@ -22,11 +37,7 @@ impl RecipientPluginV1 for RecipientPlugin { plugin_name: &str, bytes: &[u8], ) -> Result<(), recipient::Error> { - if let Some(pk) = if plugin_name == PLUGIN_NAME { - Recipient::from_bytes(bytes) - } else { - None - } { + if let Some(pk) = Recipient::from_bytes(plugin_name, bytes) { self.recipients.push(pk); Ok(()) } else { @@ -58,6 +69,10 @@ impl RecipientPluginV1 for RecipientPlugin { } } + fn labels(&mut self) -> HashSet { + HashSet::new() + } + fn wrap_file_keys( &mut self, file_keys: Vec, @@ -95,7 +110,7 @@ impl RecipientPluginV1 for RecipientPlugin { self.recipients .iter() .chain(yk_recipients.iter()) - .map(|pk| format::RecipientLine::wrap_file_key(&file_key, pk).into()) + .map(|pk| pk.wrap_file_key(&file_key)) .collect() }) .collect()) @@ -140,16 +155,16 @@ impl IdentityPluginV1 for IdentityPlugin { let mut file_keys = HashMap::with_capacity(files.len()); // Filter to files / stanzas for which we have matching YubiKeys - let mut candidate_stanzas: Vec<(&key::Stub, HashMap>)> = - self.yubikeys - .iter() - .map(|stub| (stub, HashMap::new())) - .collect(); + let mut candidate_stanzas: Vec<(&key::Stub, HashMap>)> = self + .yubikeys + .iter() + .map(|stub| (stub, HashMap::new())) + .collect(); - for (file, stanzas) in files.iter().enumerate() { - for (stanza_index, stanza) in stanzas.iter().enumerate() { + for (file, stanzas) in files.into_iter().enumerate() { + for (stanza_index, stanza) in stanzas.into_iter().enumerate() { match ( - format::RecipientLine::from_stanza(stanza).map(|res| { + SupportedStanza::parse(stanza).map(|res| { res.map_err(|_| identity::Error::Stanza { file_index: file, stanza_index, @@ -163,7 +178,7 @@ impl IdentityPluginV1 for IdentityPlugin { // A line will match at most one YubiKey. if let Some(files) = candidate_stanzas.iter_mut().find_map(|(stub, files)| { - if stub.matches(&line) { + if line.matches_stub(stub) { Some(files) } else { None @@ -233,7 +248,7 @@ impl IdentityPluginV1 for IdentityPlugin { } for (stanza_index, line) in stanzas.iter().enumerate() { - match conn.unwrap_file_key(line) { + match line.unwrap_file_key(&mut conn) { Ok(file_key) => { // We've managed to decrypt this file! file_keys.entry(file_index).or_insert(Ok(file_key)); @@ -255,3 +270,32 @@ impl IdentityPluginV1 for IdentityPlugin { Ok(file_keys) } } + +enum SupportedStanza { + PivP256(piv_p256::RecipientLine), + P256Tag(p256tag::RecipientLine), +} + +impl SupportedStanza { + fn parse(stanza: Stanza) -> Option> { + piv_p256::RecipientLine::from_stanza(&stanza) + .map(|res| res.map(Self::PivP256)) + .or_else(|| { + p256tag::RecipientLine::from_stanza(stanza).map(|res| res.map(Self::P256Tag)) + }) + } + + pub(crate) fn matches_stub(&self, stub: &key::Stub) -> bool { + match self { + SupportedStanza::PivP256(line) => stub.tag == line.tag, + SupportedStanza::P256Tag(line) => line.matches_stub(stub), + } + } + + pub(crate) fn unwrap_file_key(&self, conn: &mut key::Connection) -> Result { + match self { + SupportedStanza::PivP256(line) => line.unwrap_file_key(conn), + SupportedStanza::P256Tag(line) => line.unwrap_file_key(conn), + } + } +} diff --git a/src/recipient.rs b/src/recipient.rs new file mode 100644 index 0000000..2333ddc --- /dev/null +++ b/src/recipient.rs @@ -0,0 +1,70 @@ +use std::fmt; + +use age_core::format::{FileKey, Stanza}; +use sha2::{Digest, Sha256}; + +use crate::{native::p256tag, piv_p256, util::Metadata, PLUGIN_NAME}; + +pub(crate) const TAG_BYTES: usize = 4; + +#[derive(Clone, Debug)] +pub(crate) enum Recipient { + PivP256(piv_p256::Recipient), + P256Tag(p256tag::Recipient), +} + +impl fmt::Display for Recipient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Recipient::PivP256(recipient) => recipient.fmt(f), + Recipient::P256Tag(recipient) => recipient.fmt(f), + } + } +} + +impl Recipient { + /// Attempts to parse a supported YubiKey recipient. + pub(crate) fn from_bytes(plugin_name: &str, bytes: &[u8]) -> Option { + match plugin_name { + PLUGIN_NAME => piv_p256::Recipient::from_bytes(bytes).map(Self::PivP256), + p256tag::PLUGIN_NAME => p256tag::Recipient::from_bytes(bytes).map(Self::P256Tag), + _ => None, + } + } + + /// Helper for returning the legacy encoding of this recipient, if any. + pub(crate) fn legacy_recipient(&self, metadata: &Metadata) -> Option { + metadata + .is_pre_p256tag() + .then(|| match self { + Recipient::P256Tag(recipient) => Some( + piv_p256::Recipient::from_bytes(recipient.to_compressed().as_bytes()) + .expect("valid") + .to_string(), + ), + _ => None, + }) + .flatten() + } + + /// Returns the static tag for this recipient. + pub(crate) fn static_tag(&self) -> [u8; TAG_BYTES] { + match self { + Recipient::PivP256(recipient) => recipient.tag(), + Recipient::P256Tag(recipient) => recipient.static_tag(), + } + } + + pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> Stanza { + match self { + Recipient::PivP256(recipient) => recipient.wrap_file_key(file_key).into(), + Recipient::P256Tag(recipient) => recipient.wrap_file_key(file_key).into(), + } + } +} + +pub(crate) fn static_tag(pk: &[u8]) -> [u8; TAG_BYTES] { + Sha256::digest(pk)[0..TAG_BYTES] + .try_into() + .expect("length is correct") +} diff --git a/src/util.rs b/src/util.rs index b44d221..b723ce9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,7 @@ use std::fmt; use std::iter; +use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine}; use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid}; use yubikey::{ piv::{RetiredSlotId, SlotId}, @@ -8,7 +9,7 @@ use yubikey::{ }; use crate::fl; -use crate::{error::Error, key::Stub, p256::Recipient, BINARY_NAME, USABLE_SLOTS}; +use crate::{error::Error, key::Stub, Recipient, BINARY_NAME, USABLE_SLOTS}; pub(crate) const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8]; @@ -70,7 +71,10 @@ pub(crate) fn otp_serial_prefix(serial: Serial) -> String { .collect() } -pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String, bool)> { +pub(crate) fn extract_name_and_version( + cert: &X509Certificate, + all: bool, +) -> Option<(String, Option)> { // Look at Subject Organization to determine if we created this. match cert.subject().iter_organization().next() { Some(org) if org.as_str() == Ok(BINARY_NAME) => { @@ -83,7 +87,16 @@ pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String, .map(|s| s.to_owned()) .unwrap_or_default(); // TODO: This should always be present. - Some((name, true)) + // We store the binary version as an Organizational Unit attribute. + let version = cert + .subject() + .iter_organizational_unit() + .next() + .and_then(|cn| cn.as_str().ok()) + .map(|s| s.to_owned()) + .unwrap_or_default(); // TODO: This should always be present. + + Some((name, Some(version))) } _ => { // Not one of ours, but we've already filtered for compatibility. @@ -94,7 +107,7 @@ pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String, // Display the entire subject. let name = cert.subject().to_string(); - Some((name, false)) + Some((name, None)) } } } @@ -103,6 +116,7 @@ pub(crate) struct Metadata { serial: Serial, slot: RetiredSlotId, name: String, + version: Option, created: String, pub(crate) pin_policy: Option, pub(crate) touch_policy: Option, @@ -148,31 +162,29 @@ impl Metadata { .unwrap_or((None, None)) }; - extract_name(&cert, all) - .map(|(name, ours)| { - if ours { - let (pin_policy, touch_policy) = policies(&cert); - (name, pin_policy, touch_policy) + extract_name_and_version(&cert, all) + .map(|(name, version)| { + let (pin_policy, touch_policy) = if version.is_some() { + policies(&cert) } else { // We can extract the PIN and touch policies via an attestation. This // is slow, but the user has asked for all compatible keys, so... - let (pin_policy, touch_policy) = - yubikey::piv::attest(yubikey, SlotId::Retired(slot)) - .ok() - .and_then(|buf| { - x509_parser::parse_x509_certificate(&buf) - .map(|(_, c)| policies(&c)) - .ok() - }) - .unwrap_or((None, None)); - - (name, pin_policy, touch_policy) - } + yubikey::piv::attest(yubikey, SlotId::Retired(slot)) + .ok() + .and_then(|buf| { + x509_parser::parse_x509_certificate(&buf) + .map(|(_, c)| policies(&c)) + .ok() + }) + .unwrap_or((None, None)) + }; + (name, version, pin_policy, touch_policy) }) - .map(|(name, pin_policy, touch_policy)| Metadata { + .map(|(name, version, pin_policy, touch_policy)| Metadata { serial: yubikey.serial(), slot, name, + version, created: cert .validity() .not_before @@ -182,6 +194,19 @@ impl Metadata { touch_policy, }) } + + /// Returns `true` if this identity was generated with an `age-plugin-yubikey` version + /// before `p256tag` was added (and became the default). + pub(crate) fn is_pre_p256tag(&self) -> bool { + self.version + .as_ref() + .and_then(|version| version.split_once('.')) + .and_then(|(major, rest)| rest.split_once('.').map(|(minor, _)| (major, minor))) + .is_some_and(|(major, minor)| { + // `p256tag` added in v0.6.0 + major == "0" && minor.parse::().is_ok_and(|minor| minor < 6) + }) + } } impl fmt::Display for Metadata { @@ -203,19 +228,40 @@ impl fmt::Display for Metadata { } pub(crate) fn print_identity(stub: Stub, recipient: Recipient, metadata: Metadata) { + let legacy_recipient = recipient.legacy_recipient(&metadata); let recipient = recipient.to_string(); if !console::user_attended() { let recipient = recipient.as_str(); eprintln!("{}", fl!("print-recipient", recipient = recipient)); } + let identity = if let Some(legacy_recipient) = legacy_recipient { + format!( + "{}\n{stub}", + fl!("yubikey-legacy-recipient", recipient = legacy_recipient), + ) + } else { + stub.to_string() + }; + println!( "{}", fl!( "yubikey-identity", yubikey_metadata = metadata.to_string(), recipient = recipient, - identity = stub.to_string(), + identity = identity, ) ); } + +pub(crate) fn base64_arg, B: AsMut<[u8]>>(arg: &A, mut buf: B) -> Option { + if arg.as_ref().len() != ((4 * buf.as_mut().len()) + 2) / 3 { + return None; + } + + BASE64_STANDARD_NO_PAD + .decode_slice_unchecked(arg, buf.as_mut()) + .ok() + .and_then(|len| (len == buf.as_mut().len()).then_some(buf)) +}