libsoliton 0.1.3

Core cryptographic library for the LO protocol — hybrid post-quantum key exchange, signatures, ratchet, and storage encryption
Documentation
//! Protocol constants from Soliton Crypto Appendix A.

/// HMAC label for KEM authentication (§4.2).
pub const AUTH_HMAC_LABEL: &[u8] = b"lo-auth-v1";

/// HKDF info prefix for LO-KEX key derivation (§5.4 Step 4).
pub const KEX_HKDF_INFO_PFX: &[u8] = b"lo-kex-v1";

/// Domain separator for SPK signatures (§5.3).
/// Prepended to the SPK public key before signing/verifying to prevent
/// cross-context signature reuse.
pub const SPK_SIG_LABEL: &[u8] = b"lo-spk-sig-v1";

/// Domain separator for SessionInit signatures (§5.4).
/// Prepended to the encoded SessionInit before Alice signs / Bob verifies to
/// bind the signature to the session-initiation context and prevent reuse in
/// other protocol components.
pub const INITIATOR_SIG_LABEL: &[u8] = b"lo-kex-init-sig-v1";

/// HKDF info for LO-Ratchet root KDF (§6.4).
pub const RATCHET_HKDF_INFO: &[u8] = b"lo-ratchet-v1";

/// AAD prefix for DM message encryption (§7.3).
pub const DM_AAD: &[u8] = b"lo-dm-v1";

/// AAD prefix for community storage encryption (§11.4.1).
pub const STORAGE_AAD: &[u8] = b"lo-storage-v1";

/// AAD prefix for DM queue storage encryption (§11.4.2).
pub const DM_QUEUE_AAD: &[u8] = b"lo-dm-queue-v1";

/// HKDF info for call key derivation (§6.12).
pub const CALL_HKDF_INFO: &[u8] = b"lo-call-v1";

/// Domain separation label for the initial verification phrase hash.
///
/// Used in SHA3-256 input: `"lo-verification-v1" || sorted_pk_a || sorted_pk_b`.
/// See the `verification` module for the full phrase generation algorithm.
pub const PHRASE_HASH_LABEL: &[u8] = b"lo-verification-v1";

/// Rehash label for verification phrase expansion.
///
/// Used to extend the initial 32-byte hash when more output bytes are needed
/// for word index derivation. See the `verification` module.
pub const PHRASE_EXPAND_LABEL: &[u8] = b"lo-phrase-expand-v1";

/// Call ID size: 16 bytes (128-bit random identifier generated by the call
/// initiator) (§6.12).
pub const CALL_ID_SIZE: usize = 16;

/// HMAC input byte for deriving the first call encryption key from the call
/// chain (§6.12). Distinct from the ratchet message key domain byte (0x01) to
/// prevent structural collision between the ratchet and call derivation
/// domains — security ultimately depends on different keys, but unique data
/// bytes provide defense-in-depth at zero cost.
pub const CALL_KEY_A_BYTE: &[u8] = &[0x04];

/// HMAC input byte for deriving the second call encryption key from the call
/// chain (§6.12).
pub const CALL_KEY_B_BYTE: &[u8] = &[0x05];

/// HMAC input byte for advancing the call chain key (§6.12). The call chain
/// derives three outputs per step (two call encryption keys + next chain key),
/// using 0x04/0x05/0x06 — disjoint from the ratchet message key domain byte
/// (0x01).
pub const CALL_CHAIN_ADV_BYTE: &[u8] = &[0x06];

// 0x02 and 0x03 are deliberately unassigned. The ratchet previously used
// 0x01/0x02 for chain-mode derivation; after the switch to counter-mode
// (epoch-key based), only 0x01 remains in use (MSG_KEY_DOMAIN_BYTE). The call
// chain uses 0x04/0x05/0x06. The gap is reserved to prevent accidental reuse
// by future extensions.

/// Domain byte prefix for counter-mode message key derivation (§6.3).
/// HMAC input is `0x01 || counter_be_u32` — the prefix provides domain
/// separation from any other potential HMAC use of the epoch key (defense-in-depth).
pub const MSG_KEY_DOMAIN_BYTE: u8 = 0x01;

/// Zero salt for HKDF in LO-KEX (§5.4 Step 4).
pub const HKDF_ZERO_SALT: [u8; 32] = [0u8; 32];

/// Maximum entries in recv_seen set per epoch (§6.7). Prevents memory
/// exhaustion from an adversary sending messages with many distinct counter
/// values. The set stores only 4-byte counters (not keys), so 65536 entries
/// is ~256 KB. Resets on each KEM ratchet step.
pub const MAX_RECV_SEEN: u32 = 65536;

/// Ratchet state serialization format version. Checked on deserialization;
/// states with a different version byte are rejected with `UnsupportedVersion`.
/// Bump when the wire format changes.
pub const RATCHET_BLOB_VERSION: u8 = 0x01;

/// Crypto version string.
pub const CRYPTO_VERSION: &str = "lo-crypto-v1";

// --- Key sizes ---

/// LO composite public key size: X-Wing (1216) + Ed25519 (32) + ML-DSA-65 (1952) = 3200 bytes.
pub const LO_PUBLIC_KEY_SIZE: usize = 3200;

/// LO composite secret key size: X-Wing (2432) + Ed25519 (32) + ML-DSA-65 seed (32) = 2496 bytes.
pub const LO_SECRET_KEY_SIZE: usize = 2496;

/// X-Wing secret key size: X25519 (32) + ML-KEM-768 (2400) = 2432 bytes.
pub const XWING_SECRET_KEY_SIZE: usize = 2432;

/// X-Wing public key size: X25519 (32) + ML-KEM-768 (1184) = 1216 bytes.
pub const XWING_PUBLIC_KEY_SIZE: usize = 1216;

/// X-Wing ciphertext size: 1120 bytes (X25519 ephemeral pk (32) + ML-KEM-768 ct (1088)).
pub const XWING_CIPHERTEXT_SIZE: usize = 1120;

/// Fingerprint size (raw SHA3-256): 32 bytes.
pub const FINGERPRINT_SIZE: usize = 32;

/// Ed25519 signature size: 64 bytes (RFC 8032).
pub const ED25519_SIGNATURE_SIZE: usize = 64;

/// ML-DSA-65 signature size: 3309 bytes (FIPS 204).
pub const MLDSA_SIGNATURE_SIZE: usize = 3309;

/// Hybrid signature size: Ed25519 (64) + ML-DSA-65 (3309) = 3373 bytes.
pub const HYBRID_SIGNATURE_SIZE: usize = 3373;

/// X-Wing shared secret size: 32 bytes.
pub const SHARED_SECRET_SIZE: usize = 32;

/// AEAD (XChaCha20-Poly1305) tag size: 16 bytes.
pub const AEAD_TAG_SIZE: usize = 16;

/// AEAD (XChaCha20-Poly1305) nonce size: 24 bytes.
pub const AEAD_NONCE_SIZE: usize = 24;

// --- Streaming AEAD constants ---

/// AAD domain label for streaming AEAD (§15).
pub const STREAM_AAD: &[u8] = b"lo-stream-v1";

/// Streaming AEAD chunk size: 1 MiB plaintext per non-final chunk.
/// Balance of memory use vs per-chunk overhead. Well under WASM 16 MiB cap.
pub const STREAM_CHUNK_SIZE: usize = 1_048_576;

/// Streaming AEAD header size: version (1) + flags (1) + base_nonce (24).
pub const STREAM_HEADER_SIZE: usize = 26;

/// Per-chunk wire overhead: tag_byte (1) + Poly1305 authentication tag (16).
pub const STREAM_CHUNK_OVERHEAD: usize = 17;

/// Worst-case zstd expansion for a `STREAM_CHUNK_SIZE` input.
/// Deliberate ~5× over-estimate of zstd stored-frame overhead (actual worst
/// case is ~50 bytes for 1 MiB). Cost of over-estimation: 256 bytes of extra
/// buffer allocation per chunk — negligible against 1 MiB chunks.
pub const STREAM_ZSTD_OVERHEAD: usize = 256;

/// Maximum encrypted chunk output size (with compression).
/// `STREAM_CHUNK_SIZE + STREAM_ZSTD_OVERHEAD + STREAM_CHUNK_OVERHEAD`.
/// CAPI callers allocate output buffers of at least this size.
pub const STREAM_ENCRYPT_MAX: usize =
    STREAM_CHUNK_SIZE + STREAM_ZSTD_OVERHEAD + STREAM_CHUNK_OVERHEAD;

/// Streaming AEAD format version byte. Only `0x01` is currently defined.
pub const STREAM_VERSION: u8 = 0x01;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[allow(clippy::assertions_on_constants)]
    fn constants_match_spec() {
        // Protocol labels
        assert_eq!(AUTH_HMAC_LABEL, b"lo-auth-v1");
        assert_eq!(KEX_HKDF_INFO_PFX, b"lo-kex-v1");
        assert_eq!(SPK_SIG_LABEL, b"lo-spk-sig-v1");
        assert_eq!(INITIATOR_SIG_LABEL, b"lo-kex-init-sig-v1");
        assert_eq!(RATCHET_HKDF_INFO, b"lo-ratchet-v1");
        assert_eq!(DM_AAD, b"lo-dm-v1");
        assert_eq!(STORAGE_AAD, b"lo-storage-v1");
        assert_eq!(DM_QUEUE_AAD, b"lo-dm-queue-v1");
        assert_eq!(CALL_HKDF_INFO, b"lo-call-v1");
        assert_eq!(PHRASE_HASH_LABEL, b"lo-verification-v1");
        assert_eq!(PHRASE_EXPAND_LABEL, b"lo-phrase-expand-v1");

        // Call derivation bytes
        assert_eq!(CALL_ID_SIZE, 16);
        assert_eq!(CALL_KEY_A_BYTE, &[0x04]);
        assert_eq!(CALL_KEY_B_BYTE, &[0x05]);
        assert_eq!(CALL_CHAIN_ADV_BYTE, &[0x06]);

        // Message key domain byte
        assert_eq!(MSG_KEY_DOMAIN_BYTE, 0x01);

        // HKDF zero salt
        assert_eq!(HKDF_ZERO_SALT, [0u8; 32]);

        // Ratchet limits
        assert_eq!(MAX_RECV_SEEN, 65536);

        // Composite key sizes
        assert_eq!(LO_PUBLIC_KEY_SIZE, 3200);
        assert_eq!(LO_SECRET_KEY_SIZE, 2496);

        // X-Wing sizes
        assert_eq!(XWING_SECRET_KEY_SIZE, 2432);
        assert_eq!(XWING_PUBLIC_KEY_SIZE, 1216);
        assert_eq!(XWING_CIPHERTEXT_SIZE, 1120);

        // Other sizes
        assert_eq!(FINGERPRINT_SIZE, 32);
        assert_eq!(ED25519_SIGNATURE_SIZE, 64);
        assert_eq!(MLDSA_SIGNATURE_SIZE, 3309);
        assert_eq!(HYBRID_SIGNATURE_SIZE, 3373);
        assert_eq!(SHARED_SECRET_SIZE, 32);
        assert_eq!(AEAD_TAG_SIZE, 16);
        assert_eq!(AEAD_NONCE_SIZE, 24);

        // Streaming AEAD constants
        assert_eq!(STREAM_AAD, b"lo-stream-v1");
        assert_eq!(STREAM_CHUNK_SIZE, 1_048_576);
        assert_eq!(STREAM_HEADER_SIZE, 26);
        assert_eq!(STREAM_CHUNK_OVERHEAD, 17);
        assert_eq!(STREAM_ZSTD_OVERHEAD, 256);
        assert_eq!(
            STREAM_ENCRYPT_MAX,
            STREAM_CHUNK_SIZE + STREAM_ZSTD_OVERHEAD + STREAM_CHUNK_OVERHEAD
        );
        assert_eq!(STREAM_VERSION, 0x01);

        // Derived size consistency
        assert_eq!(
            HYBRID_SIGNATURE_SIZE,
            ED25519_SIGNATURE_SIZE + MLDSA_SIGNATURE_SIZE
        );
        assert_eq!(LO_PUBLIC_KEY_SIZE, XWING_PUBLIC_KEY_SIZE + 32 + 1952);
        assert_eq!(LO_SECRET_KEY_SIZE, XWING_SECRET_KEY_SIZE + 32 + 32);

        // Version string
        assert_eq!(CRYPTO_VERSION, "lo-crypto-v1");

        // MAX_RECV_SEEN is bounded for memory safety
        assert!(MAX_RECV_SEEN <= 65536);
    }

    #[test]
    fn domain_labels_pairwise_unique() {
        // All domain separation labels must be distinct to prevent
        // cross-context collisions. Collect every label as bytes and
        // verify no duplicates exist.
        let labels: Vec<(&str, &[u8])> = vec![
            ("AUTH_HMAC_LABEL", AUTH_HMAC_LABEL),
            ("KEX_HKDF_INFO_PFX", KEX_HKDF_INFO_PFX),
            ("SPK_SIG_LABEL", SPK_SIG_LABEL),
            ("INITIATOR_SIG_LABEL", INITIATOR_SIG_LABEL),
            ("RATCHET_HKDF_INFO", RATCHET_HKDF_INFO),
            ("DM_AAD", DM_AAD),
            ("STORAGE_AAD", STORAGE_AAD),
            ("DM_QUEUE_AAD", DM_QUEUE_AAD),
            ("CALL_HKDF_INFO", CALL_HKDF_INFO),
            ("PHRASE_HASH_LABEL", PHRASE_HASH_LABEL),
            ("PHRASE_EXPAND_LABEL", PHRASE_EXPAND_LABEL),
            ("STREAM_AAD", STREAM_AAD),
        ];
        for i in 0..labels.len() {
            for j in (i + 1)..labels.len() {
                assert_ne!(
                    labels[i].1, labels[j].1,
                    "domain label collision: {} == {}",
                    labels[i].0, labels[j].0
                );
            }
        }

        // Single-byte domain bytes must also be mutually distinct.
        let domain_bytes: Vec<(&str, u8)> = vec![
            ("MSG_KEY_DOMAIN_BYTE", MSG_KEY_DOMAIN_BYTE),
            ("CALL_KEY_A_BYTE", CALL_KEY_A_BYTE[0]),
            ("CALL_KEY_B_BYTE", CALL_KEY_B_BYTE[0]),
            ("CALL_CHAIN_ADV_BYTE", CALL_CHAIN_ADV_BYTE[0]),
        ];
        for i in 0..domain_bytes.len() {
            for j in (i + 1)..domain_bytes.len() {
                assert_ne!(
                    domain_bytes[i].1, domain_bytes[j].1,
                    "domain byte collision: {} == {}",
                    domain_bytes[i].0, domain_bytes[j].0
                );
            }
        }
    }
}