@wlfi-agent/cli 1.4.12 → 1.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/Cargo.lock +3968 -0
  2. package/Cargo.toml +50 -0
  3. package/README.md +426 -6
  4. package/crates/vault-cli-admin/Cargo.toml +26 -0
  5. package/crates/vault-cli-admin/src/io_utils.rs +500 -0
  6. package/crates/vault-cli-admin/src/main.rs +3990 -0
  7. package/crates/vault-cli-admin/src/shared_config.rs +624 -0
  8. package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
  9. package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
  10. package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
  11. package/crates/vault-cli-admin/src/tui.rs +3410 -0
  12. package/crates/vault-cli-agent/Cargo.toml +24 -0
  13. package/crates/vault-cli-agent/src/io_utils.rs +576 -0
  14. package/crates/vault-cli-agent/src/main.rs +833 -0
  15. package/crates/vault-cli-daemon/Cargo.toml +28 -0
  16. package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
  17. package/crates/vault-cli-daemon/src/main.rs +644 -0
  18. package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
  19. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
  20. package/crates/vault-daemon/Cargo.toml +32 -0
  21. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
  22. package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
  23. package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
  24. package/crates/vault-daemon/src/lib.rs +54 -0
  25. package/crates/vault-daemon/src/persistence.rs +441 -0
  26. package/crates/vault-daemon/src/tests.rs +237 -0
  27. package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
  28. package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
  29. package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
  30. package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
  31. package/crates/vault-domain/Cargo.toml +20 -0
  32. package/crates/vault-domain/src/action.rs +849 -0
  33. package/crates/vault-domain/src/address.rs +51 -0
  34. package/crates/vault-domain/src/approval.rs +90 -0
  35. package/crates/vault-domain/src/constants.rs +4 -0
  36. package/crates/vault-domain/src/error.rs +54 -0
  37. package/crates/vault-domain/src/keys.rs +71 -0
  38. package/crates/vault-domain/src/lib.rs +42 -0
  39. package/crates/vault-domain/src/nonce.rs +102 -0
  40. package/crates/vault-domain/src/policy.rs +172 -0
  41. package/crates/vault-domain/src/request.rs +53 -0
  42. package/crates/vault-domain/src/scope.rs +24 -0
  43. package/crates/vault-domain/src/session.rs +50 -0
  44. package/crates/vault-domain/src/signature.rs +34 -0
  45. package/crates/vault-domain/src/tests.rs +651 -0
  46. package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
  47. package/crates/vault-policy/Cargo.toml +17 -0
  48. package/crates/vault-policy/src/engine.rs +301 -0
  49. package/crates/vault-policy/src/error.rs +81 -0
  50. package/crates/vault-policy/src/lib.rs +17 -0
  51. package/crates/vault-policy/src/report.rs +34 -0
  52. package/crates/vault-policy/src/tests.rs +891 -0
  53. package/crates/vault-policy/src/tests_explain.rs +78 -0
  54. package/crates/vault-sdk-agent/Cargo.toml +21 -0
  55. package/crates/vault-sdk-agent/src/lib.rs +711 -0
  56. package/crates/vault-signer/Cargo.toml +25 -0
  57. package/crates/vault-signer/src/lib.rs +731 -0
  58. package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
  59. package/crates/vault-transport-unix/Cargo.toml +24 -0
  60. package/crates/vault-transport-unix/src/lib.rs +1640 -0
  61. package/crates/vault-transport-xpc/Cargo.toml +25 -0
  62. package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
  63. package/crates/vault-transport-xpc/src/lib.rs +680 -0
  64. package/crates/vault-transport-xpc/src/tests.rs +818 -0
  65. package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
  66. package/dist/cli.cjs +35088 -0
  67. package/dist/cli.cjs.map +1 -0
  68. package/package.json +49 -43
  69. package/packages/cache/.turbo/turbo-build.log +52 -0
  70. package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
  71. package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
  72. package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
  73. package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
  74. package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
  75. package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
  76. package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
  77. package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
  78. package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
  79. package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
  80. package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
  81. package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
  82. package/packages/cache/dist/client/index.cjs +11 -0
  83. package/packages/cache/dist/client/index.cjs.map +1 -0
  84. package/packages/cache/dist/client/index.d.cts +15 -0
  85. package/packages/cache/dist/client/index.d.ts +15 -0
  86. package/packages/cache/dist/client/index.js +11 -0
  87. package/packages/cache/dist/client/index.js.map +1 -0
  88. package/packages/cache/dist/errors/index.cjs +11 -0
  89. package/packages/cache/dist/errors/index.cjs.map +1 -0
  90. package/packages/cache/dist/errors/index.d.cts +26 -0
  91. package/packages/cache/dist/errors/index.d.ts +26 -0
  92. package/packages/cache/dist/errors/index.js +11 -0
  93. package/packages/cache/dist/errors/index.js.map +1 -0
  94. package/packages/cache/dist/index.cjs +29 -0
  95. package/packages/cache/dist/index.cjs.map +1 -0
  96. package/packages/cache/dist/index.d.cts +4 -0
  97. package/packages/cache/dist/index.d.ts +4 -0
  98. package/packages/cache/dist/index.js +29 -0
  99. package/packages/cache/dist/index.js.map +1 -0
  100. package/packages/cache/dist/service/index.cjs +15 -0
  101. package/packages/cache/dist/service/index.cjs.map +1 -0
  102. package/packages/cache/dist/service/index.d.cts +184 -0
  103. package/packages/cache/dist/service/index.d.ts +184 -0
  104. package/packages/cache/dist/service/index.js +15 -0
  105. package/packages/cache/dist/service/index.js.map +1 -0
  106. package/packages/cache/node_modules/.bin/jiti +17 -0
  107. package/packages/cache/node_modules/.bin/tsc +17 -0
  108. package/packages/cache/node_modules/.bin/tsserver +17 -0
  109. package/packages/cache/node_modules/.bin/tsup +17 -0
  110. package/packages/cache/node_modules/.bin/tsup-node +17 -0
  111. package/packages/cache/node_modules/.bin/tsx +17 -0
  112. package/packages/cache/node_modules/.bin/vitest +17 -0
  113. package/packages/cache/package.json +48 -0
  114. package/packages/cache/src/client/index.ts +56 -0
  115. package/packages/cache/src/errors/index.ts +53 -0
  116. package/packages/cache/src/index.ts +3 -0
  117. package/packages/cache/src/service/index.test.ts +263 -0
  118. package/packages/cache/src/service/index.ts +678 -0
  119. package/packages/cache/tsconfig.json +13 -0
  120. package/packages/cache/tsup.config.ts +13 -0
  121. package/packages/cache/vitest.config.ts +16 -0
  122. package/packages/config/.turbo/turbo-build.log +18 -0
  123. package/packages/config/dist/index.cjs +1037 -0
  124. package/packages/config/dist/index.cjs.map +1 -0
  125. package/packages/config/dist/index.d.ts +131 -0
  126. package/packages/config/node_modules/.bin/jiti +17 -0
  127. package/packages/config/node_modules/.bin/tsc +17 -0
  128. package/packages/config/node_modules/.bin/tsserver +17 -0
  129. package/packages/config/node_modules/.bin/tsup +17 -0
  130. package/packages/config/node_modules/.bin/tsup-node +17 -0
  131. package/packages/config/node_modules/.bin/tsx +17 -0
  132. package/packages/config/package.json +21 -0
  133. package/packages/config/src/index.js +1 -0
  134. package/packages/config/src/index.ts +1282 -0
  135. package/packages/config/tsconfig.json +4 -0
  136. package/packages/rpc/.turbo/turbo-build.log +32 -0
  137. package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
  138. package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
  139. package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
  140. package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
  141. package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
  142. package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
  143. package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
  144. package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
  145. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
  146. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
  147. package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
  148. package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
  149. package/packages/rpc/dist/index.cjs +7342 -0
  150. package/packages/rpc/dist/index.cjs.map +1 -0
  151. package/packages/rpc/dist/index.d.ts +3857 -0
  152. package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
  153. package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
  154. package/packages/rpc/node_modules/.bin/jiti +17 -0
  155. package/packages/rpc/node_modules/.bin/tsc +17 -0
  156. package/packages/rpc/node_modules/.bin/tsserver +17 -0
  157. package/packages/rpc/node_modules/.bin/tsup +17 -0
  158. package/packages/rpc/node_modules/.bin/tsup-node +17 -0
  159. package/packages/rpc/node_modules/.bin/tsx +17 -0
  160. package/packages/rpc/package.json +25 -0
  161. package/packages/rpc/src/index.ts +206 -0
  162. package/packages/rpc/tsconfig.json +4 -0
  163. package/packages/typescript/base.json +36 -0
  164. package/packages/typescript/nextjs.json +17 -0
  165. package/packages/typescript/package.json +10 -0
  166. package/packages/ui/.turbo/turbo-build.log +44 -0
  167. package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
  168. package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
  169. package/packages/ui/dist/components/badge.d.ts +12 -0
  170. package/packages/ui/dist/components/badge.js +31 -0
  171. package/packages/ui/dist/components/badge.js.map +1 -0
  172. package/packages/ui/dist/components/button.d.ts +13 -0
  173. package/packages/ui/dist/components/button.js +40 -0
  174. package/packages/ui/dist/components/button.js.map +1 -0
  175. package/packages/ui/dist/components/card.d.ts +10 -0
  176. package/packages/ui/dist/components/card.js +39 -0
  177. package/packages/ui/dist/components/card.js.map +1 -0
  178. package/packages/ui/dist/components/input.d.ts +5 -0
  179. package/packages/ui/dist/components/input.js +28 -0
  180. package/packages/ui/dist/components/input.js.map +1 -0
  181. package/packages/ui/dist/components/label.d.ts +5 -0
  182. package/packages/ui/dist/components/label.js +13 -0
  183. package/packages/ui/dist/components/label.js.map +1 -0
  184. package/packages/ui/dist/components/separator.d.ts +5 -0
  185. package/packages/ui/dist/components/separator.js +13 -0
  186. package/packages/ui/dist/components/separator.js.map +1 -0
  187. package/packages/ui/dist/components/textarea.d.ts +5 -0
  188. package/packages/ui/dist/components/textarea.js +27 -0
  189. package/packages/ui/dist/components/textarea.js.map +1 -0
  190. package/packages/ui/dist/tailwind.d.ts +56 -0
  191. package/packages/ui/dist/tailwind.js +60 -0
  192. package/packages/ui/dist/tailwind.js.map +1 -0
  193. package/packages/ui/dist/utils/cn.d.ts +5 -0
  194. package/packages/ui/dist/utils/cn.js +7 -0
  195. package/packages/ui/dist/utils/cn.js.map +1 -0
  196. package/packages/ui/node_modules/.bin/jiti +17 -0
  197. package/packages/ui/node_modules/.bin/tsc +17 -0
  198. package/packages/ui/node_modules/.bin/tsserver +17 -0
  199. package/packages/ui/node_modules/.bin/tsup +17 -0
  200. package/packages/ui/node_modules/.bin/tsup-node +17 -0
  201. package/packages/ui/node_modules/.bin/tsx +17 -0
  202. package/packages/ui/package.json +69 -0
  203. package/packages/ui/src/components/badge.tsx +27 -0
  204. package/packages/ui/src/components/button.tsx +40 -0
  205. package/packages/ui/src/components/card.tsx +31 -0
  206. package/packages/ui/src/components/input.tsx +21 -0
  207. package/packages/ui/src/components/label.tsx +6 -0
  208. package/packages/ui/src/components/separator.tsx +6 -0
  209. package/packages/ui/src/components/textarea.tsx +20 -0
  210. package/packages/ui/src/globals.css +70 -0
  211. package/packages/ui/src/tailwind.ts +56 -0
  212. package/packages/ui/src/utils/cn.ts +6 -0
  213. package/packages/ui/tsconfig.json +20 -0
  214. package/packages/ui/tsup.config.ts +20 -0
  215. package/pnpm-workspace.yaml +4 -0
  216. package/scripts/install-rust-binaries.mjs +84 -0
  217. package/scripts/launchd/install-user-daemon.sh +358 -0
  218. package/scripts/launchd/run-vault-daemon.sh +5 -0
  219. package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
  220. package/scripts/launchd/uninstall-user-daemon.sh +103 -0
  221. package/src/cli.ts +2121 -0
  222. package/src/lib/admin-guard.js +1 -0
  223. package/src/lib/admin-guard.ts +185 -0
  224. package/src/lib/admin-passthrough.ts +33 -0
  225. package/src/lib/admin-reset.ts +751 -0
  226. package/src/lib/admin-setup.ts +1612 -0
  227. package/src/lib/agent-auth-clear.js +1 -0
  228. package/src/lib/agent-auth-clear.ts +58 -0
  229. package/src/lib/agent-auth-forwarding.js +1 -0
  230. package/src/lib/agent-auth-forwarding.ts +149 -0
  231. package/src/lib/agent-auth-migrate.js +1 -0
  232. package/src/lib/agent-auth-migrate.ts +150 -0
  233. package/src/lib/agent-auth-revoke.ts +103 -0
  234. package/src/lib/agent-auth-rotate.ts +107 -0
  235. package/src/lib/agent-auth-token.js +1 -0
  236. package/src/lib/agent-auth-token.ts +25 -0
  237. package/src/lib/agent-auth.ts +89 -0
  238. package/src/lib/asset-broadcast.js +1 -0
  239. package/src/lib/asset-broadcast.ts +285 -0
  240. package/src/lib/bootstrap-artifacts.js +1 -0
  241. package/src/lib/bootstrap-artifacts.ts +205 -0
  242. package/src/lib/bootstrap-credentials.js +1 -0
  243. package/src/lib/bootstrap-credentials.ts +832 -0
  244. package/src/lib/config-amounts.js +1 -0
  245. package/src/lib/config-amounts.ts +189 -0
  246. package/src/lib/config-mutation.ts +27 -0
  247. package/src/lib/fs-trust.js +1 -0
  248. package/src/lib/fs-trust.ts +537 -0
  249. package/src/lib/keychain.js +1 -0
  250. package/src/lib/keychain.ts +225 -0
  251. package/src/lib/local-admin-access.ts +106 -0
  252. package/src/lib/network-selection.js +1 -0
  253. package/src/lib/network-selection.ts +71 -0
  254. package/src/lib/passthrough-security.js +1 -0
  255. package/src/lib/passthrough-security.ts +114 -0
  256. package/src/lib/rpc-guard.js +1 -0
  257. package/src/lib/rpc-guard.ts +7 -0
  258. package/src/lib/rust-spawn-options.js +1 -0
  259. package/src/lib/rust-spawn-options.ts +98 -0
  260. package/src/lib/rust.js +1 -0
  261. package/src/lib/rust.ts +143 -0
  262. package/src/lib/signed-tx.js +1 -0
  263. package/src/lib/signed-tx.ts +116 -0
  264. package/src/lib/status-repair-cli.ts +116 -0
  265. package/src/lib/sudo.js +1 -0
  266. package/src/lib/sudo.ts +172 -0
  267. package/src/lib/vault-password-forwarding.js +1 -0
  268. package/src/lib/vault-password-forwarding.ts +155 -0
  269. package/src/lib/wallet-profile.js +1 -0
  270. package/src/lib/wallet-profile.ts +332 -0
  271. package/src/lib/wallet-repair.js +1 -0
  272. package/src/lib/wallet-repair.ts +304 -0
  273. package/src/lib/wallet-setup.js +1 -0
  274. package/src/lib/wallet-setup.ts +1466 -0
  275. package/src/lib/wallet-status.js +1 -0
  276. package/src/lib/wallet-status.ts +640 -0
  277. package/tsconfig.base.json +17 -0
  278. package/tsconfig.json +10 -0
  279. package/tsup.config.ts +25 -0
  280. package/turbo.json +41 -0
  281. package/LICENSE.md +0 -1
  282. package/dist/wlfa/index.cjs +0 -250
  283. package/dist/wlfa/index.d.cts +0 -1
  284. package/dist/wlfa/index.d.ts +0 -1
  285. package/dist/wlfa/index.js +0 -250
  286. package/dist/wlfc/index.cjs +0 -1894
  287. package/dist/wlfc/index.d.cts +0 -1
  288. package/dist/wlfc/index.d.ts +0 -1
  289. package/dist/wlfc/index.js +0 -1894
@@ -0,0 +1,441 @@
1
+ use std::collections::HashMap;
2
+ use std::fs::OpenOptions;
3
+ use std::io::{Read, Write};
4
+ use std::path::{Path, PathBuf};
5
+
6
+ use argon2::{Argon2, ParamsBuilder};
7
+ use chacha20poly1305::aead::{Aead, KeyInit};
8
+ use chacha20poly1305::XChaCha20Poly1305;
9
+ use serde::{Deserialize, Serialize};
10
+ use time::OffsetDateTime;
11
+ use uuid::Uuid;
12
+ use vault_domain::{
13
+ AgentKey, Lease, ManualApprovalRequest, NonceReservation, RelayConfig, SpendEvent,
14
+ SpendingPolicy, VaultKey,
15
+ };
16
+ use zeroize::{Zeroize, Zeroizing};
17
+
18
+ #[cfg(unix)]
19
+ #[cfg(unix)]
20
+ use std::os::unix::fs::MetadataExt;
21
+ #[cfg(unix)]
22
+ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
23
+
24
+ /// Configuration for encrypted, persistent daemon state storage.
25
+ #[derive(Debug, Clone)]
26
+ pub struct PersistentStoreConfig {
27
+ /// Filesystem path to the encrypted state file.
28
+ pub path: PathBuf,
29
+ }
30
+
31
+ impl PersistentStoreConfig {
32
+ /// Creates a new persistent-store config for `path`.
33
+ pub fn new(path: PathBuf) -> Self {
34
+ Self { path }
35
+ }
36
+ }
37
+
38
+ #[derive(Debug, Clone, Serialize, Deserialize, Default)]
39
+ #[serde(default)]
40
+ pub(crate) struct PersistedDaemonState {
41
+ pub leases: HashMap<Uuid, Lease>,
42
+ pub policies: HashMap<Uuid, SpendingPolicy>,
43
+ pub vault_keys: HashMap<Uuid, VaultKey>,
44
+ pub software_signer_private_keys: HashMap<Uuid, String>,
45
+ pub agent_keys: HashMap<Uuid, AgentKey>,
46
+ pub agent_auth_tokens: HashMap<Uuid, [u8; 32]>,
47
+ pub replay_ids: HashMap<Uuid, OffsetDateTime>,
48
+ pub nonce_heads: HashMap<Uuid, HashMap<u64, u64>>,
49
+ pub nonce_reservations: HashMap<Uuid, NonceReservation>,
50
+ pub spend_log: Vec<SpendEvent>,
51
+ pub manual_approval_requests: HashMap<Uuid, ManualApprovalRequest>,
52
+ pub relay_config: RelayConfig,
53
+ pub relay_private_key_hex: String,
54
+ }
55
+
56
+ #[derive(Debug, Clone, Serialize, Deserialize)]
57
+ struct KdfParams {
58
+ memory_kib: u32,
59
+ time_cost: u32,
60
+ parallelism: u32,
61
+ }
62
+
63
+ #[derive(Debug, Clone, Serialize, Deserialize)]
64
+ struct EncryptedStateEnvelope {
65
+ version: u8,
66
+ kdf: KdfParams,
67
+ salt_hex: String,
68
+ nonce_hex: String,
69
+ ciphertext_hex: String,
70
+ }
71
+
72
+ const ENVELOPE_VERSION: u8 = 1;
73
+ const SALT_LEN: usize = 16;
74
+ const KEY_LEN: usize = 32;
75
+ const NONCE_LEN: usize = 24;
76
+
77
+ pub(crate) struct EncryptedStateStore {
78
+ path: PathBuf,
79
+ key: [u8; KEY_LEN],
80
+ salt: [u8; SALT_LEN],
81
+ kdf: KdfParams,
82
+ }
83
+
84
+ impl Drop for EncryptedStateStore {
85
+ fn drop(&mut self) {
86
+ self.key.zeroize();
87
+ }
88
+ }
89
+
90
+ impl EncryptedStateStore {
91
+ pub(crate) fn open_or_initialize(
92
+ password: &str,
93
+ config: &crate::DaemonConfig,
94
+ store: PersistentStoreConfig,
95
+ ) -> Result<(Self, PersistedDaemonState), String> {
96
+ ensure_secure_path(&store.path)?;
97
+ if store.path.exists() {
98
+ let bytes = read_file_secure(&store.path)?;
99
+ let envelope: EncryptedStateEnvelope = serde_json::from_slice(&bytes)
100
+ .map_err(|err| format!("failed to parse state envelope: {err}"))?;
101
+ if envelope.version != ENVELOPE_VERSION {
102
+ return Err(format!(
103
+ "unsupported state file version {}; expected {}",
104
+ envelope.version, ENVELOPE_VERSION
105
+ ));
106
+ }
107
+ let kdf = envelope.kdf;
108
+ let salt_bytes = hex::decode(&envelope.salt_hex)
109
+ .map_err(|err| format!("invalid state salt encoding: {err}"))?;
110
+ if salt_bytes.len() != SALT_LEN {
111
+ return Err("invalid state salt length".to_string());
112
+ }
113
+ let nonce_bytes = hex::decode(&envelope.nonce_hex)
114
+ .map_err(|err| format!("invalid state nonce encoding: {err}"))?;
115
+ if nonce_bytes.len() != NONCE_LEN {
116
+ return Err("invalid state nonce length".to_string());
117
+ }
118
+ let ciphertext = hex::decode(&envelope.ciphertext_hex)
119
+ .map_err(|err| format!("invalid state ciphertext encoding: {err}"))?;
120
+ let mut salt = [0u8; SALT_LEN];
121
+ salt.copy_from_slice(&salt_bytes);
122
+ let mut nonce = [0u8; NONCE_LEN];
123
+ nonce.copy_from_slice(&nonce_bytes);
124
+ let key = derive_key(password, &salt, &kdf)?;
125
+ let cipher = XChaCha20Poly1305::new((&key).into());
126
+ let plaintext = Zeroizing::new(
127
+ cipher
128
+ .decrypt((&nonce).into(), ciphertext.as_ref())
129
+ .map_err(|_| {
130
+ "failed to decrypt state (wrong password or tampered file)".to_string()
131
+ })?,
132
+ );
133
+ let state: PersistedDaemonState = serde_json::from_slice(&plaintext)
134
+ .map_err(|err| format!("failed to deserialize state payload: {err}"))?;
135
+ Ok((
136
+ Self {
137
+ path: store.path,
138
+ key,
139
+ salt,
140
+ kdf,
141
+ },
142
+ state,
143
+ ))
144
+ } else {
145
+ let kdf = KdfParams {
146
+ memory_kib: config.argon2_memory_kib,
147
+ time_cost: config.argon2_time_cost,
148
+ parallelism: config.argon2_parallelism,
149
+ };
150
+ let salt = rand::random::<[u8; SALT_LEN]>();
151
+ let key = derive_key(password, &salt, &kdf)?;
152
+ Ok((
153
+ Self {
154
+ path: store.path,
155
+ key,
156
+ salt,
157
+ kdf,
158
+ },
159
+ PersistedDaemonState::default(),
160
+ ))
161
+ }
162
+ }
163
+
164
+ pub(crate) fn save(&self, state: &PersistedDaemonState) -> Result<(), String> {
165
+ ensure_secure_path(&self.path)?;
166
+ let plaintext = Zeroizing::new(
167
+ serde_json::to_vec(state)
168
+ .map_err(|err| format!("failed to serialize daemon state: {err}"))?,
169
+ );
170
+ let nonce = rand::random::<[u8; NONCE_LEN]>();
171
+ let cipher = XChaCha20Poly1305::new((&self.key).into());
172
+ let ciphertext = cipher
173
+ .encrypt((&nonce).into(), plaintext.as_ref())
174
+ .map_err(|err| format!("failed to encrypt daemon state: {err}"))?;
175
+ let envelope = EncryptedStateEnvelope {
176
+ version: ENVELOPE_VERSION,
177
+ kdf: self.kdf.clone(),
178
+ salt_hex: hex::encode(self.salt),
179
+ nonce_hex: hex::encode(nonce),
180
+ ciphertext_hex: hex::encode(ciphertext),
181
+ };
182
+ let bytes = serde_json::to_vec(&envelope)
183
+ .map_err(|err| format!("failed to serialize state envelope: {err}"))?;
184
+ atomic_write_secure(&self.path, &bytes)
185
+ }
186
+ }
187
+
188
+ fn derive_key(
189
+ password: &str,
190
+ salt: &[u8; SALT_LEN],
191
+ kdf: &KdfParams,
192
+ ) -> Result<[u8; KEY_LEN], String> {
193
+ let params = ParamsBuilder::new()
194
+ .m_cost(kdf.memory_kib)
195
+ .t_cost(kdf.time_cost)
196
+ .p_cost(kdf.parallelism)
197
+ .build()
198
+ .map_err(|err| format!("invalid state kdf params: {err}"))?;
199
+ let argon = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
200
+ let mut key = [0u8; KEY_LEN];
201
+ argon
202
+ .hash_password_into(password.as_bytes(), salt, &mut key)
203
+ .map_err(|err| format!("failed to derive state key: {err}"))?;
204
+ Ok(key)
205
+ }
206
+
207
+ fn ensure_secure_path(path: &Path) -> Result<(), String> {
208
+ if is_symlink(path)? {
209
+ return Err(format!(
210
+ "state path '{}' must not be a symlink",
211
+ path.display()
212
+ ));
213
+ }
214
+
215
+ if let Some(parent) = path.parent() {
216
+ if !parent.as_os_str().is_empty() {
217
+ std::fs::create_dir_all(parent).map_err(|err| {
218
+ format!(
219
+ "failed to create state directory '{}': {err}",
220
+ parent.display()
221
+ )
222
+ })?;
223
+ if is_symlink(parent)? {
224
+ return Err(format!(
225
+ "state directory '{}' must not be a symlink",
226
+ parent.display()
227
+ ));
228
+ }
229
+ ensure_secure_directory(parent)?;
230
+ }
231
+ }
232
+
233
+ if path.exists() {
234
+ let metadata = std::fs::metadata(path)
235
+ .map_err(|err| format!("failed to inspect state file '{}': {err}", path.display()))?;
236
+ validate_private_state_file(path, &metadata)?;
237
+ }
238
+
239
+ Ok(())
240
+ }
241
+
242
+ #[cfg(unix)]
243
+ fn ensure_secure_directory(path: &Path) -> Result<(), String> {
244
+ const STICKY_BIT_MODE: u32 = 0o1000;
245
+
246
+ fn validate_directory(
247
+ path: &Path,
248
+ metadata: &std::fs::Metadata,
249
+ allow_sticky_group_other_write: bool,
250
+ ) -> Result<(), String> {
251
+ if !metadata.is_dir() {
252
+ return Err(format!(
253
+ "state directory '{}' is not a directory",
254
+ path.display()
255
+ ));
256
+ }
257
+
258
+ validate_root_owned(path, metadata, "state directory")?;
259
+
260
+ let mode = metadata.mode() & 0o7777;
261
+ if mode & 0o022 != 0 && !(allow_sticky_group_other_write && mode & STICKY_BIT_MODE != 0) {
262
+ return Err(format!(
263
+ "state directory '{}' must not be writable by group/other",
264
+ path.display()
265
+ ));
266
+ }
267
+
268
+ Ok(())
269
+ }
270
+
271
+ let metadata = std::fs::metadata(path).map_err(|err| {
272
+ format!(
273
+ "failed to inspect state directory '{}': {err}",
274
+ path.display()
275
+ )
276
+ })?;
277
+ validate_directory(path, &metadata, false)?;
278
+
279
+ let canonical = std::fs::canonicalize(path).map_err(|err| {
280
+ format!(
281
+ "failed to canonicalize state directory '{}': {err}",
282
+ path.display()
283
+ )
284
+ })?;
285
+ for ancestor in canonical.ancestors().skip(1) {
286
+ let metadata = std::fs::metadata(ancestor).map_err(|err| {
287
+ format!(
288
+ "failed to inspect ancestor state directory '{}': {err}",
289
+ ancestor.display()
290
+ )
291
+ })?;
292
+ validate_directory(ancestor, &metadata, true)?;
293
+ }
294
+
295
+ Ok(())
296
+ }
297
+
298
+ #[cfg(not(unix))]
299
+ fn ensure_secure_directory(_path: &Path) -> Result<(), String> {
300
+ Ok(())
301
+ }
302
+
303
+ #[cfg(unix)]
304
+ fn validate_root_owned(
305
+ path: &Path,
306
+ metadata: &std::fs::Metadata,
307
+ label: &str,
308
+ ) -> Result<(), String> {
309
+ let uid = metadata.uid();
310
+ if uid == 0 {
311
+ return Ok(());
312
+ }
313
+
314
+ Err(format!(
315
+ "{label} '{}' must be owned by root; found uid {uid}",
316
+ path.display()
317
+ ))
318
+ }
319
+
320
+ #[cfg(not(unix))]
321
+ fn validate_root_owned(
322
+ _path: &Path,
323
+ _metadata: &std::fs::Metadata,
324
+ _label: &str,
325
+ ) -> Result<(), String> {
326
+ Ok(())
327
+ }
328
+
329
+ fn validate_private_state_file(path: &Path, metadata: &std::fs::Metadata) -> Result<(), String> {
330
+ if !metadata.is_file() {
331
+ return Err(format!(
332
+ "state file '{}' must be a regular file",
333
+ path.display()
334
+ ));
335
+ }
336
+
337
+ validate_root_owned(path, metadata, "state file")?;
338
+
339
+ #[cfg(unix)]
340
+ {
341
+ if metadata.mode() & 0o077 != 0 {
342
+ return Err(format!(
343
+ "state file '{}' must not grant group/other permissions",
344
+ path.display()
345
+ ));
346
+ }
347
+ }
348
+
349
+ Ok(())
350
+ }
351
+
352
+ fn is_symlink(path: &Path) -> Result<bool, String> {
353
+ match std::fs::symlink_metadata(path) {
354
+ Ok(metadata) => Ok(metadata.file_type().is_symlink()),
355
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
356
+ Err(err) => Err(format!(
357
+ "failed to inspect metadata for '{}': {err}",
358
+ path.display()
359
+ )),
360
+ }
361
+ }
362
+
363
+ fn read_file_secure(path: &Path) -> Result<Vec<u8>, String> {
364
+ let mut options = OpenOptions::new();
365
+ options.read(true);
366
+ #[cfg(unix)]
367
+ {
368
+ options.custom_flags(libc::O_NOFOLLOW);
369
+ }
370
+ let mut file = options
371
+ .open(path)
372
+ .map_err(|err| format!("failed to open state file '{}': {err}", path.display()))?;
373
+ let metadata = file
374
+ .metadata()
375
+ .map_err(|err| format!("failed to inspect state file '{}': {err}", path.display()))?;
376
+ validate_private_state_file(path, &metadata)?;
377
+ let mut bytes = Vec::new();
378
+ file.read_to_end(&mut bytes)
379
+ .map_err(|err| format!("failed to read state file '{}': {err}", path.display()))?;
380
+ Ok(bytes)
381
+ }
382
+
383
+ fn atomic_write_secure(path: &Path, bytes: &[u8]) -> Result<(), String> {
384
+ let temp_name = format!(
385
+ ".{}.tmp.{}",
386
+ path.file_name()
387
+ .and_then(|value| value.to_str())
388
+ .unwrap_or("wlfi-state"),
389
+ Uuid::new_v4().simple()
390
+ );
391
+ let temp_path = path.with_file_name(temp_name);
392
+
393
+ let mut options = OpenOptions::new();
394
+ options.write(true).create_new(true).truncate(true);
395
+ #[cfg(unix)]
396
+ {
397
+ options.mode(0o600);
398
+ options.custom_flags(libc::O_NOFOLLOW);
399
+ }
400
+
401
+ let mut file = options.open(&temp_path).map_err(|err| {
402
+ format!(
403
+ "failed to create temp state file '{}': {err}",
404
+ temp_path.display()
405
+ )
406
+ })?;
407
+ file.write_all(bytes).map_err(|err| {
408
+ format!(
409
+ "failed to write temp state file '{}': {err}",
410
+ temp_path.display()
411
+ )
412
+ })?;
413
+ file.sync_all().map_err(|err| {
414
+ format!(
415
+ "failed to sync temp state file '{}': {err}",
416
+ temp_path.display()
417
+ )
418
+ })?;
419
+ drop(file);
420
+
421
+ #[cfg(unix)]
422
+ {
423
+ std::fs::set_permissions(&temp_path, std::fs::Permissions::from_mode(0o600)).map_err(
424
+ |err| {
425
+ format!(
426
+ "failed to set state file permissions '{}': {err}",
427
+ temp_path.display()
428
+ )
429
+ },
430
+ )?;
431
+ }
432
+
433
+ std::fs::rename(&temp_path, path).map_err(|err| {
434
+ format!(
435
+ "failed to atomically replace state file '{}' from '{}': {err}",
436
+ path.display(),
437
+ temp_path.display()
438
+ )
439
+ })?;
440
+ Ok(())
441
+ }
@@ -0,0 +1,237 @@
1
+ use std::collections::BTreeSet;
2
+ use std::sync::Arc;
3
+ use std::time::{SystemTime, UNIX_EPOCH};
4
+
5
+ use alloy_primitives::keccak256;
6
+ use k256::ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey};
7
+ use serde_json::to_vec;
8
+ use uuid::Uuid;
9
+ use vault_domain::{
10
+ AgentAction, AgentCredentials, AssetId, BroadcastTx, EntityScope, EvmAddress, KeySource, Lease,
11
+ NonceReleaseRequest, NonceReservationRequest, PolicyAttachment, PolicyType, SignRequest,
12
+ SpendingPolicy,
13
+ };
14
+ use vault_signer::SoftwareSignerBackend;
15
+
16
+ use super::{
17
+ validate_loaded_state, AdminSession, DaemonConfig, DaemonError, DaemonRpcRequest,
18
+ DaemonRpcResponse, InMemoryDaemon, KeyCreateRequest, KeyManagerDaemonApi,
19
+ PersistentStoreConfig, PolicyError,
20
+ };
21
+
22
+ fn policy_all_per_tx(max: u128) -> SpendingPolicy {
23
+ SpendingPolicy::new(
24
+ 1,
25
+ PolicyType::PerTxMaxSpending,
26
+ max,
27
+ EntityScope::All,
28
+ EntityScope::All,
29
+ EntityScope::All,
30
+ )
31
+ .expect("policy")
32
+ }
33
+
34
+ fn policy_per_chain_gas(chain_id: u64, max_gas_wei: u128) -> SpendingPolicy {
35
+ SpendingPolicy::new(
36
+ 2,
37
+ PolicyType::PerChainMaxGasSpend,
38
+ max_gas_wei,
39
+ EntityScope::All,
40
+ EntityScope::All,
41
+ EntityScope::Set(BTreeSet::from([chain_id])),
42
+ )
43
+ .expect("policy")
44
+ }
45
+
46
+ fn sign_request(credentials: &AgentCredentials, action: AgentAction) -> SignRequest {
47
+ let now = time::OffsetDateTime::now_utc();
48
+ SignRequest {
49
+ request_id: Uuid::new_v4(),
50
+ agent_key_id: credentials.agent_key.id,
51
+ agent_auth_token: credentials.auth_token.clone(),
52
+ payload: to_vec(&action).expect("action payload"),
53
+ action,
54
+ requested_at: now,
55
+ expires_at: now + time::Duration::minutes(2),
56
+ }
57
+ }
58
+
59
+ async fn reserve_nonce_for_agent(
60
+ daemon: &InMemoryDaemon<SoftwareSignerBackend>,
61
+ credentials: &AgentCredentials,
62
+ chain_id: u64,
63
+ nonce: u64,
64
+ ) {
65
+ let now = time::OffsetDateTime::now_utc();
66
+ daemon
67
+ .reserve_nonce(NonceReservationRequest {
68
+ request_id: Uuid::new_v4(),
69
+ agent_key_id: credentials.agent_key.id,
70
+ agent_auth_token: credentials.auth_token.clone(),
71
+ chain_id,
72
+ min_nonce: nonce,
73
+ requested_at: now,
74
+ expires_at: now + time::Duration::minutes(2),
75
+ })
76
+ .await
77
+ .expect("nonce reservation");
78
+ }
79
+
80
+ fn unique_state_path(test_name: &str) -> std::path::PathBuf {
81
+ let unique = SystemTime::now()
82
+ .duration_since(UNIX_EPOCH)
83
+ .expect("time")
84
+ .as_nanos();
85
+ std::env::temp_dir().join(format!(
86
+ "wlfi-daemon-{test_name}-{}-{}.state",
87
+ std::process::id(),
88
+ unique
89
+ ))
90
+ }
91
+
92
+ #[test]
93
+ fn daemon_rpc_request_debug_redacts_vault_password() {
94
+ let rendered = format!(
95
+ "{:?}",
96
+ DaemonRpcRequest::IssueLease {
97
+ vault_password: "super-secret-password".to_string(),
98
+ }
99
+ );
100
+
101
+ assert!(rendered.contains("<redacted>"));
102
+ assert!(!rendered.contains("super-secret-password"));
103
+ }
104
+
105
+ #[test]
106
+ fn daemon_rpc_response_debug_redacts_auth_token() {
107
+ let rendered = format!(
108
+ "{:?}",
109
+ DaemonRpcResponse::AuthToken("super-secret-token".to_string())
110
+ );
111
+
112
+ assert!(rendered.contains("<redacted>"));
113
+ assert!(!rendered.contains("super-secret-token"));
114
+ }
115
+
116
+ #[test]
117
+ fn daemon_rpc_request_zeroize_secrets_clears_nested_secret_material() {
118
+ let now = time::OffsetDateTime::now_utc();
119
+ let mut admin_request = DaemonRpcRequest::CreateAgentKey {
120
+ session: AdminSession {
121
+ vault_password: "super-secret-password".to_string(),
122
+ lease: Lease {
123
+ lease_id: Uuid::new_v4(),
124
+ issued_at: now,
125
+ expires_at: now + time::Duration::minutes(1),
126
+ },
127
+ },
128
+ vault_key_id: Uuid::new_v4(),
129
+ attachment: PolicyAttachment::AllPolicies,
130
+ };
131
+ admin_request.zeroize_secrets();
132
+ match &admin_request {
133
+ DaemonRpcRequest::CreateAgentKey { session, .. } => {
134
+ assert!(session
135
+ .vault_password
136
+ .as_bytes()
137
+ .iter()
138
+ .all(|byte| *byte == 0));
139
+ }
140
+ other => panic!("unexpected request variant: {other:?}"),
141
+ }
142
+
143
+ let mut sign_request = DaemonRpcRequest::SignForAgent {
144
+ request: SignRequest {
145
+ request_id: Uuid::new_v4(),
146
+ agent_key_id: Uuid::new_v4(),
147
+ agent_auth_token: "agent-secret-token".to_string(),
148
+ payload: vec![1, 2, 3, 4],
149
+ action: AgentAction::Approve {
150
+ token: "0x1111111111111111111111111111111111111111"
151
+ .parse()
152
+ .expect("token"),
153
+ spender: "0x2222222222222222222222222222222222222222"
154
+ .parse()
155
+ .expect("spender"),
156
+ amount_wei: 42,
157
+ chain_id: 1,
158
+ },
159
+ requested_at: now,
160
+ expires_at: now + time::Duration::minutes(2),
161
+ },
162
+ };
163
+ sign_request.zeroize_secrets();
164
+ match &sign_request {
165
+ DaemonRpcRequest::SignForAgent { request } => {
166
+ assert!(request
167
+ .agent_auth_token
168
+ .as_bytes()
169
+ .iter()
170
+ .all(|byte| *byte == 0));
171
+ assert!(request.payload.iter().all(|byte| *byte == 0));
172
+ }
173
+ other => panic!("unexpected request variant: {other:?}"),
174
+ }
175
+
176
+ let mut reserve_nonce = DaemonRpcRequest::ReserveNonce {
177
+ request: NonceReservationRequest {
178
+ request_id: Uuid::new_v4(),
179
+ agent_key_id: Uuid::new_v4(),
180
+ agent_auth_token: "nonce-secret".to_string(),
181
+ chain_id: 1,
182
+ min_nonce: 7,
183
+ requested_at: now,
184
+ expires_at: now + time::Duration::minutes(2),
185
+ },
186
+ };
187
+ reserve_nonce.zeroize_secrets();
188
+ match &reserve_nonce {
189
+ DaemonRpcRequest::ReserveNonce { request } => {
190
+ assert!(request
191
+ .agent_auth_token
192
+ .as_bytes()
193
+ .iter()
194
+ .all(|byte| *byte == 0));
195
+ }
196
+ other => panic!("unexpected request variant: {other:?}"),
197
+ }
198
+ }
199
+
200
+ #[test]
201
+ fn daemon_rpc_response_zeroize_secrets_clears_auth_tokens() {
202
+ let now = time::OffsetDateTime::now_utc();
203
+ let mut response = DaemonRpcResponse::AgentCredentials(AgentCredentials {
204
+ agent_key: vault_domain::AgentKey {
205
+ id: Uuid::new_v4(),
206
+ vault_key_id: Uuid::new_v4(),
207
+ policies: PolicyAttachment::AllPolicies,
208
+ created_at: now,
209
+ },
210
+ auth_token: "agent-secret-token".to_string(),
211
+ });
212
+ response.zeroize_secrets();
213
+ match &response {
214
+ DaemonRpcResponse::AgentCredentials(credentials) => {
215
+ assert!(credentials
216
+ .auth_token
217
+ .as_bytes()
218
+ .iter()
219
+ .all(|byte| *byte == 0));
220
+ }
221
+ other => panic!("unexpected response variant: {other:?}"),
222
+ }
223
+
224
+ let mut rotated = DaemonRpcResponse::AuthToken("rotated-secret-token".to_string());
225
+ rotated.zeroize_secrets();
226
+ match &rotated {
227
+ DaemonRpcResponse::AuthToken(token) => {
228
+ assert!(token.as_bytes().iter().all(|byte| *byte == 0));
229
+ }
230
+ other => panic!("unexpected response variant: {other:?}"),
231
+ }
232
+ }
233
+
234
+ include!("tests_parts/part1.rs");
235
+ include!("tests_parts/part2.rs");
236
+ include!("tests_parts/part3.rs");
237
+ include!("tests_parts/part4.rs");