use crate::room_state::privacy::SealedBytes;
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use curve25519_dalek::edwards::CompressedEdwardsY;
use ed25519_dalek::{SigningKey, VerifyingKey};
use sha2::{Digest, Sha256, Sha512};
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519EphemeralSecret};
const ECIES_EPHEMERAL_DOMAIN: &str = "river-ecies-ephemeral-v1 2026-05";
fn ed25519_to_x25519_public_key(ed25519_pk: &VerifyingKey) -> X25519PublicKey {
let ed_y = CompressedEdwardsY(ed25519_pk.to_bytes())
.decompress()
.expect("Invalid Edwards point");
let mont_u = ed_y.to_montgomery().to_bytes();
X25519PublicKey::from(mont_u)
}
fn ed25519_to_x25519_private_key(ed25519_sk: &SigningKey) -> X25519EphemeralSecret {
let h = Sha512::digest(ed25519_sk.to_bytes());
let mut key = [0u8; 32];
key.copy_from_slice(&h[..32]);
key[0] &= 248;
key[31] &= 127;
key[31] |= 64;
X25519EphemeralSecret::from(key)
}
pub fn decrypt(
recipient_private_key: &SigningKey,
sender_public_key: &X25519PublicKey,
ciphertext: &[u8],
nonce: &[u8; 12],
) -> Vec<u8> {
let recipient_x25519_private_key = ed25519_to_x25519_private_key(recipient_private_key);
let shared_secret = recipient_x25519_private_key.diffie_hellman(sender_public_key);
let symmetric_key = Sha256::digest(shared_secret.as_bytes());
let cipher = Aes256Gcm::new_from_slice(&symmetric_key).expect("Failed to create cipher");
cipher
.decrypt(&Nonce::from(*nonce), ciphertext.as_ref())
.expect("decryption failure!")
}
pub fn decrypt_with_symmetric_key(
key: &[u8; 32],
ciphertext: &[u8],
nonce: &[u8; 12],
) -> Result<Vec<u8>, String> {
let cipher =
Aes256Gcm::new_from_slice(key).map_err(|e| format!("Failed to create cipher: {}", e))?;
let nonce_obj = Nonce::from(*nonce);
cipher
.decrypt(&nonce_obj, ciphertext)
.map_err(|e| format!("Decryption failed: {}", e))
}
pub fn encrypt_secret_for_member(
secret: &[u8; 32],
member_public_key: &VerifyingKey,
) -> (Vec<u8>, [u8; 12], X25519PublicKey) {
let mut hasher = blake3::Hasher::new();
hasher.update(ECIES_EPHEMERAL_DOMAIN.as_bytes());
hasher.update(secret);
hasher.update(member_public_key.as_bytes());
let ephemeral_seed: [u8; 32] = *hasher.finalize().as_bytes();
let sender_private_key = X25519EphemeralSecret::from(ephemeral_seed);
let sender_public_key = X25519PublicKey::from(&sender_private_key);
let recipient_x25519_public_key = ed25519_to_x25519_public_key(member_public_key);
let shared_secret = sender_private_key.diffie_hellman(&recipient_x25519_public_key);
let symmetric_key = Sha256::digest(shared_secret.as_bytes());
let nonce: [u8; 12] = [0u8; 12]; let cipher = Aes256Gcm::new_from_slice(&symmetric_key).expect("Failed to create cipher");
let ciphertext = cipher
.encrypt(&Nonce::from(nonce), secret.as_slice())
.expect("encryption failure!");
(ciphertext, nonce, sender_public_key)
}
pub fn decrypt_secret_from_member_blob_raw(
ciphertext: &[u8],
nonce: &[u8; 12],
ephemeral_sender_key_bytes: &[u8; 32],
member_private_key: &SigningKey,
) -> Result<[u8; 32], String> {
let ephemeral = X25519PublicKey::from(*ephemeral_sender_key_bytes);
decrypt_secret_from_member_blob(ciphertext, nonce, &ephemeral, member_private_key)
}
pub fn decrypt_secret_from_member_blob(
ciphertext: &[u8],
nonce: &[u8; 12],
ephemeral_sender_key: &X25519PublicKey,
member_private_key: &SigningKey,
) -> Result<[u8; 32], String> {
let decrypted = decrypt(member_private_key, ephemeral_sender_key, ciphertext, nonce);
if decrypted.len() != 32 {
return Err(format!(
"Decrypted secret has invalid length: {} (expected 32)",
decrypted.len()
));
}
let mut secret = [0u8; 32];
secret.copy_from_slice(&decrypted);
Ok(secret)
}
pub fn unseal_bytes(
sealed: &SealedBytes,
secret_key: Option<&[u8; 32]>,
) -> Result<Vec<u8>, String> {
match sealed {
SealedBytes::Public { value } => Ok(value.clone()),
SealedBytes::Private {
ciphertext, nonce, ..
} => {
let key = secret_key.ok_or("Secret key required to unseal private data")?;
decrypt_with_symmetric_key(key, ciphertext, nonce)
}
}
}
pub fn unseal_bytes_with_secrets(
sealed: &SealedBytes,
secrets: &std::collections::HashMap<u32, [u8; 32]>,
) -> Result<Vec<u8>, String> {
match sealed {
SealedBytes::Public { value } => Ok(value.clone()),
SealedBytes::Private {
ciphertext,
nonce,
secret_version,
..
} => {
let key = secrets.get(secret_version).ok_or_else(|| {
format!(
"Secret version {} not available (have versions: {:?})",
secret_version,
secrets.keys().collect::<Vec<_>>()
)
})?;
decrypt_with_symmetric_key(key, ciphertext, nonce)
}
}
}
#[cfg(feature = "ecies-randomized")]
pub fn generate_room_secret() -> [u8; 32] {
rand::random::<[u8; 32]>()
}
#[cfg(feature = "ecies-randomized")]
pub fn encrypt_with_symmetric_key(key: &[u8; 32], plaintext: &[u8]) -> (Vec<u8>, [u8; 12]) {
let cipher = Aes256Gcm::new_from_slice(key).expect("Failed to create cipher");
let nonce_bytes = rand::random::<[u8; 12]>();
let nonce = Nonce::from(nonce_bytes);
let ciphertext = cipher
.encrypt(&nonce, plaintext)
.expect("Symmetric encryption failure");
(ciphertext, nonce_bytes)
}
#[cfg(feature = "ecies-randomized")]
pub fn seal_bytes(plaintext: &[u8], secret_key: &[u8; 32], secret_version: u32) -> SealedBytes {
let (ciphertext, nonce) = encrypt_with_symmetric_key(secret_key, plaintext);
let declared_len_bytes = plaintext.len() as u32;
SealedBytes::Private {
ciphertext,
nonce,
secret_version,
declared_len_bytes,
}
}
pub const DM_ENVELOPE_OVERHEAD_BYTES: usize = 32 + 12 + 16;
#[cfg(feature = "ecies-randomized")]
pub fn seal_dm_for_recipient(recipient_vk: &VerifyingKey, plaintext: &[u8]) -> Vec<u8> {
let ephemeral_seed: [u8; 32] = rand::random();
let ephemeral_sk = X25519EphemeralSecret::from(ephemeral_seed);
let ephemeral_pub = X25519PublicKey::from(&ephemeral_sk);
let recipient_x25519_pub = ed25519_to_x25519_public_key(recipient_vk);
let shared_secret = ephemeral_sk.diffie_hellman(&recipient_x25519_pub);
let symmetric_key = Sha256::digest(shared_secret.as_bytes());
let nonce_bytes: [u8; 12] = rand::random();
let cipher = Aes256Gcm::new_from_slice(&symmetric_key).expect("Failed to create cipher");
let ciphertext = cipher
.encrypt(&Nonce::from(nonce_bytes), plaintext)
.expect("AES-GCM encryption failure");
let mut envelope = Vec::with_capacity(32 + 12 + ciphertext.len());
envelope.extend_from_slice(ephemeral_pub.as_bytes());
envelope.extend_from_slice(&nonce_bytes);
envelope.extend_from_slice(&ciphertext);
envelope
}
pub fn unseal_dm_from_sender(
recipient_sk: &SigningKey,
envelope: &[u8],
) -> Result<Vec<u8>, String> {
if envelope.len() < 32 + 12 {
return Err(format!(
"DM envelope too short: {} bytes (need at least {})",
envelope.len(),
32 + 12
));
}
let mut ephemeral_pub_bytes = [0u8; 32];
ephemeral_pub_bytes.copy_from_slice(&envelope[..32]);
let ephemeral_pub = X25519PublicKey::from(ephemeral_pub_bytes);
let mut nonce_bytes = [0u8; 12];
nonce_bytes.copy_from_slice(&envelope[32..44]);
let ciphertext = &envelope[44..];
let recipient_x25519_sk = ed25519_to_x25519_private_key(recipient_sk);
let shared_secret = recipient_x25519_sk.diffie_hellman(&ephemeral_pub);
let symmetric_key = Sha256::digest(shared_secret.as_bytes());
let cipher = Aes256Gcm::new_from_slice(&symmetric_key)
.map_err(|e| format!("Failed to create cipher: {}", e))?;
cipher
.decrypt(&Nonce::from(nonce_bytes), ciphertext)
.map_err(|e| {
format!(
"DM decryption failed (wrong recipient or tampered bytes): {}",
e
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn fixed_signing_key(seed_byte: u8) -> SigningKey {
SigningKey::from_bytes(&[seed_byte; 32])
}
#[test]
fn encrypt_secret_for_member_round_trip_deterministic_inputs() {
let member_sk = fixed_signing_key(7);
let member_vk = VerifyingKey::from(&member_sk);
let secret = [13u8; 32];
let (ciphertext, nonce, ephemeral_key) = encrypt_secret_for_member(&secret, &member_vk);
let decrypted =
decrypt_secret_from_member_blob(&ciphertext, &nonce, &ephemeral_key, &member_sk)
.unwrap();
assert_eq!(decrypted, secret);
}
#[test]
fn encrypt_secret_for_member_is_deterministic() {
let member_sk = fixed_signing_key(7);
let member_vk = VerifyingKey::from(&member_sk);
let secret = [13u8; 32];
let (ct1, n1, ek1) = encrypt_secret_for_member(&secret, &member_vk);
let (ct2, n2, ek2) = encrypt_secret_for_member(&secret, &member_vk);
assert_eq!(ct1, ct2, "ciphertext must be deterministic");
assert_eq!(n1, n2, "nonce must be deterministic");
assert_eq!(
ek1.as_bytes(),
ek2.as_bytes(),
"ephemeral pubkey must be deterministic"
);
let decrypted = decrypt_secret_from_member_blob(&ct1, &n1, &ek1, &member_sk).unwrap();
assert_eq!(decrypted, secret);
}
#[test]
fn encrypt_secret_for_member_distinguishes_recipients() {
let member_sk_a = fixed_signing_key(1);
let member_vk_a = VerifyingKey::from(&member_sk_a);
let member_sk_b = fixed_signing_key(2);
let member_vk_b = VerifyingKey::from(&member_sk_b);
let secret = [99u8; 32];
let (ct_a, _, ek_a) = encrypt_secret_for_member(&secret, &member_vk_a);
let (ct_b, _, ek_b) = encrypt_secret_for_member(&secret, &member_vk_b);
assert_ne!(
ct_a, ct_b,
"different recipients must produce different ciphertexts"
);
assert_ne!(
ek_a.as_bytes(),
ek_b.as_bytes(),
"different recipients must produce different ephemeral pubkeys"
);
}
#[test]
fn encrypt_secret_for_member_distinguishes_secrets() {
let member_sk = fixed_signing_key(7);
let member_vk = VerifyingKey::from(&member_sk);
let secret_v0 = [0xA0u8; 32];
let secret_v1 = [0xB0u8; 32];
let (ct0, _, ek0) = encrypt_secret_for_member(&secret_v0, &member_vk);
let (ct1, _, ek1) = encrypt_secret_for_member(&secret_v1, &member_vk);
assert_ne!(
ct0, ct1,
"different secrets must produce different ciphertexts"
);
assert_ne!(
ek0.as_bytes(),
ek1.as_bytes(),
"different secrets must produce different ephemeral pubkeys"
);
}
#[test]
fn encrypt_secret_for_member_known_answer() {
let member_sk = fixed_signing_key(3);
let member_vk = VerifyingKey::from(&member_sk);
let secret = [42u8; 32];
let (ciphertext, nonce, ephemeral) = encrypt_secret_for_member(&secret, &member_vk);
assert_eq!(
hex::encode(&ciphertext),
"ae3a2f82fc8982c6014649b76c19ea0920d0eaf9bf8f2690ddf7dd70bda39bc54d829d924dc0afb8621639430515c78d",
"ciphertext byte vector changed — see test docstring"
);
assert_eq!(nonce, [0u8; 12], "nonce must remain all-zero");
assert_eq!(
hex::encode(ephemeral.as_bytes()),
"19f806d18ca5b14914ebd6831cf896369030b1e9c8c36ae60f7156317021aa12",
"ephemeral pubkey byte vector changed — see test docstring"
);
let decrypted =
decrypt_secret_from_member_blob(&ciphertext, &nonce, &ephemeral, &member_sk).unwrap();
assert_eq!(decrypted, secret);
}
#[test]
fn decrypt_random_nonce_blob_backward_compat() {
use aes_gcm::aead::{Aead, KeyInit};
use x25519_dalek::StaticSecret;
let member_sk = fixed_signing_key(7);
let member_vk = VerifyingKey::from(&member_sk);
let original_secret = [13u8; 32];
let old_ephemeral_priv = StaticSecret::from([0x55u8; 32]);
let old_ephemeral_pub = X25519PublicKey::from(&old_ephemeral_priv);
let recipient_x25519 = ed25519_to_x25519_public_key(&member_vk);
let shared = old_ephemeral_priv.diffie_hellman(&recipient_x25519);
let sym_key = Sha256::digest(shared.as_bytes());
let old_nonce: [u8; 12] = [0xAB; 12];
let cipher = Aes256Gcm::new_from_slice(&sym_key).unwrap();
let old_ct = cipher
.encrypt(&Nonce::from(old_nonce), original_secret.as_slice())
.unwrap();
let decrypted =
decrypt_secret_from_member_blob(&old_ct, &old_nonce, &old_ephemeral_pub, &member_sk)
.unwrap();
assert_eq!(decrypted, original_secret);
}
}
#[cfg(all(test, feature = "ecies-randomized"))]
mod tests_randomized {
use super::*;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
#[test]
fn symmetric_round_trip() {
let key = generate_room_secret();
let plaintext = b"Room secret message";
let (ciphertext, nonce) = encrypt_with_symmetric_key(&key, plaintext);
let decrypted = decrypt_with_symmetric_key(&key, &ciphertext, &nonce).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn seal_unseal_private_round_trip() {
let secret_key = generate_room_secret();
let plaintext = b"Private nickname";
let secret_version = 5;
let sealed = seal_bytes(plaintext, &secret_key, secret_version);
let unsealed = unseal_bytes(&sealed, Some(&secret_key)).unwrap();
assert_eq!(unsealed, plaintext);
}
#[test]
fn encrypt_decrypt_secret_for_member_round_trip_randomized_inputs() {
let mut rng = OsRng;
let member_sk = SigningKey::generate(&mut rng);
let member_vk = VerifyingKey::from(&member_sk);
let original_secret = generate_room_secret();
let (ciphertext, nonce, ephemeral_key) =
encrypt_secret_for_member(&original_secret, &member_vk);
let decrypted_secret =
decrypt_secret_from_member_blob(&ciphertext, &nonce, &ephemeral_key, &member_sk)
.unwrap();
assert_eq!(decrypted_secret, original_secret);
}
#[test]
fn dm_envelope_round_trip() {
let mut rng = OsRng;
let recipient_sk = SigningKey::generate(&mut rng);
let recipient_vk = VerifyingKey::from(&recipient_sk);
let plaintext = b"hello, this is a direct message body";
let envelope = seal_dm_for_recipient(&recipient_vk, plaintext);
let decrypted = unseal_dm_from_sender(&recipient_sk, &envelope).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn dm_envelope_is_per_call_unique() {
let mut rng = OsRng;
let recipient_sk = SigningKey::generate(&mut rng);
let recipient_vk = VerifyingKey::from(&recipient_sk);
let plaintext = b"identical plaintext";
let env1 = seal_dm_for_recipient(&recipient_vk, plaintext);
let env2 = seal_dm_for_recipient(&recipient_vk, plaintext);
assert_ne!(
env1, env2,
"DM envelopes must differ across calls (fresh randomness per message)"
);
assert_eq!(
unseal_dm_from_sender(&recipient_sk, &env1).unwrap(),
plaintext
);
assert_eq!(
unseal_dm_from_sender(&recipient_sk, &env2).unwrap(),
plaintext
);
}
#[test]
fn dm_envelope_wrong_recipient_fails() {
let mut rng = OsRng;
let recipient_sk = SigningKey::generate(&mut rng);
let recipient_vk = VerifyingKey::from(&recipient_sk);
let other_sk = SigningKey::generate(&mut rng);
let envelope = seal_dm_for_recipient(&recipient_vk, b"secret stuff");
assert!(unseal_dm_from_sender(&other_sk, &envelope).is_err());
}
#[test]
fn dm_envelope_truncated_fails() {
let mut rng = OsRng;
let recipient_sk = SigningKey::generate(&mut rng);
assert!(unseal_dm_from_sender(&recipient_sk, &[0u8; 10]).is_err());
assert!(unseal_dm_from_sender(&recipient_sk, &[0u8; 43]).is_err());
}
#[test]
fn dm_envelope_tampered_ciphertext_fails() {
let mut rng = OsRng;
let recipient_sk = SigningKey::generate(&mut rng);
let recipient_vk = VerifyingKey::from(&recipient_sk);
let mut envelope = seal_dm_for_recipient(&recipient_vk, b"some body");
let last = envelope.len() - 1;
envelope[last] ^= 0x01;
assert!(unseal_dm_from_sender(&recipient_sk, &envelope).is_err());
}
}