203 lines
6.1 KiB
Rust
203 lines
6.1 KiB
Rust
use super::*;
|
|
|
|
const CONTEXT: SnapshotCryptoContext<'static> = SnapshotCryptoContext {
|
|
user_id: "user-01",
|
|
vault_generation: 1,
|
|
snapshot_id: "device-01",
|
|
schema_rev: 1,
|
|
logical_clock: 42,
|
|
device_id: "device-01",
|
|
head_revision: 1,
|
|
base_head: None,
|
|
};
|
|
|
|
#[test]
|
|
fn snapshot_encryption_round_trips_and_hides_plaintext() -> Result<(), SyncClientError> {
|
|
let key = AccountKey::from_bytes([7; 32]);
|
|
let plaintext = br#"{"tabs":[{"url":"https://private.example"}]}"#;
|
|
let encrypted = key.encrypt(&CONTEXT, plaintext)?;
|
|
|
|
assert!(!encrypted.bytes().windows(plaintext.len()).any(|window| window == plaintext));
|
|
assert_eq!(
|
|
key.decrypt(
|
|
&CONTEXT,
|
|
SNAPSHOT_ENCRYPTION_VERSION,
|
|
encrypted.key_id(),
|
|
encrypted.content_hash(),
|
|
encrypted.bytes(),
|
|
)?,
|
|
plaintext
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_encryption_uses_fresh_nonces_and_stable_keyed_content_hashes()
|
|
-> Result<(), SyncClientError> {
|
|
let key = AccountKey::from_bytes([9; 32]);
|
|
let first = key.encrypt(&CONTEXT, b"same payload")?;
|
|
let second = key.encrypt(&CONTEXT, b"same payload")?;
|
|
|
|
assert_ne!(first.bytes(), second.bytes());
|
|
assert_eq!(first.content_hash(), second.content_hash());
|
|
assert_ne!(
|
|
first.content_hash(),
|
|
AccountKey::from_bytes([10; 32]).content_hash(b"same payload")?
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_authentication_rejects_metadata_and_ciphertext_tampering() -> Result<(), SyncClientError>
|
|
{
|
|
let key = AccountKey::from_bytes([11; 32]);
|
|
let encrypted = key.encrypt(&CONTEXT, b"authenticated payload")?;
|
|
let changed_context = SnapshotCryptoContext { logical_clock: 43, ..CONTEXT };
|
|
|
|
assert!(
|
|
key.decrypt(
|
|
&changed_context,
|
|
SNAPSHOT_ENCRYPTION_VERSION,
|
|
encrypted.key_id(),
|
|
encrypted.content_hash(),
|
|
encrypted.bytes(),
|
|
)
|
|
.is_err()
|
|
);
|
|
|
|
let mut tampered = encrypted.bytes().to_vec();
|
|
let last = tampered.len() - 1;
|
|
tampered[last] ^= 1;
|
|
assert!(
|
|
key.decrypt(
|
|
&CONTEXT,
|
|
SNAPSHOT_ENCRYPTION_VERSION,
|
|
encrypted.key_id(),
|
|
encrypted.content_hash(),
|
|
&tampered,
|
|
)
|
|
.is_err()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_authentication_binds_every_routing_field() -> Result<(), SyncClientError> {
|
|
let key = AccountKey::from_bytes([13; 32]);
|
|
let encrypted = key.encrypt(&CONTEXT, b"routing metadata")?;
|
|
let changed = [
|
|
SnapshotCryptoContext { user_id: "user-02", ..CONTEXT },
|
|
SnapshotCryptoContext { vault_generation: 2, ..CONTEXT },
|
|
SnapshotCryptoContext { snapshot_id: "device-02", ..CONTEXT },
|
|
SnapshotCryptoContext { schema_rev: 2, ..CONTEXT },
|
|
SnapshotCryptoContext { logical_clock: 41, ..CONTEXT },
|
|
SnapshotCryptoContext { device_id: "device-02", ..CONTEXT },
|
|
];
|
|
|
|
for context in changed {
|
|
assert!(
|
|
key.decrypt(
|
|
&context,
|
|
SNAPSHOT_ENCRYPTION_VERSION,
|
|
encrypted.key_id(),
|
|
encrypted.content_hash(),
|
|
encrypted.bytes(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_authentication_binds_head_lineage() -> Result<(), SyncClientError> {
|
|
let key = AccountKey::from_bytes([14; 32]);
|
|
let base = SnapshotHeadRef::new(7, "device-02", "31".repeat(32))?;
|
|
let context = SnapshotCryptoContext { head_revision: 8, base_head: Some(&base), ..CONTEXT };
|
|
let encrypted = key.encrypt(&context, b"head lineage")?;
|
|
for changed_base in [
|
|
SnapshotHeadRef::new(6, "device-02", "31".repeat(32))?,
|
|
SnapshotHeadRef::new(7, "device-03", "31".repeat(32))?,
|
|
SnapshotHeadRef::new(7, "device-02", "32".repeat(32))?,
|
|
] {
|
|
let changed = SnapshotCryptoContext {
|
|
head_revision: changed_base.revision + 1,
|
|
base_head: Some(&changed_base),
|
|
..CONTEXT
|
|
};
|
|
assert!(
|
|
key.decrypt(
|
|
&changed,
|
|
SNAPSHOT_ENCRYPTION_VERSION,
|
|
encrypted.key_id(),
|
|
encrypted.content_hash(),
|
|
encrypted.bytes(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
let changed_revision = SnapshotCryptoContext { head_revision: 9, ..context };
|
|
assert!(
|
|
key.decrypt(
|
|
&changed_revision,
|
|
SNAPSHOT_ENCRYPTION_VERSION,
|
|
encrypted.key_id(),
|
|
encrypted.content_hash(),
|
|
encrypted.bytes(),
|
|
)
|
|
.is_err()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_v1_aad_remains_decryptable() -> Result<(), SyncClientError> {
|
|
let key = AccountKey::from_bytes([16; 32]);
|
|
let encrypted = key.encrypt_with_version(&CONTEXT, b"legacy snapshot", 1)?;
|
|
assert_eq!(
|
|
key.decrypt(&CONTEXT, 1, encrypted.key_id(), encrypted.content_hash(), encrypted.bytes(),)?,
|
|
b"legacy snapshot"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_envelope_rejects_unknown_versions_and_wrong_keys() -> Result<(), SyncClientError> {
|
|
let key = AccountKey::from_bytes([15; 32]);
|
|
let encrypted = key.encrypt(&CONTEXT, b"versioned payload")?;
|
|
|
|
assert!(
|
|
key.decrypt(
|
|
&CONTEXT,
|
|
SNAPSHOT_ENCRYPTION_VERSION + 1,
|
|
encrypted.key_id(),
|
|
encrypted.content_hash(),
|
|
encrypted.bytes(),
|
|
)
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
AccountKey::from_bytes([16; 32])
|
|
.decrypt(
|
|
&CONTEXT,
|
|
SNAPSHOT_ENCRYPTION_VERSION,
|
|
encrypted.key_id(),
|
|
encrypted.content_hash(),
|
|
encrypted.bytes(),
|
|
)
|
|
.is_err()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_envelope_honors_the_transport_size_limit() -> Result<(), SyncClientError> {
|
|
let key = AccountKey::from_bytes([17; 32]);
|
|
let maximum = vec![0_u8; MAX_PLAINTEXT_BYTES];
|
|
let encrypted = key.encrypt(&CONTEXT, &maximum)?;
|
|
|
|
assert_eq!(encrypted.bytes().len(), MAX_SNAPSHOT_BYTES);
|
|
assert!(key.encrypt(&CONTEXT, &vec![0_u8; MAX_PLAINTEXT_BYTES + 1]).is_err());
|
|
Ok(())
|
|
}
|