file_adapter.rs (7415B)
1 #![cfg(feature = "file")] 2 3 use futures_executor::block_on; 4 use radroots_secrets::context::{ 5 EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, 6 }; 7 use radroots_secrets::envelope::Nonce; 8 use radroots_secrets::file::{FileOpenMode, FileProvider}; 9 use radroots_secrets::id::{BackendKind, KeyVersion}; 10 use radroots_secrets::wrapping::{SecretMaterial, UnwrapRequest, WrapRequest}; 11 use radroots_secrets::{Error, KeyWrapping, SecretId, SecretProvider, SecretRef}; 12 use std::fs; 13 14 fn reference(id: &str, version: u32) -> SecretRef { 15 SecretRef::new( 16 SecretId::parse(id).expect("valid id"), 17 BackendKind::File, 18 KeyVersion::new(version).expect("valid version"), 19 ) 20 } 21 22 fn context() -> EnvelopeContext { 23 EnvelopeContext::new( 24 EnvelopePurpose::parse("radroots.file_test").expect("purpose"), 25 EnvelopeSubject::parse("file_test", "fixture").expect("subject"), 26 PayloadSchemaId::parse("radroots.file_test.v1").expect("schema"), 27 ) 28 } 29 30 fn different_context() -> EnvelopeContext { 31 EnvelopeContext::new( 32 EnvelopePurpose::parse("radroots.file_test").expect("purpose"), 33 EnvelopeSubject::parse("file_test", "different").expect("subject"), 34 PayloadSchemaId::parse("radroots.file_test.v1").expect("schema"), 35 ) 36 } 37 38 fn provider(root: &std::path::Path, mode: FileOpenMode) -> FileProvider { 39 FileProvider::open( 40 root, 41 mode, 42 SecretMaterial::from_slice(&[0xA5; 32]).expect("master key"), 43 ) 44 .expect("open provider") 45 } 46 47 #[test] 48 fn file_provider_round_trips_encrypted_material_with_atomic_creation() { 49 let temp = tempfile::tempdir().expect("tempdir"); 50 let root = temp.path().join("vault"); 51 let provider = provider(&root, FileOpenMode::CreateNew); 52 let reference = reference("file-key", 1); 53 let material = SecretMaterial::from_slice(&[0x41; 32]).expect("material"); 54 provider 55 .provision(&reference, &material, Nonce::new([0x11; 24])) 56 .expect("provision"); 57 58 let persisted = fs::read(root.join("66696c652d6b6579.v1.rrk")).expect("read entry"); 59 assert!(!persisted.windows(32).any(|window| window == [0x41; 32])); 60 assert_eq!(fs::read_dir(&root).expect("read root").count(), 1); 61 62 let context = context(); 63 let wrapped = 64 block_on(provider.wrap(WrapRequest::new(&reference, &context, &material))).expect("wrap"); 65 let opened = block_on(provider.unwrap(UnwrapRequest::new(&reference, &context, &wrapped))) 66 .expect("unwrap"); 67 opened.expose_secret(|bytes| assert_eq!(bytes, &[0x41; 32])); 68 assert_eq!(provider.backend_kind(), BackendKind::File); 69 assert_eq!(format!("{provider:?}"), "FileProvider(<redacted>)"); 70 } 71 72 #[test] 73 fn traversal_existing_replacement_and_truncated_files_fail_closed() { 74 let temp = tempfile::tempdir().expect("tempdir"); 75 let traversal = temp.path().join("nested/../vault"); 76 assert!(matches!( 77 FileProvider::open( 78 &traversal, 79 FileOpenMode::CreateNew, 80 SecretMaterial::from_slice(&[0xA5; 32]).expect("master key"), 81 ), 82 Err(Error::UnsafePath) 83 )); 84 85 let root = temp.path().join("safe-vault"); 86 let provider = provider(&root, FileOpenMode::CreateNew); 87 let reference = reference("file-key", 1); 88 let material = SecretMaterial::from_slice(&[0x41; 32]).expect("material"); 89 provider 90 .provision(&reference, &material, Nonce::new([0x11; 24])) 91 .expect("provision"); 92 assert!(matches!( 93 provider.provision(&reference, &material, Nonce::new([0x12; 24])), 94 Err(Error::SecretAlreadyExists { .. }) 95 )); 96 97 fs::write(root.join("66696c652d6b6579.v1.rrk"), b"truncated").expect("truncate"); 98 assert!(matches!( 99 block_on(provider.wrap(WrapRequest::new(&reference, &context(), &material))), 100 Err(Error::EnvelopeMalformed) 101 )); 102 } 103 104 #[test] 105 fn rotation_resumes_after_new_version_commit_and_removes_old_version() { 106 let temp = tempfile::tempdir().expect("tempdir"); 107 let root = temp.path().join("vault"); 108 let provider = provider(&root, FileOpenMode::CreateNew); 109 let current = reference("rotating-key", 1); 110 let next = reference("rotating-key", 2); 111 let current_material = SecretMaterial::from_slice(&[0x11; 32]).expect("material"); 112 let next_material = SecretMaterial::from_slice(&[0x22; 32]).expect("material"); 113 provider 114 .provision(¤t, ¤t_material, Nonce::new([0x31; 24])) 115 .expect("current"); 116 117 // Simulate interruption after the new revision commits but before old cleanup. 118 provider 119 .provision(&next, &next_material, Nonce::new([0x32; 24])) 120 .expect("next commit"); 121 provider 122 .rotate(¤t, &next, &next_material, Nonce::new([0x32; 24])) 123 .expect("resume rotation"); 124 assert!(!provider.contains(¤t).expect("old contains")); 125 assert!(provider.contains(&next).expect("new contains")); 126 127 let context = context(); 128 let wrapped = 129 block_on(provider.wrap(WrapRequest::new(&next, &context, &next_material))).expect("wrap"); 130 assert!(block_on(provider.unwrap(UnwrapRequest::new(&next, &context, &wrapped))).is_ok()); 131 } 132 133 #[test] 134 fn wrapped_tokens_are_bound_to_the_exact_context() { 135 let temp = tempfile::tempdir().expect("tempdir"); 136 let provider = provider(&temp.path().join("vault"), FileOpenMode::CreateNew); 137 let reference = reference("context-key", 1); 138 let material = SecretMaterial::from_slice(&[0x41; 32]).expect("material"); 139 provider 140 .provision(&reference, &material, Nonce::new([0x11; 24])) 141 .expect("provision"); 142 let wrapped = 143 block_on(provider.wrap(WrapRequest::new(&reference, &context(), &material))).expect("wrap"); 144 assert!(matches!( 145 block_on(provider.unwrap(UnwrapRequest::new( 146 &reference, 147 &different_context(), 148 &wrapped, 149 ))), 150 Err(Error::BackendFailure { 151 backend: BackendKind::File, 152 .. 153 }) 154 )); 155 } 156 157 #[cfg(unix)] 158 #[test] 159 fn symlink_entries_and_insecure_permissions_are_rejected() { 160 use std::os::unix::fs::{PermissionsExt, symlink}; 161 162 let temp = tempfile::tempdir().expect("tempdir"); 163 let insecure = temp.path().join("insecure"); 164 fs::create_dir(&insecure).expect("create insecure root"); 165 fs::set_permissions(&insecure, fs::Permissions::from_mode(0o755)).expect("chmod"); 166 assert!(matches!( 167 FileProvider::open( 168 &insecure, 169 FileOpenMode::OpenExisting, 170 SecretMaterial::from_slice(&[0xA5; 32]).expect("master key"), 171 ), 172 Err(Error::InsecurePermissions) 173 )); 174 175 let root = temp.path().join("vault"); 176 let provider = provider(&root, FileOpenMode::CreateNew); 177 let outside = temp.path().join("outside"); 178 fs::write(&outside, b"not a secret entry").expect("outside file"); 179 symlink(&outside, root.join("6c696e6b65642d6b6579.v1.rrk")).expect("symlink"); 180 let linked = reference("linked-key", 1); 181 assert!(matches!(provider.contains(&linked), Err(Error::UnsafePath))); 182 183 let linked_root = temp.path().join("linked-root"); 184 symlink(&root, &linked_root).expect("root symlink"); 185 assert!(matches!( 186 FileProvider::open( 187 &linked_root, 188 FileOpenMode::OpenExisting, 189 SecretMaterial::from_slice(&[0xA5; 32]).expect("master key"), 190 ), 191 Err(Error::UnsafePath) 192 )); 193 }