revault_lockbox_api 0.0.36

reVault lockbox API to create and manage lockboxes
Documentation
use revault_lockbox_api::{
    Compression, Encryption, EncryptionMode, Lockbox, LockboxCreateOptions, LockboxOpen,
    LockboxPath, LockboxProtection, OwnerSigningKeyPair, SecretString, Signing, SigningMode,
    ZstdLevel,
};

#[test]
fn independent_modes_survive_reopen_and_mutation() {
    let password = SecretString::try_from_slice(b"creation options test password").unwrap();
    let signer = OwnerSigningKeyPair::generate().unwrap();
    let path = LockboxPath::new("/notes.txt").unwrap();
    let payload = b"A compressible line of text.\n".repeat(10_000);
    for encrypted in [false, true] {
        for signed in [false, true] {
            for compression in [
                Compression::None,
                Compression::Zstd {
                    level: ZstdLevel::new(6).unwrap(),
                },
            ] {
                let encryption = if encrypted {
                    Encryption::Encrypted(LockboxProtection::Password(&password))
                } else {
                    Encryption::None
                };
                let signing = if signed {
                    Signing::Owner(&signer)
                } else {
                    Signing::None
                };
                let mut lb = Lockbox::create_in_memory_with_options(LockboxCreateOptions {
                    compression,
                    ..LockboxCreateOptions::new(encryption, signing)
                })
                .unwrap();
                lb.add_file(&path, &payload, false).unwrap();
                let variable = revault_lockbox_api::VariableName::new("test_variable").unwrap();
                lb.set_variable(&variable, "normal value").unwrap();
                let secret_name = revault_lockbox_api::VariableName::new("test_secret").unwrap();
                lb.set_secret_variable(&secret_name, &password).unwrap();
                lb.commit().unwrap();
                let bytes = lb.try_to_bytes().unwrap();
                let open = || {
                    if encrypted {
                        LockboxOpen::Password(&password)
                    } else {
                        LockboxOpen::Unencrypted
                    }
                };
                if signed {
                    assert!(
                        Lockbox::open_bytes_for_write(bytes.clone(), open(), Signing::None)
                            .is_err()
                    );
                }
                let reopened = Lockbox::open_bytes(bytes.clone(), open()).unwrap();
                assert_eq!(
                    reopened
                        .read_file_range(&path, 0, payload.len() as u64)
                        .unwrap(),
                    payload
                );
                assert_eq!(reopened.format_options().compression, compression);
                assert_eq!(
                    reopened.format_options().encryption,
                    if encrypted {
                        EncryptionMode::ChaCha20Poly1305
                    } else {
                        EncryptionMode::None
                    }
                );
                assert_eq!(
                    reopened.format_options().signing,
                    if signed {
                        SigningMode::Owner
                    } else {
                        SigningMode::None
                    }
                );
                assert_eq!(reopened.owner_inspection().unwrap().signed, signed);
                assert_eq!(
                    reopened.get_variable(&variable).unwrap().as_deref(),
                    Some("normal value")
                );
                if !encrypted && compression == Compression::None {
                    let marker = b"A compressible line of text.\n";
                    assert!(bytes.windows(marker.len()).any(|window| window == marker));
                }
                if encrypted {
                    let marker = b"A compressible line of text.\n";
                    assert!(!bytes.windows(marker.len()).any(|window| window == marker));
                }
                let mut writer = Lockbox::open_bytes_for_write(bytes, open(), signing).unwrap();
                writer.add_file(&path, b"replacement", true).unwrap();
                writer.commit().unwrap();
                let reopened = Lockbox::open_bytes(writer.try_to_bytes().unwrap(), open()).unwrap();
                assert_eq!(
                    reopened.read_file_range(&path, 0, 11).unwrap(),
                    b"replacement"
                );
            }
        }
    }
}

#[test]
fn levels_are_validated_and_encrypted_archives_refuse_credential_free_open() {
    assert!(ZstdLevel::new(0).is_err());
    assert!(ZstdLevel::new(23).is_err());
    assert!(ZstdLevel::new(22).is_ok());
    let password = SecretString::try_from_slice(b"password").unwrap();
    let lb = Lockbox::create_in_memory_with_options(LockboxCreateOptions::new(
        Encryption::Encrypted(LockboxProtection::Password(&password)),
        Signing::None,
    ))
    .unwrap();
    assert!(Lockbox::open_bytes(lb.try_to_bytes().unwrap(), LockboxOpen::Unencrypted).is_err());
}

#[test]
fn numeric_zstd_levels_round_trip_and_compression_reduces_storage() {
    let path = LockboxPath::new("/repeated.txt").unwrap();
    let payload = b"An easily compressed sentence repeated across the file.\n".repeat(1000);
    let mut raw = Lockbox::create_in_memory_with_options(LockboxCreateOptions {
        compression: Compression::None,
        ..LockboxCreateOptions::new(Encryption::None, Signing::None)
    })
    .unwrap();
    raw.add_file(&path, &payload, false).unwrap();
    raw.commit().unwrap();
    let uncompressed_size = raw.try_to_bytes().unwrap().len();
    for level in [1, 3, 9, 19, 22] {
        let mut lb = Lockbox::create_in_memory_with_options(LockboxCreateOptions {
            compression: Compression::Zstd {
                level: ZstdLevel::new(level).unwrap(),
            },
            ..LockboxCreateOptions::new(Encryption::None, Signing::None)
        })
        .unwrap();
        lb.add_file(&path, &payload, false).unwrap();
        lb.commit().unwrap();
        let bytes = lb.try_to_bytes().unwrap();
        assert!(bytes.len() < uncompressed_size, "level {level}");
        let reopened = Lockbox::open_bytes(bytes, LockboxOpen::Unencrypted).unwrap();
        assert_eq!(reopened.get_file(&path).unwrap(), payload, "level {level}");
    }
}