beaconcrypt 0.4.2

Cryptographic transport for C2 channels
Documentation
use beaconcrypt::*;
use libsodium_rs::crypto_sign;
use serde_json::{Value, json};

fn test_register_pqxdh_beacon(
	server: &mut BeaconCryptPqxdh,
	beacon: &mut BeaconCryptPqxdh,
) -> Vec<u8> {
	let message = [0xFFu8; 32];

	let phase_1 = beacon.get_registration_bundle().unwrap();
	let reg_out = server.get_shared_secret(&phase_1).unwrap();
	let phase2 = server
		.build_registration_response(reg_out, Some(&message))
		.unwrap();
	beacon.finish_registration(&phase2.serialized).unwrap()
}

fn assert_exchange(
	server: &mut BeaconCryptPqxdh,
	beacon: &mut BeaconCryptPqxdh,
	kid: u64,
	server_message: &[u8],
	beacon_message: &[u8],
) {
	let encrypted = server.encrypt_message(server_message, kid).unwrap();
	assert_eq!(
		beacon.decrypt_message(&encrypted).unwrap().plaintext,
		server_message
	);

	let encrypted = beacon
		.encrypt_message(beacon_message, beacon.server_kid())
		.unwrap();
	assert_eq!(
		server.decrypt_message(&encrypted).unwrap().plaintext,
		beacon_message
	);
}

#[test]
fn server_from_seed() {
	let empty = [0u8; ED25519_SEED_SIZE];
	let seeded = crypto_sign::KeyPair::from_seed(&empty).unwrap();
	let server = BeaconCryptPqxdh::new(false, 0, None, Some(&empty));
	assert_eq!(
		seeded.secret_key.as_bytes(),
		server.identity_sk().as_bytes()
	);
	assert_eq!(
		seeded.public_key.as_bytes(),
		server.identity_pk().as_bytes()
	);
}

#[test]
fn server_can_register_multiple() {
	let mut server = BeaconCryptPqxdh::new(false, 0, None, None);
	let server_id = server.identity_pk().to_owned();

	let mut b1 = BeaconCryptPqxdh::new(true, 0, Some(server_id.as_bytes()), None);
	let b1_reg = test_register_pqxdh_beacon(&mut server, &mut b1);
	let mut b2 = BeaconCryptPqxdh::new(true, 0, Some(server_id.as_bytes()), None);
	let b2_reg = test_register_pqxdh_beacon(&mut server, &mut b2);

	assert_eq!(b1_reg, b2_reg);
}

#[test]
fn recreated_server_decrypts_beacon_response() {
	let seed = [0x41; ED25519_SEED_SIZE];
	let mut server = BeaconCryptPqxdh::new(false, 0, None, Some(&seed));
	let server_id = server.identity_pk().to_owned();
	let mut beacon = BeaconCryptPqxdh::new(true, 0, Some(server_id.as_bytes()), None);

	test_register_pqxdh_beacon(&mut server, &mut beacon);
	let request = server.encrypt_message(b"request", 1).unwrap();
	assert_eq!(
		beacon.decrypt_message(&request).unwrap().plaintext,
		b"request"
	);

	let state = server.export_state().unwrap();
	drop(server);

	let response = beacon
		.encrypt_message(b"response", beacon.server_kid())
		.unwrap();
	let mut server: BeaconCryptPqxdh = ProviderServer::from_state(0, Some(&seed), state);
	assert_eq!(
		server.decrypt_message(&response).unwrap().plaintext,
		b"response"
	);
}

#[test]
fn known_ids_round_trip_and_continue_each_session() {
	let seed = [0x51; ED25519_SEED_SIZE];
	let mut server = BeaconCryptPqxdh::new(false, 0, None, Some(&seed));
	let server_id = server.identity_pk().to_owned();
	let mut b1 = BeaconCryptPqxdh::new(true, 0, Some(server_id.as_bytes()), None);
	let mut b2 = BeaconCryptPqxdh::new(true, 0, Some(server_id.as_bytes()), None);

	test_register_pqxdh_beacon(&mut server, &mut b1);
	test_register_pqxdh_beacon(&mut server, &mut b2);
	assert_exchange(&mut server, &mut b1, 1, b"server one", b"beacon one");
	assert_exchange(&mut server, &mut b2, 2, b"server two", b"beacon two");

	let state = server.export_state().unwrap();
	let json: Value = serde_json::from_str(&state).unwrap();
	for kid in ["1", "2"] {
		let encoded_pk = json[kid]["pk"].as_array().unwrap();
		assert_eq!(encoded_pk.len(), crypto_sign::PUBLICKEYBYTES + 1);
		assert_eq!(encoded_pk[0], json!(u8::from(SignType::Ed25519)));
		assert!(json[kid]["ratchet"].is_object());
	}

	let mut restored: BeaconCryptPqxdh = ProviderServer::from_state(0, Some(&seed), state);
	assert_eq!(restored.identity_pk(), &server_id);
	assert_eq!(restored.server_kid(), 2);
	assert_eq!(restored.pk_by_kid(1), server.pk_by_kid(1));
	assert_eq!(restored.pk_by_kid(2), server.pk_by_kid(2));

	assert_exchange(
		&mut restored,
		&mut b1,
		1,
		b"server one restored",
		b"beacon one restored",
	);
	assert_exchange(
		&mut restored,
		&mut b2,
		2,
		b"server two restored",
		b"beacon two restored",
	);

	let mut b3 = BeaconCryptPqxdh::new(true, 0, Some(server_id.as_bytes()), None);
	test_register_pqxdh_beacon(&mut restored, &mut b3);
	assert_eq!(restored.server_kid(), 3);
	assert!(restored.pk_by_kid(3).is_some());
}

#[test]
fn known_ids_round_trip_preserves_cached_out_of_order_receive_keys() {
	let seed = [0x71; ED25519_SEED_SIZE];
	let mut server = BeaconCryptPqxdh::new(false, 0, None, Some(&seed));
	let server_id = server.identity_pk().to_owned();
	let mut beacon = BeaconCryptPqxdh::new(true, 0, Some(server_id.as_bytes()), None);
	test_register_pqxdh_beacon(&mut server, &mut beacon);

	let first = beacon.encrypt_message(b"first", 0).unwrap();
	let second = beacon.encrypt_message(b"second", 0).unwrap();
	let third = beacon.encrypt_message(b"third", 0).unwrap();
	assert_eq!(server.decrypt_message(&third).unwrap().plaintext, b"third");

	let state = server.export_state().unwrap();
	let encoded: Value = serde_json::from_str(&state).unwrap();
	let cached = encoded["1"]["ratchet"]["recv_past"].as_object().unwrap();
	assert_eq!(cached.len(), 2);
	assert!(cached.contains_key("1"));
	assert!(cached.contains_key("2"));

	let mut restored: BeaconCryptPqxdh = ProviderServer::from_state(0, Some(&seed), state);
	assert_eq!(
		restored.decrypt_message(&first).unwrap().plaintext,
		b"first"
	);
	assert_eq!(
		restored.decrypt_message(&second).unwrap().plaintext,
		b"second"
	);
}

#[test]
fn invalid_known_ids_state_is_rejected() {
	let seed = [0x61; ED25519_SEED_SIZE];
	let mut server = BeaconCryptPqxdh::new(false, 0, None, Some(&seed));
	let server_id = server.identity_pk().to_owned();
	let mut beacon = BeaconCryptPqxdh::new(true, 0, Some(server_id.as_bytes()), None);
	test_register_pqxdh_beacon(&mut server, &mut beacon);

	let state = server.export_state().unwrap();
	let parsed: Value = serde_json::from_str(&state).unwrap();

	let mut wrong_key_type = parsed.clone();
	wrong_key_type["1"]["pk"][0] = json!(u8::from(SignType::MlDsa87));
	let mut short_key = parsed.clone();
	short_key["1"]["pk"].as_array_mut().unwrap().pop();
	let mut malformed_ratchet = parsed;
	malformed_ratchet["1"]["ratchet"]["send_key"][0] = json!(0);

	for malformed in [wrong_key_type, short_key, malformed_ratchet] {
		let malformed = serde_json::to_string(&malformed).unwrap();
		assert!(
			std::panic::catch_unwind(|| {
				let _: BeaconCryptPqxdh = ProviderServer::from_state(0, Some(&seed), malformed);
			})
			.is_err()
		);
	}

	let entry = &state[1..state.len() - 1];
	let duplicate_kid = format!("{{{entry},{entry}}}");
	assert!(
		std::panic::catch_unwind(|| {
			let _: BeaconCryptPqxdh = ProviderServer::from_state(0, Some(&seed), duplicate_kid);
		})
		.is_err()
	);

	assert_exchange(
		&mut server,
		&mut beacon,
		1,
		b"server remains live",
		b"beacon remains live",
	);
}

#[test]
fn empty_known_ids_state_round_trips_without_lowering_the_kid_counter() {
	let server = BeaconCryptPqxdh::new(false, 7, None, None);
	let state = server.export_state().unwrap();
	assert_eq!(state, "{}");

	let restored: BeaconCryptPqxdh = ProviderServer::from_state(7, None, state);
	assert_eq!(restored.server_kid(), 7);
	assert!(restored.pk_by_kid(1).is_none());
}

#[test]
fn fallible_state_restore_rejects_invalid_seed_and_state() {
	let short_seed = [0u8; ED25519_SEED_SIZE - 1];
	assert!(
		<BeaconCryptPqxdh as ProviderServer>::try_from_state(0, Some(&short_seed), "{}").is_none()
	);
	assert!(<BeaconCryptPqxdh as ProviderServer>::try_from_state(0, None, "not JSON").is_none());
}