derec-library 0.0.1-alpha.8

Rust SDK for the DeRec protocol, including native and WebAssembly bindings.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 DeRec Alliance. All rights reserved.

use super::*;
use crate::{
    derec_message::DeRecMessageBuilder, protocol_version::ProtocolVersion, types::ChannelId,
};
use derec_cryptography::pairing::pairing_ecies;
use derec_proto::{MessageBody, PairRequestMessage};
use prost::Message;
use rand08::thread_rng;

const CHANNEL_ID: ChannelId = ChannelId(42);
const SEQUENCE: u32 = 7;
const SHARED_KEY: [u8; 32] = [0xAB; 32];

fn pairing_keypair() -> (Vec<u8>, Vec<u8>) {
    pairing_ecies::generate_key(&mut thread_rng()).unwrap()
}

fn sample_message() -> PairRequestMessage {
    PairRequestMessage::default()
}

#[test]
fn test_pairing_builder_new_sets_defaults() {
    let builder = DeRecMessageBuilder::pairing();

    assert!(builder.sequence.is_none());
    assert!(builder.channel_id.is_none());
    assert!(builder.timestamp.is_none());
    assert!(builder.message.is_none());
    assert!(builder.trace_id.is_none());
}

#[test]
fn test_channel_builder_new_sets_defaults() {
    let builder = DeRecMessageBuilder::channel();

    assert!(builder.sequence.is_none());
    assert!(builder.channel_id.is_none());
    assert!(builder.timestamp.is_none());
    assert!(builder.message.is_none());
    assert!(builder.trace_id.is_none());
}

#[test]
fn test_channel_id_sequence_timestamp_and_message_set_fields() {
    let inner = sample_message();
    let timestamp = current_timestamp();

    let builder = DeRecMessageBuilder::pairing()
        .channel_id(CHANNEL_ID)
        .sequence(SEQUENCE)
        .timestamp(timestamp)
        .message_body(MessageBody::PairRequest(inner.clone()));

    assert_eq!(builder.channel_id, Some(CHANNEL_ID));
    assert_eq!(builder.sequence, Some(SEQUENCE));
    assert_eq!(builder.timestamp, Some(timestamp));
    assert!(
        matches!(builder.message, Some(MessageBody::PairRequest(inner_message)) if inner_message == inner),
    );
}

#[test]
fn test_pairing_encrypt_rejects_missing_message() {
    let (_sk, pk) = pairing_keypair();

    let err = DeRecMessageBuilder::pairing()
        .channel_id(CHANNEL_ID)
        .timestamp(current_timestamp())
        .encrypt_pairing(&pk)
        .unwrap_err();

    assert!(matches!(err, DeRecMessageBuilderError::MissingMessage));
}

#[test]
fn test_channel_encrypt_rejects_missing_message() {
    let err = DeRecMessageBuilder::channel()
        .channel_id(CHANNEL_ID)
        .timestamp(current_timestamp())
        .encrypt(&SHARED_KEY)
        .unwrap_err();

    assert!(matches!(err, DeRecMessageBuilderError::MissingMessage));
}

#[test]
fn test_channel_encrypt_rejects_missing_channel_id() {
    let err = DeRecMessageBuilder::channel()
        .timestamp(current_timestamp())
        .message_body(MessageBody::PairRequest(sample_message()))
        .encrypt(&SHARED_KEY)
        .unwrap_err();

    assert!(matches!(err, DeRecMessageBuilderError::MissingChannelId));
}

#[test]
fn test_pairing_build_rejects_missing_timestamp() {
    let (_sk, pk) = pairing_keypair();

    let err = DeRecMessageBuilder::pairing()
        .channel_id(CHANNEL_ID)
        .message_body(MessageBody::PairRequest(sample_message()))
        .encrypt_pairing(&pk)
        .unwrap()
        .build()
        .unwrap_err();

    assert!(matches!(err, DeRecMessageBuilderError::MissingTimestamp));
}

#[test]
fn test_pairing_build_rejects_missing_channel_id() {
    let (_sk, pk) = pairing_keypair();

    let err = DeRecMessageBuilder::pairing()
        .timestamp(current_timestamp())
        .message_body(MessageBody::PairRequest(sample_message()))
        .encrypt_pairing(&pk)
        .unwrap()
        .build()
        .unwrap_err();

    assert!(matches!(err, DeRecMessageBuilderError::MissingChannelId));
}

#[test]
fn test_channel_build_rejects_missing_timestamp() {
    let err = DeRecMessageBuilder::channel()
        .channel_id(CHANNEL_ID)
        .message_body(MessageBody::PairRequest(sample_message()))
        .encrypt(&SHARED_KEY)
        .unwrap()
        .build()
        .unwrap_err();

    assert!(matches!(err, DeRecMessageBuilderError::MissingTimestamp));
}

#[test]
fn test_channel_build_rejects_missing_channel_id_after_encrypt_is_impossible_but_checked_before() {
    let err = DeRecMessageBuilder::channel()
        .timestamp(current_timestamp())
        .message_body(MessageBody::PairRequest(sample_message()))
        .encrypt(&SHARED_KEY)
        .unwrap_err();

    assert!(matches!(err, DeRecMessageBuilderError::MissingChannelId));
}

#[test]
fn test_pairing_encrypt_and_build_succeeds() {
    let (_sk, pk) = pairing_keypair();
    let inner = sample_message();
    let plaintext = inner.encode_to_vec();
    let timestamp = current_timestamp();

    let envelope = DeRecMessageBuilder::pairing()
        .channel_id(CHANNEL_ID)
        .sequence(SEQUENCE)
        .timestamp(timestamp)
        .message_body(MessageBody::PairRequest(inner))
        .encrypt_pairing(&pk)
        .unwrap()
        .build()
        .unwrap();

    let version = ProtocolVersion::current();

    assert_eq!(envelope.protocol_version_major, version.major);
    assert_eq!(envelope.protocol_version_minor, version.minor);
    assert_eq!(envelope.sequence, SEQUENCE);
    assert_eq!(envelope.channel_id, u64::from(CHANNEL_ID));
    assert_eq!(envelope.timestamp, Some(timestamp));
    assert!(!envelope.message.is_empty());
    assert_ne!(envelope.message, plaintext);
}

#[test]
fn test_channel_encrypt_and_build_succeeds() {
    let inner = sample_message();
    let plaintext = inner.encode_to_vec();
    let timestamp = current_timestamp();

    let envelope = DeRecMessageBuilder::channel()
        .channel_id(CHANNEL_ID)
        .sequence(SEQUENCE)
        .timestamp(timestamp)
        .message_body(MessageBody::PairRequest(inner))
        .encrypt(&SHARED_KEY)
        .unwrap()
        .build()
        .unwrap();

    let version = ProtocolVersion::current();

    assert_eq!(envelope.protocol_version_major, version.major);
    assert_eq!(envelope.protocol_version_minor, version.minor);
    assert_eq!(envelope.sequence, SEQUENCE);
    assert_eq!(envelope.channel_id, u64::from(CHANNEL_ID));
    assert_eq!(envelope.timestamp, Some(timestamp));
    assert!(!envelope.message.is_empty());
    assert_ne!(envelope.message, plaintext);
}

#[test]
fn test_build_defaults_sequence_to_zero_when_not_set() {
    let (_sk, pk) = pairing_keypair();

    let envelope = DeRecMessageBuilder::pairing()
        .channel_id(CHANNEL_ID)
        .timestamp(current_timestamp())
        .message_body(MessageBody::PairRequest(sample_message()))
        .encrypt_pairing(&pk)
        .unwrap()
        .build()
        .unwrap();

    assert_eq!(envelope.sequence, 0);
}

#[test]
fn test_current_timestamp_returns_valid_range() {
    let ts = current_timestamp();

    assert!(ts.seconds >= 0);
    assert!((0..1_000_000_000).contains(&ts.nanos));
}

#[test]
fn test_message_overwrites_previous_payload() {
    let first = PairRequestMessage {
        nonce: 1,
        ..Default::default()
    };
    let second = PairRequestMessage {
        nonce: 2,
        ..Default::default()
    };

    let builder = DeRecMessageBuilder::pairing()
        .message_body(MessageBody::PairRequest(first))
        .message_body(MessageBody::PairRequest(second.clone()));

    assert!(
        matches!(builder.message, Some(MessageBody::PairRequest(inner_message)) if inner_message == second),
    );
}

#[test]
fn test_trace_id_is_propagated_through_pairing_build() {
    let (_sk, pk) = pairing_keypair();
    let timestamp = current_timestamp();

    let envelope = DeRecMessageBuilder::pairing()
        .channel_id(CHANNEL_ID)
        .timestamp(timestamp)
        .message_body(MessageBody::PairRequest(sample_message()))
        .trace_id(0xCAFEBABE_DEADBEEF)
        .encrypt_pairing(&pk)
        .expect("encrypt should succeed")
        .build()
        .expect("build should succeed");

    assert_eq!(envelope.trace_id, 0xCAFEBABE_DEADBEEF);
}

#[test]
fn test_trace_id_is_propagated_through_channel_build() {
    let timestamp = current_timestamp();

    let envelope = DeRecMessageBuilder::channel()
        .channel_id(CHANNEL_ID)
        .timestamp(timestamp)
        .message_body(MessageBody::PairRequest(sample_message()))
        .trace_id(0x0102_0304_0506_0708)
        .encrypt(&SHARED_KEY)
        .expect("encrypt should succeed")
        .build()
        .expect("build should succeed");

    assert_eq!(envelope.trace_id, 0x0102_0304_0506_0708);
}

#[test]
fn test_auto_trace_id_produces_non_zero_value() {
    let timestamp = current_timestamp();
    let (_sk, pk) = pairing_keypair();

    let envelope = DeRecMessageBuilder::pairing()
        .channel_id(CHANNEL_ID)
        .timestamp(timestamp)
        .message_body(MessageBody::PairRequest(sample_message()))
        .auto_trace_id()
        .encrypt_pairing(&pk)
        .expect("encrypt should succeed")
        .build()
        .expect("build should succeed");

    assert_ne!(envelope.trace_id, 0);
}

#[test]
fn test_trace_id_defaults_to_zero_when_unset() {
    let (_sk, pk) = pairing_keypair();
    let timestamp = current_timestamp();

    let envelope = DeRecMessageBuilder::pairing()
        .channel_id(CHANNEL_ID)
        .timestamp(timestamp)
        .message_body(MessageBody::PairRequest(sample_message()))
        .encrypt_pairing(&pk)
        .expect("encrypt should succeed")
        .build()
        .expect("build should succeed");

    assert_eq!(envelope.trace_id, 0);
}

#[test]
fn test_trace_id_round_trips_through_proto_encoding() {
    let (_sk, pk) = pairing_keypair();
    let timestamp = current_timestamp();

    let original = DeRecMessageBuilder::pairing()
        .channel_id(CHANNEL_ID)
        .timestamp(timestamp)
        .message_body(MessageBody::PairRequest(sample_message()))
        .trace_id(0xFEED_FACE_BAAD_F00D)
        .encrypt_pairing(&pk)
        .expect("encrypt should succeed")
        .build()
        .expect("build should succeed");

    let wire_bytes = original.encode_to_vec();
    let decoded = derec_proto::DeRecMessage::decode(wire_bytes.as_slice())
        .expect("envelope should decode");

    assert_eq!(decoded.trace_id, 0xFEED_FACE_BAAD_F00D);
}

#[test]
fn test_extract_inner_plaintext_message_round_trips() {
    let inner = sample_message();
    let encoded = MessageBody::PairRequest(inner.clone()).encode_to_vec();

    let decoded = extract_inner_plaintext_message(&encoded)
        .expect("extract_inner_plaintext_message should succeed");

    assert!(
        matches!(decoded, MessageBody::PairRequest(m) if m == inner),
        "plaintext extract must round-trip the inner MessageBody"
    );
}

#[test]
fn test_extract_inner_plaintext_message_rejects_garbage() {
    let err = extract_inner_plaintext_message(b"not a valid protobuf")
        .expect_err("garbage bytes must not decode as a MessageBody");

    assert!(matches!(err, crate::Error::ProtobufDecode(_)));
}