lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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(&current, &current_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(&current, &next, &next_material, Nonce::new([0x32; 24]))
    123         .expect("resume rotation");
    124     assert!(!provider.contains(&current).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 }