libsoliton 0.1.3

Core cryptographic library for the LO protocol — hybrid post-quantum key exchange, signatures, ratchet, and storage encryption
Documentation
#![allow(deprecated)] // Tests exercise from_bytes directly.

use soliton::primitives::random;
use soliton::primitives::xwing;
use soliton::ratchet::RatchetState;
use soliton::storage::{StorageKey, StorageKeyRing, decrypt_blob, encrypt_blob};

#[test]
fn key_rotation_lifecycle() {
    let key_v1 = StorageKey::new(1, random::random_array()).unwrap();
    let key_v2 = StorageKey::new(2, random::random_array()).unwrap();

    let mut ring = StorageKeyRing::new(key_v1).unwrap();

    // Encrypt blobs with v1.
    let blob_a = encrypt_blob(
        ring.active_key().unwrap(),
        b"message A",
        "channel-1",
        "seg-0",
        false,
    )
    .unwrap();
    let blob_b = encrypt_blob(
        ring.active_key().unwrap(),
        b"message B",
        "channel-1",
        "seg-1",
        true,
    )
    .unwrap();

    // Add v2 as the new active key.
    ring.add_key(key_v2, true).unwrap();
    assert_eq!(ring.active_key().unwrap().version(), 2);

    // Encrypt new blob with v2.
    let blob_c = encrypt_blob(
        ring.active_key().unwrap(),
        b"message C",
        "channel-2",
        "seg-0",
        false,
    )
    .unwrap();

    // Decrypt all blobs — v1 blobs should still work.
    let pt_a = decrypt_blob(&ring, &blob_a, "channel-1", "seg-0").unwrap();
    assert_eq!(&*pt_a, b"message A");
    let pt_b = decrypt_blob(&ring, &blob_b, "channel-1", "seg-1").unwrap();
    assert_eq!(&*pt_b, b"message B");
    let pt_c = decrypt_blob(&ring, &blob_c, "channel-2", "seg-0").unwrap();
    assert_eq!(&*pt_c, b"message C");

    // Remove v1.
    assert!(ring.remove_key(1).unwrap());

    // v1 blobs can no longer be decrypted.
    assert!(decrypt_blob(&ring, &blob_a, "channel-1", "seg-0").is_err());

    // v2 blobs still work.
    let pt_c2 = decrypt_blob(&ring, &blob_c, "channel-2", "seg-0").unwrap();
    assert_eq!(&*pt_c2, b"message C");
}

#[test]
fn ratchet_state_storage_round_trip() {
    // Create a ratchet state with some conversation history.
    let (ek_pk, ek_sk) = xwing::keygen().unwrap();
    let rk: [u8; 32] = random::random_array();
    let ck: [u8; 32] = random::random_array();
    let fp_a = [0xAAu8; 32];
    let fp_b = [0xBBu8; 32];
    let mut alice = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk.clone(), ek_sk).unwrap();
    let mut bob = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk).unwrap();

    // Exchange some messages.
    let enc = alice.encrypt(b"hello").unwrap();
    bob.decrypt(&enc.header, &enc.ciphertext).unwrap();
    let enc = bob.encrypt(b"world").unwrap();
    alice.decrypt(&enc.header, &enc.ciphertext).unwrap();

    // Serialize the ratchet state.
    let alice_bytes = alice.to_bytes().unwrap().0;

    // Encrypt as a storage blob.
    let storage_key = StorageKey::new(1, random::random_array()).unwrap();
    let blob = encrypt_blob(&storage_key, &alice_bytes, "session-store", "alice-1", true).unwrap();

    // Decrypt the blob.
    let ring = StorageKeyRing::new(storage_key).unwrap();
    let decrypted = decrypt_blob(&ring, &blob, "session-store", "alice-1").unwrap();

    // Verify serialization fidelity: the round-tripped bytes must be identical
    // (excluding the epoch field, which advances on every to_bytes() call).
    let alice2 = RatchetState::from_bytes(&decrypted).unwrap();
    let alice2_bytes = alice2.to_bytes().unwrap().0;
    assert_eq!(alice_bytes[0], alice2_bytes[0], "version must match");
    assert_eq!(
        alice_bytes[9..],
        alice2_bytes[9..],
        "ratchet state serialization round-trip must be identical (post-epoch fields)",
    );
    // Epoch must advance by exactly 1 per to_bytes() call.
    let epoch1 = u64::from_be_bytes(alice_bytes[1..9].try_into().unwrap());
    let epoch2 = u64::from_be_bytes(alice2_bytes[1..9].try_into().unwrap());
    assert_eq!(epoch2, epoch1 + 1);

    // Verify continued operation after deserialization.
    let mut alice2 = RatchetState::from_bytes(&decrypted).unwrap();
    let enc = alice2.encrypt(b"after storage").unwrap();
    let pt = bob.decrypt(&enc.header, &enc.ciphertext).unwrap();
    assert_eq!(&*pt, b"after storage");
}