difftreelog
chore(deps) update to `age` version `0.11` (#9)
in: trunk
* chore(deps): update to `age` version `0.11` * chore(deps): update the remaining dependencies * chore: simplify `encrypt_secret_data` bounds * chore: simplify `encrypt_secret_data` bounds even more
9 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -63,9 +63,9 @@
[[package]]
name = "age"
-version = "0.10.0"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edeef7d7b199195a2d7d7a8155d2d04aee736e60c5c7bdd7097d115369a8817d"
+checksum = "2020562e68d7a02c2743707b262c62484b340a296924a5e4146d5a0a96ca8103"
dependencies = [
"aes",
"aes-gcm",
@@ -98,9 +98,9 @@
[[package]]
name = "age-core"
-version = "0.10.0"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5f11899bc2bbddd135edbc30c36b1924fa59d0746bb45beb5933fafe3fe509b"
+checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99"
dependencies = [
"base64 0.21.7",
"chacha20poly1305",
@@ -261,9 +261,9 @@
[[package]]
name = "axum"
-version = "0.7.7"
+version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core",
@@ -318,7 +318,7 @@
"miniz_oxide",
"object",
"rustc-demangle",
- "windows-targets 0.52.6",
+ "windows-targets",
]
[[package]]
@@ -480,9 +480,9 @@
[[package]]
name = "cc"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8"
+checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
dependencies = [
"shlex",
]
@@ -544,7 +544,7 @@
"num-traits",
"serde",
"wasm-bindgen",
- "windows-targets 0.52.6",
+ "windows-targets",
]
[[package]]
@@ -571,9 +571,9 @@
[[package]]
name = "clap"
-version = "4.5.20"
+version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
+checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
dependencies = [
"clap_builder",
"clap_derive",
@@ -581,14 +581,14 @@
[[package]]
name = "clap_builder"
-version = "4.5.20"
+version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
+checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
- "strsim 0.11.1",
+ "strsim",
"terminal_size",
"unicase",
"unicode-width 0.2.0",
@@ -596,9 +596,9 @@
[[package]]
name = "clap_complete"
-version = "4.5.37"
+version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11611dca53440593f38e6b25ec629de50b14cdfa63adc0fb856115a2c6d97595"
+checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01"
dependencies = [
"clap",
]
@@ -617,9 +617,9 @@
[[package]]
name = "clap_lex"
-version = "0.7.2"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
+checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
[[package]]
name = "colorchoice"
@@ -636,7 +636,7 @@
"encode_unicode",
"lazy_static",
"libc",
- "unicode-width 0.1.14",
+ "unicode-width 0.1.11",
"windows-sys 0.52.0",
]
@@ -677,17 +677,23 @@
]
[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
+[[package]]
name = "crossterm"
-version = "0.27.0"
+version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags",
"crossterm_winapi",
"filedescriptor",
- "libc",
- "mio 0.8.11",
+ "mio",
"parking_lot",
+ "rustix",
"signal-hook",
"signal-hook-mio",
"winapi",
@@ -751,11 +757,12 @@
[[package]]
name = "dashmap"
-version = "5.5.3"
+version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
+ "crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
@@ -921,7 +928,7 @@
"nix-eval",
"nixlike",
"nom",
- "openssh 0.10.5",
+ "openssh",
"owo-colors",
"peg",
"regex",
@@ -954,7 +961,7 @@
"nix-eval",
"nixlike",
"nom",
- "openssh 0.11.3",
+ "openssh",
"serde",
"serde_json",
"tempfile",
@@ -1418,9 +1425,9 @@
[[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",
@@ -1439,9 +1446,9 @@
[[package]]
name = "i18n-embed-fl"
-version = "0.7.0"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fc1f8715195dffc4caddcf1cf3128da15fe5d8a137606ea8856c9300047d5a2"
+checksum = "f6e9571c3cba9eba538eaa5ee40031b26debe76f0c7e17bafc97ea57a76cd82e"
dependencies = [
"dashmap",
"find-crate",
@@ -1450,10 +1457,10 @@
"i18n-config",
"i18n-embed",
"lazy_static",
- "proc-macro-error",
+ "proc-macro-error2",
"proc-macro2",
"quote",
- "strsim 0.10.0",
+ "strsim",
"syn 2.0.87",
"unic-langid",
]
@@ -1636,9 +1643,9 @@
[[package]]
name = "libc"
-version = "0.2.162"
+version = "0.2.164"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
+checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
[[package]]
name = "libloading"
@@ -1647,7 +1654,7 @@
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
- "windows-targets 0.52.6",
+ "windows-targets",
]
[[package]]
@@ -1752,18 +1759,6 @@
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
-]
-
-[[package]]
-name = "mio"
-version = "0.8.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
-dependencies = [
- "libc",
- "log",
- "wasi",
- "windows-sys 0.48.0",
]
[[package]]
@@ -1774,6 +1769,7 @@
dependencies = [
"hermit-abi 0.3.9",
"libc",
+ "log",
"wasi",
"windows-sys 0.52.0",
]
@@ -1956,21 +1952,6 @@
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
-
-[[package]]
-name = "openssh"
-version = "0.10.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "330f4b61092456dc0aaa0cf9a205d956cae07d8127a69ffeff6760a72549c77f"
-dependencies = [
- "libc",
- "once_cell",
- "shell-escape",
- "tempfile",
- "thiserror 1.0.69",
- "tokio",
- "tokio-pipe",
-]
[[package]]
name = "openssh"
@@ -2004,13 +1985,13 @@
[[package]]
name = "papergrid"
-version = "0.11.0"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ad43c07024ef767f9160710b3a6773976194758c7919b17e63b863db0bdf7fb"
+checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725"
dependencies = [
"bytecount",
"fnv",
- "unicode-width 0.1.14",
+ "unicode-width 0.1.11",
]
[[package]]
@@ -2033,7 +2014,7 @@
"libc",
"redox_syscall",
"smallvec",
- "windows-targets 0.52.6",
+ "windows-targets",
]
[[package]]
@@ -2243,6 +2224,28 @@
]
[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+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.87",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2583,9 +2586,9 @@
[[package]]
name = "rustls"
-version = "0.23.16"
+version = "0.23.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
+checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e"
dependencies = [
"log",
"once_cell",
@@ -2680,9 +2683,9 @@
[[package]]
name = "secrecy"
-version = "0.8.0"
+version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
+checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"zeroize",
]
@@ -2748,9 +2751,9 @@
[[package]]
name = "serde_json"
-version = "1.0.132"
+version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
+checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
@@ -2807,7 +2810,7 @@
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
- "mio 0.8.11",
+ "mio",
"signal-hook",
]
@@ -2882,12 +2885,6 @@
[[package]]
name = "strsim"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
-
-[[package]]
-name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
@@ -2953,20 +2950,19 @@
[[package]]
name = "tabled"
-version = "0.15.0"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c998b0c8b921495196a48aabaf1901ff28be0760136e31604f7967b0792050e"
+checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b"
dependencies = [
"papergrid",
"tabled_derive",
- "unicode-width 0.1.14",
]
[[package]]
name = "tabled_derive"
-version = "0.7.0"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17"
+checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069"
dependencies = [
"heck 0.4.1",
"proc-macro-error",
@@ -3141,7 +3137,7 @@
"backtrace",
"bytes",
"libc",
- "mio 1.0.2",
+ "mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@@ -3161,16 +3157,6 @@
]
[[package]]
-name = "tokio-pipe"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f213a84bffbd61b8fa0ba8a044b4bbe35d471d0b518867181e82bd5c15542784"
-dependencies = [
- "libc",
- "tokio",
-]
-
-[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3464,9 +3450,9 @@
[[package]]
name = "unicode-width"
-version = "0.1.14"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "unicode-width"
@@ -3528,7 +3514,7 @@
dependencies = [
"itoa",
"log",
- "unicode-width 0.1.14",
+ "unicode-width 0.1.11",
"vte",
]
@@ -3693,7 +3679,7 @@
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core",
- "windows-targets 0.52.6",
+ "windows-targets",
]
[[package]]
@@ -3702,25 +3688,16 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
- "windows-targets 0.52.6",
+ "windows-targets",
]
[[package]]
name = "windows-sys"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
-dependencies = [
- "windows-targets 0.48.5",
-]
-
-[[package]]
-name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "windows-targets 0.52.6",
+ "windows-targets",
]
[[package]]
@@ -3729,22 +3706,7 @@
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"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
-dependencies = [
- "windows_aarch64_gnullvm 0.48.5",
- "windows_aarch64_msvc 0.48.5",
- "windows_i686_gnu 0.48.5",
- "windows_i686_msvc 0.48.5",
- "windows_x86_64_gnu 0.48.5",
- "windows_x86_64_gnullvm 0.48.5",
- "windows_x86_64_msvc 0.48.5",
+ "windows-targets",
]
[[package]]
@@ -3753,33 +3715,21 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
- "windows_aarch64_gnullvm 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
"windows_i686_gnullvm",
- "windows_i686_msvc 0.52.6",
- "windows_x86_64_gnu 0.52.6",
- "windows_x86_64_gnullvm 0.52.6",
- "windows_x86_64_msvc 0.52.6",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
@@ -3789,12 +3739,6 @@
[[package]]
name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
-
-[[package]]
-name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
@@ -3804,24 +3748,12 @@
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
@@ -3831,21 +3763,9 @@
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,7 @@
tokio-util = { version = "0.7.11", features = ["codec"] }
clap = { version = "4.5", features = ["derive", "env", "wrap_help", "unicode"] }
clap_complete = "4.5"
-age = { version = "0.10", features = ["ssh"] }
+age = { version = "0.11", features = ["ssh"] }
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
cmds/fleet/Cargo.tomldiffbeforeafterboth--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -20,7 +20,7 @@
tempfile.workspace = true
time = { version = "0.3", features = ["serde"] }
hostname = "0.4.0"
-age-core = "0.10"
+age-core = "0.11"
peg = "0.8"
base64 = "0.22.1"
chrono = { version = "0.4", features = ["serde"] }
@@ -29,15 +29,15 @@
futures = "0.3"
itertools = "0.13"
shlex = "1.3"
-tabled = { version = "0.15" }
+tabled = { version = "0.16" }
owo-colors = { version = "4.0", features = [
"supports-color",
"supports-colors",
] }
abort-on-drop = "0.2"
regex = "1.10"
-openssh = "0.10"
-crossterm = { version = "0.27.0", features = ["use-dev-tty"] }
+openssh = "0.11"
+crossterm = { version = "0.28.0", features = ["use-dev-tty"] }
fleet-shared.workspace = true
tracing-indicatif = { version = "0.3", optional = true }
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth1use std::{2 collections::{BTreeMap, BTreeSet, HashSet},3 io::{self, stdin, stdout, Read, Write},4 path::PathBuf,5};67use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::Parser;10use fleet_base::{11 fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},12 host::Config,13 opts::FleetOpts,14};15use fleet_shared::SecretData;16use nix_eval::{nix_go, nix_go_json, NixBuildBatch, Value};17use owo_colors::OwoColorize;18use serde::Deserialize;19use tabled::{Table, Tabled};20use tokio::fs::read;21use tracing::{error, info, info_span, warn, Instrument};2223#[derive(Parser)]24pub enum Secret {25 /// Force load host keys for all defined hosts26 ForceKeys,27 /// Add secret, data should be provided in stdin28 AddShared {29 /// Secret name30 name: String,31 /// Secret owners32 #[clap(long, short)]33 machines: Vec<String>,34 /// Override secret if already present35 #[clap(long)]36 force: bool,37 /// Secret public part38 #[clap(long)]39 public: Option<String>,40 /// Load public part from specified file41 #[clap(long)]42 public_file: Option<PathBuf>,4344 /// Create a notification on secret expiration45 #[clap(long)]46 expires_at: Option<DateTime<Utc>>,4748 /// Secret with this name already exists, override its value while keeping the same owners.49 #[clap(long)]50 re_add: bool,5152 /// How to name public secret part53 #[clap(long, short = 'p', default_value = "public")]54 public_part: String,55 /// How to name private secret part56 #[clap(short = 's', long, default_value = "secret")]57 part: String,58 },59 /// Add secret, data should be provided in stdin60 Add {61 /// Secret name62 name: String,63 /// Secret owner64 #[clap(short = 'm', long)]65 machine: String,66 /// Replace secret if already present67 #[clap(long)]68 replace: bool,69 /// Add new parts to existing secret70 #[clap(long)]71 merge: bool,72 /// Secret public part73 #[clap(long)]74 public: Option<String>,75 /// Load public part from specified file76 #[clap(long)]77 public_file: Option<PathBuf>,7879 /// How to name public secret part80 #[clap(short = 'p', long, default_value = "public")]81 public_part: String,82 /// How to name private secret part83 #[clap(short = 's', long, default_value = "secret")]84 part: String,85 },86 /// Read secret from remote host, requires sudo on said host87 Read {88 name: String,89 #[clap(short = 'm', long)]90 machine: String,9192 /// Which private secret part to read93 #[clap(short = 'p', long, default_value = "secret")]94 part: String,95 },96 UpdateShared {97 name: String,9899 #[clap(short = 'm', long)]100 machine: Option<Vec<String>>,101102 #[clap(long)]103 add_machine: Vec<String>,104 #[clap(long)]105 remove_machine: Vec<String>,106107 /// Which host should we use to decrypt108 #[clap(long)]109 prefer_identities: Vec<String>,110 },111 Regenerate {112 /// Which host should we use to decrypt, in case if reencryption is required, without113 /// regeneration114 #[clap(long)]115 prefer_identities: Vec<String>,116 },117 List {},118 Edit {119 name: String,120 #[clap(short = 'm', long)]121 machine: String,122123 #[clap(long)]124 add: bool,125126 /// Which private secret part to read127 #[clap(short = 'p', long, default_value = "secret")]128 part: String,129 },130}131132#[tracing::instrument(skip(config, secret, field, prefer_identities, batch))]133async fn update_owner_set(134 secret_name: &str,135 config: &Config,136 mut secret: FleetSharedSecret,137 field: Value,138 updated_set: &[String],139 prefer_identities: &[String],140 batch: Option<NixBuildBatch>,141) -> Result<FleetSharedSecret> {142 let original_set = secret.owners.clone();143144 let set = original_set.iter().collect::<BTreeSet<_>>();145 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();146147 if set == expected_set {148 info!("no need to update owner list, it is already correct");149 return Ok(secret);150 }151152 let should_regenerate = if set.difference(&expected_set).next().is_some() {153 // TODO: Remove this warning for revokable secrets.154 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");155 nix_go_json!(field.regenerateOnOwnerRemoved)156 } else if expected_set.difference(&set).next().is_some() {157 nix_go_json!(field.regenerateOnOwnerAdded)158 } else {159 false160 };161162 if should_regenerate {163 info!("secret is owner-dependent, will regenerate");164 let generated = generate_shared(config, secret_name, field, updated_set.to_vec(), batch).await?;165 Ok(generated)166 } else {167 drop(batch);168 let identity_holder = if !prefer_identities.is_empty() {169 prefer_identities170 .iter()171 .find(|i| original_set.iter().any(|s| s == *i))172 } else {173 secret.owners.first()174 };175 let Some(identity_holder) = identity_holder else {176 bail!("no available holder found");177 };178179 for (part_name, part) in secret.secret.parts.iter_mut() {180 let _span = info_span!("part reencryption", part_name);181 if !part.raw.encrypted {182 continue;183 }184 let host = config.host(identity_holder).await?;185 let encrypted = host186 .reencrypt(part.raw.clone(), updated_set.to_vec())187 .await?;188 part.raw = encrypted;189 }190191 secret.owners = updated_set.to_vec();192 Ok(secret)193 }194}195196#[derive(Deserialize)]197#[serde(rename_all = "camelCase")]198enum GeneratorKind {199 Impure,200 Pure,201}202203async fn generate_pure(204 _config: &Config,205 _display_name: &str,206 _secret: Value,207 _default_generator: Value,208 _owners: &[String],209) -> Result<FleetSecret> {210 bail!("pure generators are broken for now")211}212async fn generate_impure(213 config: &Config,214 _display_name: &str,215 secret: Value,216 default_generator: Value,217 owners: &[String],218 batch: Option<NixBuildBatch>,219) -> Result<FleetSecret> {220 let generator = nix_go!(secret.generator);221 let on: Option<String> = nix_go_json!(default_generator.impureOn);222223 let host = if let Some(on) = &on {224 config.host(on).await?225 } else {226 config.local_host()227 };228 let on_pkgs = host.pkgs().await?;229 let call_package = nix_go!(on_pkgs.callPackage);230 let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);231232 let mut recipients = Vec::new();233 for owner in owners {234 let key = config.key(owner).await?;235 recipients.push(key);236 }237 let generators = nix_go!(mk_secret_generators(Obj { recipients }));238239 let generator = nix_go!(call_package(generator)(generators));240241 let generator = generator.build_maybe_batch(batch).await?;242 let generator = generator243 .get("out")244 .ok_or_else(|| anyhow!("missing generateImpure out"))?;245 let generator = host.remote_derivation(generator).await?;246247 let out_parent = host.mktemp_dir().await?;248 let out = format!("{out_parent}/out");249250 let mut gen = host.cmd(generator).await?;251 gen.env("out", &out);252 if on.is_none() {253 // This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.254 let project_path: String = config255 .directory256 .clone()257 .into_os_string()258 .into_string()259 .map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;260 gen.env("FLEET_PROJECT", project_path);261 }262 gen.run().await.context("impure generator")?;263264 {265 let marker = host.read_file_text(format!("{out}/marker")).await?;266 ensure!(marker == "SUCCESS", "generation not succeeded");267 }268269 let mut parts = BTreeMap::new();270 for part in host.read_dir(&out).await? {271 if part == "created_at" || part == "expired_at" || part == "marker" {272 continue;273 }274 let contents: SecretData = host275 .read_file_text(format!("{out}/{part}"))276 .await?277 .parse()278 .map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;279 parts.insert(part.to_owned(), FleetSecretPart { raw: contents });280 }281282 let created_at = host.read_file_value(format!("{out}/created_at")).await?;283 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();284285 Ok(FleetSecret {286 created_at,287 expires_at,288 parts,289 })290}291async fn generate(292 config: &Config,293 display_name: &str,294 secret: Value,295 owners: &[String],296 batch: Option<NixBuildBatch>,297) -> Result<FleetSecret> {298 let generator = nix_go!(secret.generator);299 // Can't properly check on nix module system level300 {301 let gen_ty = generator.type_of().await?;302 if gen_ty == "null" {303 bail!("secret has no generator defined, can't automatically generate it.");304 }305 if gen_ty != "lambda" {306 bail!("generator should be lambda, got {gen_ty}");307 }308 }309 let default_pkgs = &config.default_pkgs;310 let default_call_package = nix_go!(default_pkgs.callPackage);311 let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);312 // Generators provide additional information in passthru, to access313 // passthru we should call generator, but information about where this generator is supposed to build314 // is located in passthru... Thus evaluating generator on host.315 //316 // Maybe it is also possible to do some magic with __functor?317 //318 // I don't want to make modules always responsible for additional secret data anyway,319 // so it should be in derivation, and not in the secret data itself.320 let generators = nix_go!(default_mk_secret_generators(Obj {321 recipients: <Vec<String>>::new(),322 }));323 let default_generator = nix_go!(default_call_package(generator)(generators));324325 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);326327 match kind {328 GeneratorKind::Impure => {329 generate_impure(330 config,331 display_name,332 secret,333 default_generator,334 owners,335 batch,336 )337 .await338 }339 GeneratorKind::Pure => {340 generate_pure(config, display_name, secret, default_generator, owners).await341 }342 }343}344async fn generate_shared(345 config: &Config,346 display_name: &str,347 secret: Value,348 expected_owners: Vec<String>,349 batch: Option<NixBuildBatch>,350) -> Result<FleetSharedSecret> {351 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);352 Ok(FleetSharedSecret {353 secret: generate(config, display_name, secret, &expected_owners, batch).await?,354 owners: expected_owners,355 })356}357358async fn parse_public(359 public: Option<String>,360 public_file: Option<PathBuf>,361) -> Result<Option<SecretData>> {362 Ok(match (public, public_file) {363 (Some(v), None) => Some(SecretData {364 data: v.into(),365 encrypted: false,366 }),367 (None, Some(v)) => Some(SecretData {368 data: read(v).await?,369 encrypted: false,370 }),371 (Some(_), Some(_)) => {372 bail!("only public or public_file should be set")373 }374 (None, None) => None,375 })376}377378async fn parse_secret() -> Result<Option<Vec<u8>>> {379 let mut input = vec![];380 stdin().read_to_end(&mut input)?;381 if input.is_empty() {382 Ok(None)383 } else {384 Ok(Some(input))385 }386}387388fn parse_machines(389 initial: Vec<String>,390 machines: Option<Vec<String>>,391 mut add_machines: Vec<String>,392 mut remove_machines: Vec<String>,393) -> Result<Vec<String>> {394 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {395 bail!("no operation");396 }397398 let initial_machines = initial.clone();399 let mut target_machines = initial;400 info!("Currently encrypted for {initial_machines:?}");401402 // ensure!(machines.is_some() || !add_machines.is_empty() || )403 if let Some(machines) = machines {404 ensure!(405 add_machines.is_empty() && remove_machines.is_empty(),406 "can't combine --machines and --add-machines/--remove-machines"407 );408 let target = initial_machines.iter().collect::<HashSet<_>>();409 let source = machines.iter().collect::<HashSet<_>>();410 for removed in target.difference(&source) {411 remove_machines.push((*removed).clone());412 }413 for added in source.difference(&target) {414 add_machines.push((*added).clone());415 }416 }417418 for machine in &remove_machines {419 let mut removed = false;420 while let Some(pos) = target_machines.iter().position(|m| m == machine) {421 target_machines.swap_remove(pos);422 removed = true;423 }424 if !removed {425 warn!("secret is not enabled for {machine}");426 }427 }428 for machine in &add_machines {429 if target_machines.iter().any(|m| m == machine) {430 warn!("secret is already added to {machine}");431 } else {432 target_machines.push(machine.to_owned());433 }434 }435 if !remove_machines.is_empty() {436 // TODO: maybe force secret regeneration?437 // Not that useful without revokation.438 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");439 }440 Ok(target_machines)441}442impl Secret {443 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {444 match self {445 Secret::ForceKeys => {446 for host in config.list_hosts().await? {447 if opts.should_skip(&host).await? {448 continue;449 }450 config.key(&host.name).await?;451 }452 }453 Secret::AddShared {454 mut machines,455 name,456 force,457 public,458 public_part: public_name,459 public_file,460 expires_at,461 re_add,462 part: part_name,463 } => {464 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).465466 let exists = config.has_shared(&name);467 if exists && !force && !re_add {468 bail!("secret already defined");469 }470 if re_add {471 // Fixme: use clap to limit this usage472 ensure!(!force, "--force and --readd are not compatible");473 ensure!(exists, "secret doesn't exists");474 ensure!(475 machines.is_empty(),476 "you can't use machines argument for --readd"477 );478 let shared = config.shared_secret(&name)?;479 machines = shared.owners;480 }481482 let recipients = config.recipients(machines.clone()).await?;483484 let mut parts = BTreeMap::new();485486 let mut input = vec![];487 io::stdin().read_to_end(&mut input)?;488489 if !input.is_empty() {490 let encrypted = encrypt_secret_data(recipients, input)491 .ok_or_else(|| anyhow!("no recipients provided"))?;492 parts.insert(part_name, FleetSecretPart { raw: encrypted });493 }494495 if let Some(public) = parse_public(public, public_file).await? {496 parts.insert(public_name, FleetSecretPart { raw: public });497 }498499 config.replace_shared(500 name,501 FleetSharedSecret {502 owners: machines,503 secret: FleetSecret {504 created_at: Utc::now(),505 expires_at,506 parts,507 },508 },509 );510 }511 Secret::Add {512 machine,513 name,514 replace,515 merge,516 public,517 public_part: public_name,518 public_file,519 part: part_name,520 } => {521 if config.has_secret(&machine, &name) && !replace && !merge {522 bail!("secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret");523 }524525 let mut out = if merge && !replace {526 config527 .host_secret(&machine, &name)528 .context("failed to read existing secret for --merge")?529 } else {530 FleetSecret {531 created_at: Utc::now(),532 expires_at: None,533 parts: BTreeMap::new(),534 }535 };536537 if let Some(secret) = parse_secret().await? {538 let recipient = config.recipient(&machine).await?;539 let encrypted =540 encrypt_secret_data(vec![recipient], secret).expect("recipient provided");541 if out542 .parts543 .insert(part_name.clone(), FleetSecretPart { raw: encrypted })544 .is_some() && !replace545 {546 bail!("part {part_name:?} is already defined");547 }548 }549550 if let Some(public) = parse_public(public, public_file).await? {551 if out552 .parts553 .insert(public_name.clone(), FleetSecretPart { raw: public })554 .is_some() && !replace555 {556 bail!("part {public_name:?} is already defined");557 }558 };559560 config.insert_secret(&machine, name, out);561 }562 #[allow(clippy::await_holding_refcell_ref)]563 Secret::Read {564 name,565 machine,566 part: part_name,567 } => {568 let secret = config.host_secret(&machine, &name)?;569 let Some(secret) = secret.parts.get(&part_name) else {570 bail!("no part {part_name} in secret {name}");571 };572 let data = if secret.raw.encrypted {573 let host = config.host(&machine).await?;574 host.decrypt(secret.raw.clone()).await?575 } else {576 secret.raw.data.clone()577 };578579 stdout().write_all(&data)?;580 }581 Secret::UpdateShared {582 name,583 machine,584 add_machine,585 remove_machine,586 prefer_identities,587 } => {588 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).589590 let secret = config.shared_secret(&name)?;591 if secret.secret.parts.values().all(|v| !v.raw.encrypted) {592 bail!("no secret");593 }594595 let initial_machines = secret.owners.clone();596 let target_machines = parse_machines(597 initial_machines.clone(),598 machine,599 add_machine,600 remove_machine,601 )?;602603 if target_machines.is_empty() {604 info!("no machines left for secret, removing it");605 config.remove_shared(&name);606 return Ok(());607 }608609 let config_field = &config.config_field;610 let field = nix_go!(config_field.sharedSecrets[{ name }]);611612 let updated = update_owner_set(613 &name,614 config,615 secret,616 field,617 &target_machines,618 &prefer_identities,619 None,620 )621 .await?;622 config.replace_shared(name, updated);623 }624 Secret::Regenerate { prefer_identities } => {625 info!("checking for secrets to regenerate");626 {627 let shared_batch = None;628 let _span = info_span!("shared").entered();629 let expected_shared_set = config630 .list_configured_shared()631 .await?632 .into_iter()633 .collect::<HashSet<_>>();634 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();635 for missing in expected_shared_set.difference(&shared_set) {636 let config_field = &config.config_field;637 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);638 let expected_owners: Option<Vec<String>> =639 nix_go_json!(secret.expectedOwners);640 let Some(expected_owners) = expected_owners else {641 // TODO: Might still need to regenerate642 continue;643 };644 info!("generating secret: {missing}");645 let shared = generate_shared(646 config,647 missing,648 secret,649 expected_owners,650 shared_batch.clone(),651 )652 .in_current_span()653 .await?;654 config.replace_shared(missing.to_string(), shared)655 }656 }657 let hosts_batch = None;658 for host in config.list_hosts().await? {659 if opts.should_skip(&host).await? {660 continue;661 }662663 let _span = info_span!("host", host = host.name).entered();664 let expected_set = host665 .list_configured_secrets()666 .in_current_span()667 .await?668 .into_iter()669 .collect::<HashSet<_>>();670 let stored_set = config671 .list_secrets(&host.name)672 .into_iter()673 .collect::<HashSet<_>>();674 for missing in expected_set.difference(&stored_set) {675 info!("generating secret: {missing}");676 let secret = host.secret_field(missing).in_current_span().await?;677 let generated = match generate(678 config,679 missing,680 secret,681 &[host.name.clone()],682 hosts_batch.clone(),683 )684 .in_current_span()685 .await686 {687 Ok(v) => v,688 Err(e) => {689 error!("{e:?}");690 continue;691 }692 };693 config.insert_secret(&host.name, missing.to_string(), generated)694 }695 }696 let mut to_remove = Vec::new();697 for name in &config.list_shared() {698 info!("updating secret: {name}");699 let data = config.shared_secret(name)?;700 let config_field = &config.config_field;701 let expected_owners: Vec<String> =702 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);703 if expected_owners.is_empty() {704 warn!("secret was removed from fleet config: {name}, removing from data");705 to_remove.push(name.to_string());706 continue;707 }708709 let secret = nix_go!(config_field.sharedSecrets[{ name }]);710 config.replace_shared(711 name.to_owned(),712 update_owner_set(713 name,714 config,715 data,716 secret,717 &expected_owners,718 &prefer_identities,719 None,720 )721 .await?,722 );723 }724 for k in to_remove {725 config.remove_shared(&k);726 }727 }728 Secret::List {} => {729 let _span = info_span!("loading secrets").entered();730 let configured = config.list_configured_shared().await?;731 #[derive(Tabled)]732 struct SecretDisplay {733 #[tabled(rename = "Name")]734 name: String,735 #[tabled(rename = "Owners")]736 owners: String,737 }738 let mut table = vec![];739 for name in configured.iter().cloned() {740 let config = config.clone();741 let expected_owners = config.shared_secret_expected_owners(&name).await?;742 let data = config.shared_secret(&name)?;743 let owners = data744 .owners745 .iter()746 .map(|o| {747 if expected_owners.contains(o) {748 o.green().to_string()749 } else {750 o.red().to_string()751 }752 })753 .collect::<Vec<_>>();754 table.push(SecretDisplay {755 owners: owners.join(", "),756 name,757 })758 }759 info!("loaded\n{}", Table::new(table).to_string())760 }761 Secret::Edit {762 name,763 machine,764 part,765 add,766 } => {767 let secret = config.host_secret(&machine, &name)?;768 if let Some(data) = secret.parts.get(&part) {769 let host = config.host(&machine).await?;770 let secret = host.decrypt(data.raw.clone()).await?;771 String::from_utf8(secret).context("secret is not utf8")?772 } else if add {773 String::new()774 } else {775 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");776 };777 }778 }779 Ok(())780 }781}782783/*784async fn edit_temp_file(785 builder: tempfile::Builder<'_, '_>,786 r: Vec<u8>,787 header: &str,788 comment: &str,789) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {790 if !stdin().is_tty() {791 // TODO: Also try to open /dev/tty directly?792 bail!("stdin is not tty, can't open editor");793 }794795 use std::fmt::Write;796 let mut file = builder.tempfile()?;797798 let mut full_header = String::new();799 let mut had = false;800 for line in header.trim_end().lines() {801 had = true;802 writeln!(&mut full_header, "{comment}{line}")?;803 }804 if had {805 writeln!(&mut full_header, "{}", comment.trim_end())?;806 }807 writeln!(808 &mut full_header,809 "{comment}Do not touch this header! It will be removed automatically"810 )?;811812 file.write_all(full_header.as_bytes())?;813 file.write_all(&r)?;814815 let abs_path = file.into_temp_path();816 let editor = std::env::var_os("VISUAL")817 .or_else(|| std::env::var_os("EDITOR"))818 .unwrap_or_else(|| "vi".into());819 let editor_args = shlex::bytes::split(editor.as_encoded_bytes())820 .ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;821 let editor_args = editor_args822 .into_iter()823 .map(|v| {824 // Only ASCII subsequences are replaced825 unsafe { OsString::from_encoded_bytes_unchecked(v) }826 })827 .collect_vec();828 let Some((editor, args)) = editor_args.split_first() else {829 bail!("EDITOR env var has no command");830 };831 let mut command = Command::new(editor);832 command.args(args);833834 let path_arg = abs_path.canonicalize()?;835836 // TODO: Save full state, using tcget/_getmode/_setmode837 let was_raw = terminal::is_raw_mode_enabled()?;838 terminal::enable_raw_mode()?;839840 let status = command.arg(path_arg).status().await;841842 if !was_raw {843 terminal::disable_raw_mode()?;844 }845846 let success = match status {847 Ok(s) => s.success(),848 Err(e) if e.kind() == io::ErrorKind::NotFound => {849 bail!("editor not found")850 }851 Err(e) => bail!("editor spawn error: {e}"),852 };853854 let mut file = std::fs::read(&abs_path).context("read editor output")?;855 let Some(v) = file.strip_prefix(full_header.as_bytes()) else {856 todo!();857 };858 todo!();859860 // Ok((success, abs_path))861}862*/1use std::{2 collections::{BTreeMap, BTreeSet, HashSet},3 io::{self, stdin, stdout, Read, Write},4 path::PathBuf,5};67use age::Recipient;8use anyhow::{anyhow, bail, ensure, Context, Result};9use chrono::{DateTime, Utc};10use clap::Parser;11use fleet_base::{12 fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},13 host::Config,14 opts::FleetOpts,15};16use fleet_shared::SecretData;17use nix_eval::{nix_go, nix_go_json, NixBuildBatch, Value};18use owo_colors::OwoColorize;19use serde::Deserialize;20use tabled::{Table, Tabled};21use tokio::fs::read;22use tracing::{error, info, info_span, warn, Instrument};2324#[derive(Parser)]25pub enum Secret {26 /// Force load host keys for all defined hosts27 ForceKeys,28 /// Add secret, data should be provided in stdin29 AddShared {30 /// Secret name31 name: String,32 /// Secret owners33 #[clap(long, short)]34 machines: Vec<String>,35 /// Override secret if already present36 #[clap(long)]37 force: bool,38 /// Secret public part39 #[clap(long)]40 public: Option<String>,41 /// Load public part from specified file42 #[clap(long)]43 public_file: Option<PathBuf>,4445 /// Create a notification on secret expiration46 #[clap(long)]47 expires_at: Option<DateTime<Utc>>,4849 /// Secret with this name already exists, override its value while keeping the same owners.50 #[clap(long)]51 re_add: bool,5253 /// How to name public secret part54 #[clap(long, short = 'p', default_value = "public")]55 public_part: String,56 /// How to name private secret part57 #[clap(short = 's', long, default_value = "secret")]58 part: String,59 },60 /// Add secret, data should be provided in stdin61 Add {62 /// Secret name63 name: String,64 /// Secret owner65 #[clap(short = 'm', long)]66 machine: String,67 /// Replace secret if already present68 #[clap(long)]69 replace: bool,70 /// Add new parts to existing secret71 #[clap(long)]72 merge: bool,73 /// Secret public part74 #[clap(long)]75 public: Option<String>,76 /// Load public part from specified file77 #[clap(long)]78 public_file: Option<PathBuf>,7980 /// How to name public secret part81 #[clap(short = 'p', long, default_value = "public")]82 public_part: String,83 /// How to name private secret part84 #[clap(short = 's', long, default_value = "secret")]85 part: String,86 },87 /// Read secret from remote host, requires sudo on said host88 Read {89 name: String,90 #[clap(short = 'm', long)]91 machine: String,9293 /// Which private secret part to read94 #[clap(short = 'p', long, default_value = "secret")]95 part: String,96 },97 UpdateShared {98 name: String,99100 #[clap(short = 'm', long)]101 machine: Option<Vec<String>>,102103 #[clap(long)]104 add_machine: Vec<String>,105 #[clap(long)]106 remove_machine: Vec<String>,107108 /// Which host should we use to decrypt109 #[clap(long)]110 prefer_identities: Vec<String>,111 },112 Regenerate {113 /// Which host should we use to decrypt, in case if reencryption is required, without114 /// regeneration115 #[clap(long)]116 prefer_identities: Vec<String>,117 },118 List {},119 Edit {120 name: String,121 #[clap(short = 'm', long)]122 machine: String,123124 #[clap(long)]125 add: bool,126127 /// Which private secret part to read128 #[clap(short = 'p', long, default_value = "secret")]129 part: String,130 },131}132133#[tracing::instrument(skip(config, secret, field, prefer_identities, batch))]134async fn update_owner_set(135 secret_name: &str,136 config: &Config,137 mut secret: FleetSharedSecret,138 field: Value,139 updated_set: &[String],140 prefer_identities: &[String],141 batch: Option<NixBuildBatch>,142) -> Result<FleetSharedSecret> {143 let original_set = secret.owners.clone();144145 let set = original_set.iter().collect::<BTreeSet<_>>();146 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();147148 if set == expected_set {149 info!("no need to update owner list, it is already correct");150 return Ok(secret);151 }152153 let should_regenerate = if set.difference(&expected_set).next().is_some() {154 // TODO: Remove this warning for revokable secrets.155 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");156 nix_go_json!(field.regenerateOnOwnerRemoved)157 } else if expected_set.difference(&set).next().is_some() {158 nix_go_json!(field.regenerateOnOwnerAdded)159 } else {160 false161 };162163 if should_regenerate {164 info!("secret is owner-dependent, will regenerate");165 let generated =166 generate_shared(config, secret_name, field, updated_set.to_vec(), batch).await?;167 Ok(generated)168 } else {169 drop(batch);170 let identity_holder = if !prefer_identities.is_empty() {171 prefer_identities172 .iter()173 .find(|i| original_set.iter().any(|s| s == *i))174 } else {175 secret.owners.first()176 };177 let Some(identity_holder) = identity_holder else {178 bail!("no available holder found");179 };180181 for (part_name, part) in secret.secret.parts.iter_mut() {182 let _span = info_span!("part reencryption", part_name);183 if !part.raw.encrypted {184 continue;185 }186 let host = config.host(identity_holder).await?;187 let encrypted = host188 .reencrypt(part.raw.clone(), updated_set.to_vec())189 .await?;190 part.raw = encrypted;191 }192193 secret.owners = updated_set.to_vec();194 Ok(secret)195 }196}197198#[derive(Deserialize)]199#[serde(rename_all = "camelCase")]200enum GeneratorKind {201 Impure,202 Pure,203}204205async fn generate_pure(206 _config: &Config,207 _display_name: &str,208 _secret: Value,209 _default_generator: Value,210 _owners: &[String],211) -> Result<FleetSecret> {212 bail!("pure generators are broken for now")213}214async fn generate_impure(215 config: &Config,216 _display_name: &str,217 secret: Value,218 default_generator: Value,219 owners: &[String],220 batch: Option<NixBuildBatch>,221) -> Result<FleetSecret> {222 let generator = nix_go!(secret.generator);223 let on: Option<String> = nix_go_json!(default_generator.impureOn);224225 let host = if let Some(on) = &on {226 config.host(on).await?227 } else {228 config.local_host()229 };230 let on_pkgs = host.pkgs().await?;231 let call_package = nix_go!(on_pkgs.callPackage);232 let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);233234 let mut recipients = Vec::new();235 for owner in owners {236 let key = config.key(owner).await?;237 recipients.push(key);238 }239 let generators = nix_go!(mk_secret_generators(Obj { recipients }));240241 let generator = nix_go!(call_package(generator)(generators));242243 let generator = generator.build_maybe_batch(batch).await?;244 let generator = generator245 .get("out")246 .ok_or_else(|| anyhow!("missing generateImpure out"))?;247 let generator = host.remote_derivation(generator).await?;248249 let out_parent = host.mktemp_dir().await?;250 let out = format!("{out_parent}/out");251252 let mut gen = host.cmd(generator).await?;253 gen.env("out", &out);254 if on.is_none() {255 // This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.256 let project_path: String = config257 .directory258 .clone()259 .into_os_string()260 .into_string()261 .map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;262 gen.env("FLEET_PROJECT", project_path);263 }264 gen.run().await.context("impure generator")?;265266 {267 let marker = host.read_file_text(format!("{out}/marker")).await?;268 ensure!(marker == "SUCCESS", "generation not succeeded");269 }270271 let mut parts = BTreeMap::new();272 for part in host.read_dir(&out).await? {273 if part == "created_at" || part == "expired_at" || part == "marker" {274 continue;275 }276 let contents: SecretData = host277 .read_file_text(format!("{out}/{part}"))278 .await?279 .parse()280 .map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;281 parts.insert(part.to_owned(), FleetSecretPart { raw: contents });282 }283284 let created_at = host.read_file_value(format!("{out}/created_at")).await?;285 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();286287 Ok(FleetSecret {288 created_at,289 expires_at,290 parts,291 })292}293async fn generate(294 config: &Config,295 display_name: &str,296 secret: Value,297 owners: &[String],298 batch: Option<NixBuildBatch>,299) -> Result<FleetSecret> {300 let generator = nix_go!(secret.generator);301 // Can't properly check on nix module system level302 {303 let gen_ty = generator.type_of().await?;304 if gen_ty == "null" {305 bail!("secret has no generator defined, can't automatically generate it.");306 }307 if gen_ty != "lambda" {308 bail!("generator should be lambda, got {gen_ty}");309 }310 }311 let default_pkgs = &config.default_pkgs;312 let default_call_package = nix_go!(default_pkgs.callPackage);313 let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);314 // Generators provide additional information in passthru, to access315 // passthru we should call generator, but information about where this generator is supposed to build316 // is located in passthru... Thus evaluating generator on host.317 //318 // Maybe it is also possible to do some magic with __functor?319 //320 // I don't want to make modules always responsible for additional secret data anyway,321 // so it should be in derivation, and not in the secret data itself.322 let generators = nix_go!(default_mk_secret_generators(Obj {323 recipients: <Vec<String>>::new(),324 }));325 let default_generator = nix_go!(default_call_package(generator)(generators));326327 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);328329 match kind {330 GeneratorKind::Impure => {331 generate_impure(332 config,333 display_name,334 secret,335 default_generator,336 owners,337 batch,338 )339 .await340 }341 GeneratorKind::Pure => {342 generate_pure(config, display_name, secret, default_generator, owners).await343 }344 }345}346async fn generate_shared(347 config: &Config,348 display_name: &str,349 secret: Value,350 expected_owners: Vec<String>,351 batch: Option<NixBuildBatch>,352) -> Result<FleetSharedSecret> {353 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);354 Ok(FleetSharedSecret {355 secret: generate(config, display_name, secret, &expected_owners, batch).await?,356 owners: expected_owners,357 })358}359360async fn parse_public(361 public: Option<String>,362 public_file: Option<PathBuf>,363) -> Result<Option<SecretData>> {364 Ok(match (public, public_file) {365 (Some(v), None) => Some(SecretData {366 data: v.into(),367 encrypted: false,368 }),369 (None, Some(v)) => Some(SecretData {370 data: read(v).await?,371 encrypted: false,372 }),373 (Some(_), Some(_)) => {374 bail!("only public or public_file should be set")375 }376 (None, None) => None,377 })378}379380async fn parse_secret() -> Result<Option<Vec<u8>>> {381 let mut input = vec![];382 stdin().read_to_end(&mut input)?;383 if input.is_empty() {384 Ok(None)385 } else {386 Ok(Some(input))387 }388}389390fn parse_machines(391 initial: Vec<String>,392 machines: Option<Vec<String>>,393 mut add_machines: Vec<String>,394 mut remove_machines: Vec<String>,395) -> Result<Vec<String>> {396 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {397 bail!("no operation");398 }399400 let initial_machines = initial.clone();401 let mut target_machines = initial;402 info!("Currently encrypted for {initial_machines:?}");403404 // ensure!(machines.is_some() || !add_machines.is_empty() || )405 if let Some(machines) = machines {406 ensure!(407 add_machines.is_empty() && remove_machines.is_empty(),408 "can't combine --machines and --add-machines/--remove-machines"409 );410 let target = initial_machines.iter().collect::<HashSet<_>>();411 let source = machines.iter().collect::<HashSet<_>>();412 for removed in target.difference(&source) {413 remove_machines.push((*removed).clone());414 }415 for added in source.difference(&target) {416 add_machines.push((*added).clone());417 }418 }419420 for machine in &remove_machines {421 let mut removed = false;422 while let Some(pos) = target_machines.iter().position(|m| m == machine) {423 target_machines.swap_remove(pos);424 removed = true;425 }426 if !removed {427 warn!("secret is not enabled for {machine}");428 }429 }430 for machine in &add_machines {431 if target_machines.iter().any(|m| m == machine) {432 warn!("secret is already added to {machine}");433 } else {434 target_machines.push(machine.to_owned());435 }436 }437 if !remove_machines.is_empty() {438 // TODO: maybe force secret regeneration?439 // Not that useful without revokation.440 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");441 }442 Ok(target_machines)443}444impl Secret {445 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {446 match self {447 Secret::ForceKeys => {448 for host in config.list_hosts().await? {449 if opts.should_skip(&host).await? {450 continue;451 }452 config.key(&host.name).await?;453 }454 }455 Secret::AddShared {456 mut machines,457 name,458 force,459 public,460 public_part: public_name,461 public_file,462 expires_at,463 re_add,464 part: part_name,465 } => {466 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).467468 let exists = config.has_shared(&name);469 if exists && !force && !re_add {470 bail!("secret already defined");471 }472 if re_add {473 // Fixme: use clap to limit this usage474 ensure!(!force, "--force and --readd are not compatible");475 ensure!(exists, "secret doesn't exists");476 ensure!(477 machines.is_empty(),478 "you can't use machines argument for --readd"479 );480 let shared = config.shared_secret(&name)?;481 machines = shared.owners;482 }483484 let recipients = config.recipients(machines.clone()).await?;485486 let mut parts = BTreeMap::new();487488 let mut input = vec![];489 io::stdin().read_to_end(&mut input)?;490491 if !input.is_empty() {492 let encrypted =493 encrypt_secret_data(recipients.iter().map(|r| r as &dyn Recipient), input)494 .ok_or_else(|| anyhow!("no recipients provided"))?;495 parts.insert(part_name, FleetSecretPart { raw: encrypted });496 }497498 if let Some(public) = parse_public(public, public_file).await? {499 parts.insert(public_name, FleetSecretPart { raw: public });500 }501502 config.replace_shared(503 name,504 FleetSharedSecret {505 owners: machines,506 secret: FleetSecret {507 created_at: Utc::now(),508 expires_at,509 parts,510 },511 },512 );513 }514 Secret::Add {515 machine,516 name,517 replace,518 merge,519 public,520 public_part: public_name,521 public_file,522 part: part_name,523 } => {524 if config.has_secret(&machine, &name) && !replace && !merge {525 bail!("secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret");526 }527528 let mut out = if merge && !replace {529 config530 .host_secret(&machine, &name)531 .context("failed to read existing secret for --merge")?532 } else {533 FleetSecret {534 created_at: Utc::now(),535 expires_at: None,536 parts: BTreeMap::new(),537 }538 };539540 if let Some(secret) = parse_secret().await? {541 let recipient = config.recipient(&machine).await?;542 let encrypted = encrypt_secret_data([&recipient as &dyn Recipient], secret)543 .expect("recipient provided");544 if out545 .parts546 .insert(part_name.clone(), FleetSecretPart { raw: encrypted })547 .is_some() && !replace548 {549 bail!("part {part_name:?} is already defined");550 }551 }552553 if let Some(public) = parse_public(public, public_file).await? {554 if out555 .parts556 .insert(public_name.clone(), FleetSecretPart { raw: public })557 .is_some() && !replace558 {559 bail!("part {public_name:?} is already defined");560 }561 };562563 config.insert_secret(&machine, name, out);564 }565 #[allow(clippy::await_holding_refcell_ref)]566 Secret::Read {567 name,568 machine,569 part: part_name,570 } => {571 let secret = config.host_secret(&machine, &name)?;572 let Some(secret) = secret.parts.get(&part_name) else {573 bail!("no part {part_name} in secret {name}");574 };575 let data = if secret.raw.encrypted {576 let host = config.host(&machine).await?;577 host.decrypt(secret.raw.clone()).await?578 } else {579 secret.raw.data.clone()580 };581582 stdout().write_all(&data)?;583 }584 Secret::UpdateShared {585 name,586 machine,587 add_machine,588 remove_machine,589 prefer_identities,590 } => {591 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).592593 let secret = config.shared_secret(&name)?;594 if secret.secret.parts.values().all(|v| !v.raw.encrypted) {595 bail!("no secret");596 }597598 let initial_machines = secret.owners.clone();599 let target_machines = parse_machines(600 initial_machines.clone(),601 machine,602 add_machine,603 remove_machine,604 )?;605606 if target_machines.is_empty() {607 info!("no machines left for secret, removing it");608 config.remove_shared(&name);609 return Ok(());610 }611612 let config_field = &config.config_field;613 let field = nix_go!(config_field.sharedSecrets[{ name }]);614615 let updated = update_owner_set(616 &name,617 config,618 secret,619 field,620 &target_machines,621 &prefer_identities,622 None,623 )624 .await?;625 config.replace_shared(name, updated);626 }627 Secret::Regenerate { prefer_identities } => {628 info!("checking for secrets to regenerate");629 {630 let shared_batch = None;631 let _span = info_span!("shared").entered();632 let expected_shared_set = config633 .list_configured_shared()634 .await?635 .into_iter()636 .collect::<HashSet<_>>();637 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();638 for missing in expected_shared_set.difference(&shared_set) {639 let config_field = &config.config_field;640 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);641 let expected_owners: Option<Vec<String>> =642 nix_go_json!(secret.expectedOwners);643 let Some(expected_owners) = expected_owners else {644 // TODO: Might still need to regenerate645 continue;646 };647 info!("generating secret: {missing}");648 let shared = generate_shared(649 config,650 missing,651 secret,652 expected_owners,653 shared_batch.clone(),654 )655 .in_current_span()656 .await?;657 config.replace_shared(missing.to_string(), shared)658 }659 }660 let hosts_batch = None;661 for host in config.list_hosts().await? {662 if opts.should_skip(&host).await? {663 continue;664 }665666 let _span = info_span!("host", host = host.name).entered();667 let expected_set = host668 .list_configured_secrets()669 .in_current_span()670 .await?671 .into_iter()672 .collect::<HashSet<_>>();673 let stored_set = config674 .list_secrets(&host.name)675 .into_iter()676 .collect::<HashSet<_>>();677 for missing in expected_set.difference(&stored_set) {678 info!("generating secret: {missing}");679 let secret = host.secret_field(missing).in_current_span().await?;680 let generated = match generate(681 config,682 missing,683 secret,684 &[host.name.clone()],685 hosts_batch.clone(),686 )687 .in_current_span()688 .await689 {690 Ok(v) => v,691 Err(e) => {692 error!("{e:?}");693 continue;694 }695 };696 config.insert_secret(&host.name, missing.to_string(), generated)697 }698 }699 let mut to_remove = Vec::new();700 for name in &config.list_shared() {701 info!("updating secret: {name}");702 let data = config.shared_secret(name)?;703 let config_field = &config.config_field;704 let expected_owners: Vec<String> =705 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);706 if expected_owners.is_empty() {707 warn!("secret was removed from fleet config: {name}, removing from data");708 to_remove.push(name.to_string());709 continue;710 }711712 let secret = nix_go!(config_field.sharedSecrets[{ name }]);713 config.replace_shared(714 name.to_owned(),715 update_owner_set(716 name,717 config,718 data,719 secret,720 &expected_owners,721 &prefer_identities,722 None,723 )724 .await?,725 );726 }727 for k in to_remove {728 config.remove_shared(&k);729 }730 }731 Secret::List {} => {732 let _span = info_span!("loading secrets").entered();733 let configured = config.list_configured_shared().await?;734 #[derive(Tabled)]735 struct SecretDisplay {736 #[tabled(rename = "Name")]737 name: String,738 #[tabled(rename = "Owners")]739 owners: String,740 }741 let mut table = vec![];742 for name in configured.iter().cloned() {743 let config = config.clone();744 let expected_owners = config.shared_secret_expected_owners(&name).await?;745 let data = config.shared_secret(&name)?;746 let owners = data747 .owners748 .iter()749 .map(|o| {750 if expected_owners.contains(o) {751 o.green().to_string()752 } else {753 o.red().to_string()754 }755 })756 .collect::<Vec<_>>();757 table.push(SecretDisplay {758 owners: owners.join(", "),759 name,760 })761 }762 info!("loaded\n{}", Table::new(table).to_string())763 }764 Secret::Edit {765 name,766 machine,767 part,768 add,769 } => {770 let secret = config.host_secret(&machine, &name)?;771 if let Some(data) = secret.parts.get(&part) {772 let host = config.host(&machine).await?;773 let secret = host.decrypt(data.raw.clone()).await?;774 String::from_utf8(secret).context("secret is not utf8")?775 } else if add {776 String::new()777 } else {778 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");779 };780 }781 }782 Ok(())783 }784}785786/*787async fn edit_temp_file(788 builder: tempfile::Builder<'_, '_>,789 r: Vec<u8>,790 header: &str,791 comment: &str,792) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {793 if !stdin().is_tty() {794 // TODO: Also try to open /dev/tty directly?795 bail!("stdin is not tty, can't open editor");796 }797798 use std::fmt::Write;799 let mut file = builder.tempfile()?;800801 let mut full_header = String::new();802 let mut had = false;803 for line in header.trim_end().lines() {804 had = true;805 writeln!(&mut full_header, "{comment}{line}")?;806 }807 if had {808 writeln!(&mut full_header, "{}", comment.trim_end())?;809 }810 writeln!(811 &mut full_header,812 "{comment}Do not touch this header! It will be removed automatically"813 )?;814815 file.write_all(full_header.as_bytes())?;816 file.write_all(&r)?;817818 let abs_path = file.into_temp_path();819 let editor = std::env::var_os("VISUAL")820 .or_else(|| std::env::var_os("EDITOR"))821 .unwrap_or_else(|| "vi".into());822 let editor_args = shlex::bytes::split(editor.as_encoded_bytes())823 .ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;824 let editor_args = editor_args825 .into_iter()826 .map(|v| {827 // Only ASCII subsequences are replaced828 unsafe { OsString::from_encoded_bytes_unchecked(v) }829 })830 .collect_vec();831 let Some((editor, args)) = editor_args.split_first() else {832 bail!("EDITOR env var has no command");833 };834 let mut command = Command::new(editor);835 command.args(args);836837 let path_arg = abs_path.canonicalize()?;838839 // TODO: Save full state, using tcget/_getmode/_setmode840 let was_raw = terminal::is_raw_mode_enabled()?;841 terminal::enable_raw_mode()?;842843 let status = command.arg(path_arg).status().await;844845 if !was_raw {846 terminal::disable_raw_mode()?;847 }848849 let success = match status {850 Ok(s) => s.success(),851 Err(e) if e.kind() == io::ErrorKind::NotFound => {852 bail!("editor not found")853 }854 Err(e) => bail!("editor spawn error: {e}"),855 };856857 let mut file = std::fs::read(&abs_path).context("read editor output")?;858 let Some(v) = file.strip_prefix(full_header.as_bytes()) else {859 todo!();860 };861 todo!();862863 // Ok((success, abs_path))864}865*/cmds/generator-helper/src/main.rsdiffbeforeafterboth--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -89,15 +89,10 @@
.map_err(|e| anyhow!("parse recipients: {e:?}"))
}
fn make_encryptor(r: &Identities) -> Result<Encryptor> {
- Ok(Encryptor::with_recipients(
- r.iter()
- .map(|v| {
- let coerced: Box<dyn Recipient + Send> = Box::new(v.clone());
- coerced
- })
- .collect(),
+ Ok(
+ Encryptor::with_recipients(r.iter().map(|v| v as &dyn Recipient))
+ .expect("list is not empty"),
)
- .expect("list is not empty"))
}
fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't {
fn coerce<'t>(w: impl Write + 't) -> Box<dyn Write + 't> {
cmds/install-secrets/src/main.rsdiffbeforeafterboth--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -68,10 +68,9 @@
ensure!(input.encrypted, "passed data is not encrypted!");
let mut input = Cursor::new(&input.data);
let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;
- let decryptor = match decryptor {
- Decryptor::Recipients(r) => r,
- Decryptor::Passphrase(_) => bail!("should be recipients"),
- };
+ if decryptor.is_scrypt() {
+ bail!("should be recipients");
+ }
let mut decryptor = decryptor
.decrypt(iter::once(identity as &dyn age::Identity))
.context("failed to decrypt, wrong key?")?;
@@ -89,10 +88,7 @@
SshRecipient::from_str(&t).map_err(|e| anyhow!("failed to parse recipient: {e:?}"))
})
.collect::<Result<Vec<SshRecipient>>>()?;
- let recipients = recipients
- .into_iter()
- .map(|v| Box::new(v) as Box<dyn Recipient + Send>)
- .collect::<Vec<_>>();
+ let recipients = recipients.iter().map(|v| v as &dyn Recipient);
let mut encrypted = vec![];
let mut encryptor = Encryptor::with_recipients(recipients)
.expect("recipients provided")
crates/fleet-base/src/fleetdata.rsdiffbeforeafterboth--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -6,7 +6,6 @@
use age::Recipient;
use chrono::{DateTime, Utc};
use fleet_shared::SecretData;
-use itertools::Itertools;
use serde::{de::Error, Deserialize, Serialize};
use serde_json::Value;
@@ -73,16 +72,13 @@
}
/// Returns None if recipients.is_empty()
-pub fn encrypt_secret_data(
- recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,
+pub fn encrypt_secret_data<'a>(
+ recipients: impl IntoIterator<Item = &'a dyn Recipient>,
data: Vec<u8>,
) -> Option<SecretData> {
let mut encrypted = vec![];
- let recipients = recipients
- .into_iter()
- .map(|v| Box::new(v) as Box<dyn Recipient + Send>)
- .collect_vec();
- let mut encryptor = age::Encryptor::with_recipients(recipients)?
+ let mut encryptor = age::Encryptor::with_recipients(recipients.into_iter())
+ .ok()?
.wrap_output(&mut encrypted)
.expect("in memory write");
io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");
flake.lockdiffbeforeafterboth--- a/flake.lock
+++ b/flake.lock
@@ -37,11 +37,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1731514040,
- "narHash": "sha256-4VkY8gwyR83N6MPT7ipXTOSBXpVL2Hrwh898UAR3HZ8=",
+ "lastModified": 1731873344,
+ "narHash": "sha256-bKfFggwcvvh9gmOsaMCXKVAGBfXCZZ6QrxLq9Nb1/vw=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "155168226cb666d242306e13d7dbdaa8a76d20e1",
+ "rev": "39e98fadd66c2564ac85b1f65bab89e044302c62",
"type": "github"
},
"original": {
@@ -66,11 +66,11 @@
]
},
"locked": {
- "lastModified": 1731464916,
- "narHash": "sha256-WZ5rpjr/wCt7yBOUsvDE2i22hYz9g8W921jlwVktRQ4=",
+ "lastModified": 1731820690,
+ "narHash": "sha256-/hHFMTD+FGURXZ4JtfXoIgpy87zL505pVi6AL76Wc+U=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "2c19bad6e881b5a154cafb7f9106879b5b356d1f",
+ "rev": "bbab2ab9e1932133b1996baa1dc00fefe924ca81",
"type": "github"
},
"original": {
modules/secrets.nixdiffbeforeafterboth--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -1,4 +1,8 @@
-{lib, config, ...}: let
+{
+ lib,
+ config,
+ ...
+}: let
inherit (lib.options) mkOption;
inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule;
inherit (lib.strings) concatStringsSep;
@@ -51,9 +55,11 @@
};
};
config = {
- hosts = mapAttrs (_: secretMap: {
- nixos.secrets = mapAttrs (_: s: removeAttrs s ["createdAt" "expiresAt"]) secretMap;
- }) config.data.hostSecrets;
+ hosts =
+ mapAttrs (_: secretMap: {
+ nixos.secrets = mapAttrs (_: s: removeAttrs s ["createdAt" "expiresAt"]) secretMap;
+ })
+ config.data.hostSecrets;
nixpkgs.overlays = [
(final: prev: {
mkSecretGenerators = {recipients}: rec {