use secure_data::algorithm::{AlgorithmPolicy, CryptoAlgorithm};
#[cfg(feature = "pq")]
use secure_data::envelope::encrypt_with_policy;
#[cfg(not(feature = "pq"))]
use secure_data::envelope::EnvelopeEncrypted;
use secure_data::envelope::{decrypt_for_use, decrypt_with_policy, encrypt_for_storage};
use secure_data::error::DataError;
use secure_data::kms::StaticDevKeyProvider;
#[tokio::test]
async fn cell_v1_producer_consumer_no_pq_round_trips() {
let provider = StaticDevKeyProvider::new();
let plaintext = b"v1 producer, consumer without pq feature";
let envelope = encrypt_for_storage(plaintext, "default", &provider)
.await
.expect("v1 encrypt must succeed");
let recovered = decrypt_for_use(&envelope, &provider)
.await
.expect("v1 decrypt must succeed");
assert_eq!(recovered, plaintext);
assert_eq!(envelope.version, "1");
assert_eq!(envelope.combiner_id, None);
}
#[tokio::test]
async fn cell_v1_producer_round_trips_regardless_of_pq_feature() {
let provider = StaticDevKeyProvider::new();
let plaintext = b"v1 envelope is feature-flag-agnostic on the read side";
let envelope = encrypt_for_storage(plaintext, "default", &provider)
.await
.expect("v1 encrypt");
let recovered = decrypt_for_use(&envelope, &provider)
.await
.expect("v1 decrypt");
assert_eq!(recovered, plaintext);
}
#[tokio::test]
#[cfg(not(feature = "pq"))]
async fn cell_v2_producer_consumer_no_pq_returns_pq_feature_required() {
let v2_json = serde_json::json!({
"version": "2",
"algorithm": "X25519+ML-KEM-768/HKDF-SHA-256",
"key_alias": "default",
"key_version": "1",
"wrapped_data_key": vec![0u8; 32],
"nonce": vec![0u8; 12],
"ciphertext": vec![0u8; 16],
"aad": vec![0u8; 16],
"combiner_id": 1,
});
let envelope: EnvelopeEncrypted = serde_json::from_value(v2_json).unwrap();
let provider = StaticDevKeyProvider::new();
let result = decrypt_for_use(&envelope, &provider).await;
match result {
Err(DataError::PqFeatureRequired) => {}
other => panic!(
"expected PqFeatureRequired on v2-without-pq build, got: {:?}",
other
),
}
}
#[tokio::test]
#[cfg(feature = "pq")]
async fn cell_v2_producer_consumer_with_pq_round_trips() {
let provider = StaticDevKeyProvider::new();
let policy = AlgorithmPolicy::prefer(CryptoAlgorithm::HybridX25519MlKem768);
let plaintext = b"v2 producer, consumer with pq feature";
let envelope = encrypt_with_policy(plaintext, "default", &provider, &policy)
.await
.expect("v2 hybrid encrypt must succeed");
let recovered = decrypt_for_use(&envelope, &provider)
.await
.expect("v2 hybrid decrypt must succeed");
assert_eq!(recovered, plaintext);
assert_eq!(envelope.version, "2");
assert_eq!(
envelope.combiner_id,
Some(secure_data::pq::COMBINER_ID_X25519_ML_KEM_768)
);
}
#[tokio::test]
async fn tm_pqd_abuse_6_downgrade_attack_rejected_by_policy() {
let provider = StaticDevKeyProvider::new();
let plaintext = b"protected by min_envelope_version=2 policy";
let v1_envelope = encrypt_for_storage(plaintext, "default", &provider)
.await
.expect("encrypt");
assert_eq!(v1_envelope.version, "1");
let strict =
AlgorithmPolicy::prefer(CryptoAlgorithm::HybridX25519MlKem768).with_min_envelope_version(2);
let result = decrypt_with_policy(&v1_envelope, &provider, &strict).await;
match result {
Err(DataError::AlgorithmRejectedByPolicy { reason }) => {
assert!(reason.contains("envelope_version"));
assert!(reason.contains("min_envelope_version"));
}
_ => panic!("expected AlgorithmRejectedByPolicy on downgrade"),
}
}
#[tokio::test]
async fn default_policy_accepts_v1_envelope() {
let provider = StaticDevKeyProvider::new();
let plaintext = b"default policy is permissive";
let envelope = encrypt_for_storage(plaintext, "default", &provider)
.await
.expect("encrypt");
let policy = AlgorithmPolicy::default();
let recovered = decrypt_with_policy(&envelope, &provider, &policy)
.await
.expect("default policy must accept v1");
assert_eq!(recovered, plaintext);
}
#[tokio::test]
async fn tm_pqd_abuse_7_version_byte_tamper_fails_at_aead_authentication() {
let provider = StaticDevKeyProvider::new();
let plaintext = b"version-byte tamper";
let mut envelope = encrypt_for_storage(plaintext, "default", &provider)
.await
.expect("encrypt");
envelope.version = "9".into();
let result = decrypt_for_use(&envelope, &provider).await;
match result {
Err(DataError::AuthenticationFailure) => {}
Err(DataError::AlgorithmRejectedByPolicy { .. }) => {}
Err(DataError::EnvelopeMalformed { .. }) => {}
_ => panic!("version-byte tamper must NOT silently decrypt"),
}
}
#[test]
fn min_envelope_version_builder_and_accessor() {
let policy = AlgorithmPolicy::default();
assert_eq!(policy.min_envelope_version(), None);
let strict =
AlgorithmPolicy::prefer(CryptoAlgorithm::HybridX25519MlKem768).with_min_envelope_version(2);
assert_eq!(strict.min_envelope_version(), Some(2));
assert!(strict.validate_envelope_version("2").is_ok());
assert!(strict.validate_envelope_version("3").is_ok());
assert!(strict.validate_envelope_version("1").is_err());
}
#[test]
fn unparseable_envelope_version_under_min_policy_fails_closed() {
let strict = AlgorithmPolicy::default().with_min_envelope_version(2);
let result = strict.validate_envelope_version("not-a-number");
match result {
Err(DataError::AlgorithmRejectedByPolicy { reason }) => {
assert!(reason.contains("cannot be parsed"));
}
other => panic!("expected AlgorithmRejectedByPolicy, got: {:?}", other),
}
}
#[test]
fn no_min_policy_accepts_any_version_string() {
let permissive = AlgorithmPolicy::default();
assert!(permissive.validate_envelope_version("1").is_ok());
assert!(permissive.validate_envelope_version("not-a-number").is_ok());
}