use crate::error::ProtocolError;
use crate::Result;
use blvm_secp256k1::ellswift::{ellswift_create, ellswift_xdh};
use chacha20poly1305::{
aead::{Aead, AeadCore, KeyInit},
ChaCha20Poly1305, Key, Nonce,
};
use getrandom::getrandom;
use sha2::{Digest, Sha256};
use std::borrow::Cow;
pub struct V2Transport {
send_key: [u8; 32],
recv_key: [u8; 32],
send_cipher: ChaCha20Poly1305,
recv_cipher: ChaCha20Poly1305,
send_nonce: u64,
recv_nonce: u64,
}
pub enum V2Handshake {
Initiator {
private_key: [u8; 32],
ellswift: [u8; 64],
},
Responder {
private_key: [u8; 32],
initiator_ellswift: Option<[u8; 64]>,
},
}
const REKEY_INTERVAL: u64 = 224;
fn rekey_derive_next_key(key: &[u8; 32], rekey_epoch: u64) -> Result<[u8; 32]> {
let mut rekey_nonce = [0u8; 12];
rekey_nonce[0..4].copy_from_slice(&[0xff, 0xff, 0xff, 0xff]);
rekey_nonce[4..12].copy_from_slice(&rekey_epoch.to_le_bytes());
let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
let ct = cipher
.encrypt(Nonce::from_slice(&rekey_nonce), [0u8; 32].as_slice())
.map_err(|e| {
ProtocolError::Consensus(blvm_consensus::error::ConsensusError::Serialization(
Cow::Owned(format!("BIP324 rekey encrypt failed: {e}")),
))
})?;
let mut new_key = [0u8; 32];
new_key.copy_from_slice(&ct[..32]);
Ok(new_key)
}
impl V2Transport {
pub fn new(send_key: [u8; 32], recv_key: [u8; 32]) -> Self {
let send_cipher = ChaCha20Poly1305::new(&Key::from_slice(&send_key));
let recv_cipher = ChaCha20Poly1305::new(&Key::from_slice(&recv_key));
Self {
send_key,
recv_key,
send_cipher,
recv_cipher,
send_nonce: 0,
recv_nonce: 0,
}
}
pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>> {
let mut nonce_bytes = [0u8; 12];
nonce_bytes[..8].copy_from_slice(&self.send_nonce.to_le_bytes());
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = self.send_cipher.encrypt(nonce, plaintext).map_err(|e| {
ProtocolError::Consensus(blvm_consensus::error::ConsensusError::Serialization(
Cow::Owned(format!("Encryption failed: {}", e)),
))
})?;
self.send_nonce += 1;
if self.send_nonce.is_multiple_of(REKEY_INTERVAL) {
let rekey_epoch = (self.send_nonce / REKEY_INTERVAL) - 1;
self.send_key = rekey_derive_next_key(&self.send_key, rekey_epoch)?;
self.send_cipher = ChaCha20Poly1305::new(Key::from_slice(&self.send_key));
}
let mut packet = Vec::with_capacity(20 + ciphertext.len());
if ciphertext.len() < 16 {
return Err(ProtocolError::Consensus(
blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
"Ciphertext too short".to_string(),
)),
));
}
let tag_start = ciphertext.len() - 16;
packet.extend_from_slice(&ciphertext[tag_start..]);
let payload_len = ciphertext.len() - 16; if payload_len > 0xFFFFFF {
return Err(ProtocolError::Consensus(
blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
"Payload too large".to_string(),
)),
));
}
let len_bytes = (payload_len as u32).to_le_bytes();
packet.extend_from_slice(&len_bytes[..3]);
packet.push(0);
packet.extend_from_slice(&ciphertext[..tag_start]);
Ok(packet)
}
pub fn decrypt(&mut self, packet: &[u8]) -> Result<Vec<u8>> {
if packet.len() < 20 {
return Err(ProtocolError::Consensus(
blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
"Packet too short".to_string(),
)),
));
}
let tag = &packet[0..16];
let length_bytes = [packet[16], packet[17], packet[18], 0];
let payload_len = u32::from_le_bytes(length_bytes) as usize;
let payload_start = 20;
let payload_end = payload_start + payload_len;
if packet.len() < payload_end {
return Err(ProtocolError::Consensus(
blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
"Packet incomplete".to_string(),
)),
));
}
let mut ciphertext = Vec::with_capacity(payload_len + 16);
ciphertext.extend_from_slice(&packet[payload_start..payload_end]);
ciphertext.extend_from_slice(tag);
let mut nonce_bytes = [0u8; 12];
nonce_bytes[..8].copy_from_slice(&self.recv_nonce.to_le_bytes());
let nonce = Nonce::from_slice(&nonce_bytes);
let plaintext = self
.recv_cipher
.decrypt(nonce, ciphertext.as_slice())
.map_err(|e| {
ProtocolError::Consensus(blvm_consensus::error::ConsensusError::Serialization(
Cow::Owned(format!("Decryption failed: {}", e)),
))
})?;
self.recv_nonce += 1;
if self.recv_nonce.is_multiple_of(REKEY_INTERVAL) {
let rekey_epoch = (self.recv_nonce / REKEY_INTERVAL) - 1;
self.recv_key = rekey_derive_next_key(&self.recv_key, rekey_epoch)?;
self.recv_cipher = ChaCha20Poly1305::new(Key::from_slice(&self.recv_key));
}
Ok(plaintext)
}
}
impl V2Handshake {
pub fn new_initiator() -> (Vec<u8>, Self) {
let mut key_bytes = [0u8; 32];
getrandom(&mut key_bytes).expect("Failed to generate random bytes");
let mut aux_rand = [0u8; 32];
getrandom(&mut aux_rand).expect("Failed to generate aux_rand");
let ellswift = ellswift_create(&key_bytes, Some(&aux_rand))
.expect("Failed to create ElligatorSwift encoding");
(
ellswift.to_vec(),
Self::Initiator {
private_key: key_bytes,
ellswift,
},
)
}
pub fn new_responder() -> Self {
let mut key_bytes = [0u8; 32];
getrandom(&mut key_bytes).expect("Failed to generate random bytes");
Self::Responder {
private_key: key_bytes,
initiator_ellswift: None,
}
}
pub fn process_initiator_message(
&mut self,
initiator_msg: &[u8],
) -> Result<(Vec<u8>, V2Transport)> {
if initiator_msg.len() != 64 {
return Err(ProtocolError::Consensus(
blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
"Invalid initiator message length".to_string(),
)),
));
}
let mut initiator_ell64 = [0u8; 64];
initiator_ell64.copy_from_slice(initiator_msg);
let responder_private = match self {
Self::Responder { private_key, .. } => *private_key,
_ => {
return Err(ProtocolError::Consensus(
blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
"Not a responder handshake".to_string(),
)),
));
}
};
let mut aux_rand = [0u8; 32];
getrandom(&mut aux_rand).expect("Failed to generate aux_rand");
let responder_ell64 = ellswift_create(&responder_private, Some(&aux_rand))
.expect("Failed to create ElligatorSwift encoding");
let shared_x = ellswift_xdh(
&initiator_ell64,
&responder_ell64,
&responder_private,
true, )
.expect("ElligatorSwift ECDH failed");
let send_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_send");
let recv_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_recv");
let transport = V2Transport::new(send_key, recv_key);
if let Self::Responder {
initiator_ellswift: ref mut iell,
..
} = self
{
*iell = Some(initiator_ell64);
}
Ok((responder_ell64.to_vec(), transport))
}
pub fn complete_handshake(self, responder_msg: &[u8]) -> Result<V2Transport> {
if responder_msg.len() != 64 {
return Err(ProtocolError::Consensus(
blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
"Invalid responder message length".to_string(),
)),
));
}
let mut responder_ell64 = [0u8; 64];
responder_ell64.copy_from_slice(responder_msg);
let (private_key, initiator_ell64) = match self {
Self::Initiator {
private_key,
ellswift,
} => (private_key, ellswift),
_ => {
return Err(ProtocolError::Consensus(
blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
"Not an initiator handshake".to_string(),
)),
));
}
};
let shared_x = ellswift_xdh(
&initiator_ell64,
&responder_ell64,
&private_key,
false, )
.expect("ElligatorSwift ECDH failed");
let send_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_send");
let recv_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_recv");
Ok(V2Transport::new(send_key, recv_key))
}
}
fn hkdf_sha256(ikm: &[u8], info: &[u8]) -> [u8; 32] {
use hkdf::Hkdf;
let hk = Hkdf::<sha2::Sha256>::new(None, ikm);
let mut okm = [0u8; 32];
hk.expand(info, &mut okm)
.expect("HKDF expansion failed (should never happen for 32-byte output)");
okm
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_v2_transport_encrypt_decrypt() {
let key = [0x42; 32];
let mut transport = V2Transport::new(key, key);
let plaintext = b"Hello, Bitcoin!";
let encrypted = transport.encrypt(plaintext).unwrap();
let decrypted = transport.decrypt(&encrypted).unwrap();
assert_eq!(plaintext, decrypted.as_slice());
}
#[test]
fn test_v2_transport_rekey_round_trip() {
let key = [0x11; 32];
let mut enc = V2Transport::new(key, key);
let mut dec = V2Transport::new(key, key);
for i in 0..300 {
let pt = format!("msg{i}").into_bytes();
let pkt = enc.encrypt(&pt).unwrap();
let out = dec.decrypt(&pkt).unwrap();
assert_eq!(pt, out, "round-trip failed at i={i}");
}
}
#[test]
fn test_elligator_swift_encode_decode() {
use blvm_secp256k1::ellswift::{ellswift_create, ellswift_xdh};
let seckey = [0x01u8; 32];
let ell = ellswift_create(&seckey, None).expect("valid key");
assert_eq!(ell.len(), 64);
let shared = ellswift_xdh(&ell, &ell, &seckey, false);
assert!(shared.is_some(), "XDH should not fail on valid inputs");
}
}