im_core/internal/secret_vault/
mod.rs1mod crypto;
2pub(crate) mod policy;
3pub(crate) mod record;
4mod store;
5
6use crate::internal::platform_secret::{DeviceVaultRootKey, SecretBytes};
7
8pub use self::policy::SecretAccessPolicy;
9pub use self::record::{SecretKind, SecretMetadata, SecretRef};
10pub use self::store::FileSecretVaultStore;
11
12pub struct SealSecretRequest {
13 pub metadata: SecretMetadata,
14 pub plaintext: SecretBytes,
15}
16
17pub trait SecretVault {
18 fn seal(&self, request: SealSecretRequest) -> crate::ImResult<SecretRef>;
19
20 fn open(&self, secret_ref: &SecretRef) -> crate::ImResult<SecretBytes>;
21
22 fn delete(&self, secret_ref: &SecretRef) -> crate::ImResult<()>;
23
24 fn list(&self) -> crate::ImResult<Vec<SecretRef>>;
25}
26
27#[derive(Debug)]
28pub struct FileSecretVault {
29 root_key: DeviceVaultRootKey,
30 store: FileSecretVaultStore,
31}
32
33impl FileSecretVault {
34 pub fn new(root_key: DeviceVaultRootKey, store: FileSecretVaultStore) -> Self {
35 Self { root_key, store }
36 }
37
38 pub fn store(&self) -> &FileSecretVaultStore {
39 &self.store
40 }
41}
42
43impl SecretVault for FileSecretVault {
44 fn seal(&self, request: SealSecretRequest) -> crate::ImResult<SecretRef> {
45 let record = crypto::seal_record(&self.root_key, request.metadata, &request.plaintext)?;
46 self.store.put(&record)
47 }
48
49 fn open(&self, secret_ref: &SecretRef) -> crate::ImResult<SecretBytes> {
50 let record = self.store.get(secret_ref)?;
51 crypto::open_record(&self.root_key, &record)
52 }
53
54 fn delete(&self, secret_ref: &SecretRef) -> crate::ImResult<()> {
55 self.store.delete(secret_ref)
56 }
57
58 fn list(&self) -> crate::ImResult<Vec<SecretRef>> {
59 self.store.list()
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::internal::platform_secret::DeviceVaultRootKey;
67 use crate::internal::secret_vault::policy::SecretAccessPolicy;
68 use crate::internal::secret_vault::record::{SecretKind, VaultSecretRecord};
69 use std::fs;
70
71 #[test]
72 fn secret_vault_file_store_seals_opens_lists_and_deletes_secret() {
73 let root = tempfile::tempdir().unwrap();
74 let vault = test_vault(root.path().join("vault"), [3_u8; 32]);
75 let metadata = test_metadata("workspace-a", "device-a");
76
77 let secret_ref = vault
78 .seal(SealSecretRequest {
79 metadata,
80 plaintext: SecretBytes::from_vec(b"private-key-pem".to_vec()),
81 })
82 .unwrap();
83 let opened = vault.open(&secret_ref).unwrap();
84
85 assert_eq!(opened.expose_secret(), b"private-key-pem");
86 assert_eq!(vault.list().unwrap(), vec![secret_ref.clone()]);
87 vault.delete(&secret_ref).unwrap();
88 assert!(vault.list().unwrap().is_empty());
89 }
90
91 #[test]
92 fn secret_vault_file_store_rejects_wrong_device_metadata_tamper() {
93 let root = tempfile::tempdir().unwrap();
94 let vault = test_vault(root.path().join("vault"), [3_u8; 32]);
95 let secret_ref = vault
96 .seal(SealSecretRequest {
97 metadata: test_metadata("workspace-a", "device-a"),
98 plaintext: SecretBytes::from_vec(b"private-key-pem".to_vec()),
99 })
100 .unwrap();
101 let path = vault.store().record_path(&secret_ref);
102 let mut record: VaultSecretRecord =
103 serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
104 record.device_id = "device-b".to_owned();
105 fs::write(&path, serde_json::to_vec_pretty(&record).unwrap()).unwrap();
106
107 let err = vault.open(&secret_ref).unwrap_err();
108
109 assert_eq!(err, crate::ImError::PermissionDenied);
110 }
111
112 #[test]
113 fn secret_vault_debug_redacts_record_secret_fields() {
114 let root_key = DeviceVaultRootKey::from_bytes([3_u8; 32]);
115 let metadata = test_metadata("workspace-a", "device-a");
116 let plaintext = SecretBytes::from_vec(b"debug-private-key".to_vec());
117 let record = crypto::seal_record(&root_key, metadata, &plaintext).unwrap();
118
119 let debug = format!("{record:?}");
120
121 assert!(!debug.contains(&record.ciphertext_b64u));
122 assert!(!debug.contains(&record.nonce_b64u));
123 assert!(!debug.contains("debug-private-key"));
124 assert!(debug.contains("[REDACTED]"));
125 }
126
127 fn test_vault(path: std::path::PathBuf, key: [u8; 32]) -> FileSecretVault {
128 FileSecretVault::new(
129 DeviceVaultRootKey::from_bytes(key),
130 FileSecretVaultStore::new(path),
131 )
132 }
133
134 fn test_metadata(workspace_id: &str, device_id: &str) -> SecretMetadata {
135 SecretMetadata {
136 workspace_id: workspace_id.to_owned(),
137 device_id: device_id.to_owned(),
138 identity_id: Some("identity-a".to_owned()),
139 did: Some("did:wba:alice@example.com".to_owned()),
140 kind: SecretKind::IdentityRootPrivate,
141 key_id: "key-1".to_owned(),
142 key_version: 1,
143 policy: SecretAccessPolicy::no_prompt_local_secret(),
144 }
145 }
146}