use crate::compat::CompatError;
use crate::primitives::aes_cbc::{aes_cbc_decrypt, aes_cbc_encrypt};
use crate::primitives::big_number::Endian;
use crate::primitives::hash::{sha256_hmac, sha512};
use crate::primitives::private_key::PrivateKey;
use crate::primitives::public_key::PublicKey;
use crate::primitives::random::random_bytes;
pub struct ECIES;
fn derive_electrum_keys(
priv_key: &PrivateKey,
pub_key: &PublicKey,
) -> ([u8; 16], [u8; 16], [u8; 32]) {
let p = pub_key.point().mul(priv_key.bn());
let s_buf = p.to_der(true); let hash = sha512(&s_buf);
let mut iv = [0u8; 16];
let mut k_e = [0u8; 16];
let mut k_m = [0u8; 32];
iv.copy_from_slice(&hash[0..16]);
k_e.copy_from_slice(&hash[16..32]);
k_m.copy_from_slice(&hash[32..64]);
(iv, k_e, k_m)
}
fn derive_bitcore_keys(priv_key: &PrivateKey, pub_key: &PublicKey) -> ([u8; 32], [u8; 32]) {
let p = pub_key.point().mul(priv_key.bn());
let s_buf = p.get_x().to_array(Endian::Big, Some(32));
let hash = sha512(&s_buf);
let mut k_e = [0u8; 32];
let mut k_m = [0u8; 32];
k_e.copy_from_slice(&hash[0..32]);
k_m.copy_from_slice(&hash[32..64]);
(k_e, k_m)
}
impl ECIES {
pub fn electrum_encrypt(
plaintext: &[u8],
recipient_pub_key: &PublicKey,
sender_priv_key: Option<&PrivateKey>,
) -> Result<Vec<u8>, CompatError> {
let ephemeral_key = match sender_priv_key {
Some(key) => key.clone(),
None => PrivateKey::from_random()?,
};
let (iv, k_e, k_m) = derive_electrum_keys(&ephemeral_key, recipient_pub_key);
let ct = aes_cbc_encrypt(&k_e, &iv, plaintext)?;
let ephemeral_pub = ephemeral_key.to_public_key();
let r_buf = ephemeral_pub.to_der();
let mut payload = Vec::with_capacity(4 + 33 + ct.len() + 32);
payload.extend_from_slice(b"BIE1");
payload.extend_from_slice(&r_buf);
payload.extend_from_slice(&ct);
let mac = sha256_hmac(&k_m, &payload);
payload.extend_from_slice(&mac);
Ok(payload)
}
pub fn electrum_decrypt(
ciphertext: &[u8],
recipient_priv_key: &PrivateKey,
) -> Result<Vec<u8>, CompatError> {
if ciphertext.len() < 85 {
return Err(CompatError::InvalidCiphertext(format!(
"electrum ciphertext too short: {} bytes (min 85)",
ciphertext.len()
)));
}
if &ciphertext[0..4] != b"BIE1" {
return Err(CompatError::InvalidMagic);
}
let ephemeral_pub_bytes = &ciphertext[4..37];
let hmac_start = ciphertext.len() - 32;
let encrypted_data = &ciphertext[37..hmac_start];
let mac = &ciphertext[hmac_start..];
let ephemeral_pub = PublicKey::from_der_bytes(ephemeral_pub_bytes)?;
let (iv, k_e, k_m) = derive_electrum_keys(recipient_priv_key, &ephemeral_pub);
let expected_mac = sha256_hmac(&k_m, &ciphertext[0..hmac_start]);
if mac != expected_mac {
return Err(CompatError::HmacMismatch);
}
let plaintext = aes_cbc_decrypt(&k_e, &iv, encrypted_data)?;
Ok(plaintext)
}
pub fn bitcore_encrypt(
plaintext: &[u8],
recipient_pub_key: &PublicKey,
sender_priv_key: Option<&PrivateKey>,
) -> Result<Vec<u8>, CompatError> {
let ephemeral_key = match sender_priv_key {
Some(key) => key.clone(),
None => PrivateKey::from_random()?,
};
let (k_e, k_m) = derive_bitcore_keys(&ephemeral_key, recipient_pub_key);
let iv_vec = random_bytes(16);
let mut iv = [0u8; 16];
iv.copy_from_slice(&iv_vec);
let ct = aes_cbc_encrypt(&k_e, &iv, plaintext)?;
let mut c = Vec::with_capacity(16 + ct.len());
c.extend_from_slice(&iv);
c.extend_from_slice(&ct);
let mac = sha256_hmac(&k_m, &c);
let r_buf = ephemeral_key.to_public_key().to_der();
let mut result = Vec::with_capacity(33 + c.len() + 32);
result.extend_from_slice(&r_buf);
result.extend_from_slice(&c);
result.extend_from_slice(&mac);
Ok(result)
}
pub fn bitcore_decrypt(
ciphertext: &[u8],
recipient_priv_key: &PrivateKey,
) -> Result<Vec<u8>, CompatError> {
if ciphertext.len() < 97 {
return Err(CompatError::InvalidCiphertext(format!(
"bitcore ciphertext too short: {} bytes (min 97)",
ciphertext.len()
)));
}
let ephemeral_pub_bytes = &ciphertext[0..33];
let ephemeral_pub = PublicKey::from_der_bytes(ephemeral_pub_bytes)?;
let c = &ciphertext[33..ciphertext.len() - 32];
let mac = &ciphertext[ciphertext.len() - 32..];
let (k_e, k_m) = derive_bitcore_keys(recipient_priv_key, &ephemeral_pub);
let expected_mac = sha256_hmac(&k_m, c);
if mac != expected_mac {
return Err(CompatError::HmacMismatch);
}
let iv: [u8; 16] = c[0..16]
.try_into()
.map_err(|_| CompatError::InvalidCiphertext("IV extraction failed".into()))?;
let encrypted_data = &c[16..];
let plaintext = aes_cbc_decrypt(&k_e, &iv, encrypted_data)?;
Ok(plaintext)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::hash::sha256;
fn base64_decode(input: &str) -> Vec<u8> {
let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = Vec::new();
let mut buf: u32 = 0;
let mut bits: u32 = 0;
for &byte in input.as_bytes() {
if byte == b'=' {
break;
}
let val = table.iter().position(|&b| b == byte);
if let Some(v) = val {
buf = (buf << 6) | (v as u32);
bits += 6;
if bits >= 8 {
bits -= 8;
result.push((buf >> bits) as u8);
buf &= (1 << bits) - 1;
}
}
}
result
}
fn base64_encode(data: &[u8]) -> String {
let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
let mut i = 0;
while i < data.len() {
let b0 = data[i] as u32;
let b1 = if i + 1 < data.len() {
data[i + 1] as u32
} else {
0
};
let b2 = if i + 2 < data.len() {
data[i + 2] as u32
} else {
0
};
let triple = (b0 << 16) | (b1 << 8) | b2;
result.push(table[((triple >> 18) & 0x3f) as usize] as char);
result.push(table[((triple >> 12) & 0x3f) as usize] as char);
if i + 1 < data.len() {
result.push(table[((triple >> 6) & 0x3f) as usize] as char);
} else {
result.push('=');
}
if i + 2 < data.len() {
result.push(table[(triple & 0x3f) as usize] as char);
} else {
result.push('=');
}
i += 3;
}
result
}
#[allow(dead_code)]
fn hex_to_bytes(hex: &str) -> Vec<u8> {
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
.collect()
}
#[allow(dead_code)]
fn bytes_to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
#[test]
fn test_electrum_encrypt_decrypt_roundtrip() {
let sender_key = PrivateKey::from_hex(
"77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
)
.unwrap();
let recipient_key = PrivateKey::from_hex(
"2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
)
.unwrap();
let recipient_pub = recipient_key.to_public_key();
let plaintext = b"this is my test message";
let encrypted =
ECIES::electrum_encrypt(plaintext, &recipient_pub, Some(&sender_key)).unwrap();
let decrypted = ECIES::electrum_decrypt(&encrypted, &recipient_key).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_electrum_ciphertext_starts_with_bie1() {
let recipient_key = PrivateKey::from_hex(
"2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
)
.unwrap();
let recipient_pub = recipient_key.to_public_key();
let encrypted = ECIES::electrum_encrypt(b"hello", &recipient_pub, None).unwrap();
assert_eq!(
&encrypted[0..4],
b"BIE1",
"Electrum ciphertext must start with BIE1"
);
}
#[test]
fn test_electrum_hmac_rejects_tampered_ciphertext() {
let recipient_key = PrivateKey::from_hex(
"2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
)
.unwrap();
let sender_key = PrivateKey::from_hex(
"77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
)
.unwrap();
let recipient_pub = recipient_key.to_public_key();
let mut encrypted =
ECIES::electrum_encrypt(b"hello", &recipient_pub, Some(&sender_key)).unwrap();
let mid = encrypted.len() / 2;
encrypted[mid] ^= 0xff;
let result = ECIES::electrum_decrypt(&encrypted, &recipient_key);
assert!(result.is_err(), "should reject tampered ciphertext");
}
#[test]
fn test_electrum_decrypt_wrong_key_fails() {
let sender_key = PrivateKey::from_hex(
"77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
)
.unwrap();
let recipient_key = PrivateKey::from_hex(
"2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
)
.unwrap();
let wrong_key = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
let recipient_pub = recipient_key.to_public_key();
let encrypted =
ECIES::electrum_encrypt(b"secret", &recipient_pub, Some(&sender_key)).unwrap();
let result = ECIES::electrum_decrypt(&encrypted, &wrong_key);
assert!(result.is_err(), "should fail with wrong private key");
}
#[test]
fn test_electrum_cross_sdk_decrypt_alice_to_bob() {
let bob_key = PrivateKey::from_hex(
"2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
)
.unwrap();
let ciphertext = base64_decode(
"QklFMQM55QTWSSsILaluEejwOXlrBs1IVcEB4kkqbxDz4Fap53XHOt6L3tKmrXho6yj6phfoiMkBOhUldRPnEI4fSZXbvZJHgyAzxA6SoujduvJXv+A9ri3po9veilrmc8p6dwo="
);
let plaintext = ECIES::electrum_decrypt(&ciphertext, &bob_key).unwrap();
assert_eq!(
std::str::from_utf8(&plaintext).unwrap(),
"this is my test message"
);
}
#[test]
fn test_electrum_cross_sdk_decrypt_bob_to_alice() {
let alice_key = PrivateKey::from_hex(
"77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
)
.unwrap();
let ciphertext = base64_decode(
"QklFMQOGFyMXLo9Qv047K3BYJhmnJgt58EC8skYP/R2QU/U0yXXHOt6L3tKmrXho6yj6phfoiMkBOhUldRPnEI4fSZXbiaH4FsxKIOOvzolIFVAS0FplUmib2HnlAM1yP/iiPsU="
);
let plaintext = ECIES::electrum_decrypt(&ciphertext, &alice_key).unwrap();
assert_eq!(
std::str::from_utf8(&plaintext).unwrap(),
"this is my test message"
);
}
#[test]
fn test_electrum_cross_sdk_encrypt_matches_ts() {
let alice_key = PrivateKey::from_hex(
"77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
)
.unwrap();
let bob_key = PrivateKey::from_hex(
"2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
)
.unwrap();
let bob_pub = bob_key.to_public_key();
let message = b"this is my test message";
let encrypted = ECIES::electrum_encrypt(message, &bob_pub, Some(&alice_key)).unwrap();
let expected_b64 = "QklFMQM55QTWSSsILaluEejwOXlrBs1IVcEB4kkqbxDz4Fap53XHOt6L3tKmrXho6yj6phfoiMkBOhUldRPnEI4fSZXbvZJHgyAzxA6SoujduvJXv+A9ri3po9veilrmc8p6dwo=";
assert_eq!(base64_encode(&encrypted), expected_b64);
}
#[test]
fn test_bitcore_encrypt_decrypt_roundtrip() {
let sender_key = PrivateKey::from_hex(
"000000000000000000000000000000000000000000000000000000000000002a",
)
.unwrap();
let recipient_key = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000058",
)
.unwrap();
let recipient_pub = recipient_key.to_public_key();
let plaintext = sha256(b"my message is the hash of this string");
let encrypted =
ECIES::bitcore_encrypt(&plaintext, &recipient_pub, Some(&sender_key)).unwrap();
let decrypted = ECIES::bitcore_decrypt(&encrypted, &recipient_key).unwrap();
assert_eq!(decrypted, plaintext.to_vec());
}
#[test]
fn test_bitcore_no_bie1_magic() {
let recipient_key = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000058",
)
.unwrap();
let recipient_pub = recipient_key.to_public_key();
let encrypted = ECIES::bitcore_encrypt(b"hello", &recipient_pub, None).unwrap();
assert_ne!(
&encrypted[0..4],
b"BIE1",
"Bitcore should NOT have BIE1 magic"
);
assert!(
encrypted[0] == 0x02 || encrypted[0] == 0x03,
"Bitcore ciphertext should start with compressed pubkey prefix"
);
}
#[test]
fn test_bitcore_encrypt_decrypt_with_random_ephemeral() {
let recipient_key = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000058",
)
.unwrap();
let recipient_pub = recipient_key.to_public_key();
let plaintext = sha256(b"random ephemeral test");
let encrypted = ECIES::bitcore_encrypt(&plaintext, &recipient_pub, None).unwrap();
let decrypted = ECIES::bitcore_decrypt(&encrypted, &recipient_key).unwrap();
assert_eq!(decrypted, plaintext.to_vec());
}
#[test]
fn test_bitcore_hmac_rejects_tampered() {
let sender_key = PrivateKey::from_hex(
"000000000000000000000000000000000000000000000000000000000000002a",
)
.unwrap();
let recipient_key = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000058",
)
.unwrap();
let recipient_pub = recipient_key.to_public_key();
let mut encrypted =
ECIES::bitcore_encrypt(b"secret data", &recipient_pub, Some(&sender_key)).unwrap();
let mid = 33 + 8; encrypted[mid] ^= 0xff;
let result = ECIES::bitcore_decrypt(&encrypted, &recipient_key);
assert!(result.is_err(), "should reject tampered bitcore ciphertext");
}
#[test]
fn test_electrum_ephemeral_encrypt_decrypt() {
let recipient_key = PrivateKey::from_hex(
"2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
)
.unwrap();
let recipient_pub = recipient_key.to_public_key();
let plaintext = b"ephemeral key test message";
let encrypted = ECIES::electrum_encrypt(plaintext, &recipient_pub, None).unwrap();
let decrypted = ECIES::electrum_decrypt(&encrypted, &recipient_key).unwrap();
assert_eq!(decrypted, plaintext);
}
}