armdb 0.11.2

sharded bitcask key-value storage optimized for NVMe
Documentation
//! Encryption coverage for caller-configured variable queue payloads.

use std::sync::Arc;

use armdb::armour::Db;
use armdb::{DbError, FixedConfig, QueueConfig, TxConfig, queue};
use armour_core::GetType;
use rapira::Rapira;

const DATA_KEY: [u8; 32] = [0x31; 32];
const LOG_KEY: [u8; 32] = [0x32; 32];

#[derive(Clone, Debug, PartialEq, Eq, GetType, Rapira)]
pub struct SecretJob {
    body: String,
}

queue!(
    EncryptedJobs,
    payload = SecretJob,
    name = "queue_encrypted_jobs",
    var
);

fn payload_config() -> armdb::Config {
    armdb::Config::balanced()
        .shard_count(1)
        .encryption_key(DATA_KEY)
        .build()
}

fn queue_config() -> QueueConfig {
    let config = QueueConfig::balanced()
        // Queue scans must stay ascending even if a caller supplies a descending
        // FixedStore preset for the header collection.
        .header_config(FixedConfig::balanced().reversed(true).grow_step(64).build())
        .payload_config(payload_config());
    let header = config.fixed_config();
    assert!(!header.reversed);
    assert_eq!(header.grow_step, 64);
    config.build().unwrap()
}

#[test]
fn encrypted_var_queue_direct_and_transactional_operations_require_the_right_keys() {
    let dir = tempfile::tempdir().unwrap();

    let db = Arc::new(
        Db::builder(dir.path())
            .for_tests()
            .tx_config(TxConfig {
                log_encryption_key: Some(LOG_KEY),
                ..TxConfig::default()
            })
            .open()
            .unwrap(),
    );
    let queue = db.open_queue::<EncryptedJobs>(queue_config()).unwrap();

    queue
        .push(
            SecretJob {
                body: "first".into(),
            },
            None,
        )
        .unwrap();
    queue
        .push(
            SecretJob {
                body: "second".into(),
            },
            None,
        )
        .unwrap();
    let direct = queue.lease(1, None).unwrap().pop().unwrap();
    assert_eq!(direct.payload().body, "first");
    queue.ack(direct.token()).unwrap();
    let direct = queue.lease(1, None).unwrap().pop().unwrap();
    assert_eq!(direct.payload().body, "second");
    queue.ack(direct.token()).unwrap();

    db.tx()
        .push(
            &queue,
            SecretJob {
                body: "transactional".into(),
            },
            None,
        )
        .unwrap()
        .commit()
        .unwrap();
    let transactional = queue.lease(1, None).unwrap().pop().unwrap();
    assert_eq!(transactional.payload().body, "transactional");
    queue.ack(transactional.token()).unwrap();
    drop(queue);
    drop(db);

    let missing_log_dir = tempfile::tempdir().unwrap();
    let db = Arc::new(Db::open_test(missing_log_dir.path()).unwrap());
    let queue = db.open_queue::<EncryptedJobs>(queue_config()).unwrap();
    let err = db
        .tx()
        .push(
            &queue,
            SecretJob {
                body: "must-not-commit".into(),
            },
            None,
        )
        .unwrap()
        .commit()
        .err()
        .expect("opening with no payload key must fail");
    assert!(matches!(err, DbError::Config(_)), "{err}");
    assert_eq!(queue.len(), 0);
    assert_eq!(queue.debug_payload_len(), 0);
    drop(queue);
    drop(db);

    // The payload collection itself reopens with its matching key, independently
    // from the txlog configuration required by transactional operations.
    let db = Arc::new(
        Db::builder(dir.path())
            .for_tests()
            .tx_config(TxConfig {
                log_encryption_key: Some(LOG_KEY),
                ..TxConfig::default()
            })
            .open()
            .unwrap(),
    );
    db.open_queue::<EncryptedJobs>(queue_config())
        .expect("the matching payload key reopens the encrypted queue");
    drop(db);

    let missing_key = Db::open_test(dir.path()).unwrap();
    let err = missing_key
        .open_queue::<EncryptedJobs>(
            QueueConfig::balanced()
                .payload_config(armdb::Config::balanced().shard_count(1).build())
                .build()
                .unwrap(),
        )
        .err()
        .expect("opening with no payload key must fail");
    assert!(
        matches!(err, DbError::EncryptionError(_) | DbError::Config(_)),
        "{err}"
    );
    drop(missing_key);

    let wrong_key = Db::open_test(dir.path()).unwrap();
    let err = wrong_key
        .open_queue::<EncryptedJobs>(
            QueueConfig::balanced()
                .payload_config(
                    armdb::Config::balanced()
                        .shard_count(1)
                        .encryption_key([0x33; 32])
                        .build(),
                )
                .build()
                .unwrap(),
        )
        .err()
        .expect("opening with the wrong payload key must fail");
    assert!(
        matches!(err, DbError::EncryptionError(_) | DbError::Config(_)),
        "{err}"
    );
}