1use crate::compat::CompatError;
8use crate::primitives::aes_cbc::{aes_cbc_decrypt, aes_cbc_encrypt};
9use crate::primitives::big_number::Endian;
10use crate::primitives::hash::{sha256_hmac, sha512};
11use crate::primitives::private_key::PrivateKey;
12use crate::primitives::public_key::PublicKey;
13use crate::primitives::random::random_bytes;
14
15pub struct ECIES;
23
24fn derive_electrum_keys(
31 priv_key: &PrivateKey,
32 pub_key: &PublicKey,
33) -> ([u8; 16], [u8; 16], [u8; 32]) {
34 let p = pub_key.point().mul(priv_key.bn());
35 let s_buf = p.to_der(true); let hash = sha512(&s_buf);
37
38 let mut iv = [0u8; 16];
39 let mut k_e = [0u8; 16];
40 let mut k_m = [0u8; 32];
41 iv.copy_from_slice(&hash[0..16]);
42 k_e.copy_from_slice(&hash[16..32]);
43 k_m.copy_from_slice(&hash[32..64]);
44
45 (iv, k_e, k_m)
46}
47
48fn derive_bitcore_keys(priv_key: &PrivateKey, pub_key: &PublicKey) -> ([u8; 32], [u8; 32]) {
55 let p = pub_key.point().mul(priv_key.bn());
56 let s_buf = p.get_x().to_array(Endian::Big, Some(32));
57 let hash = sha512(&s_buf);
58
59 let mut k_e = [0u8; 32];
60 let mut k_m = [0u8; 32];
61 k_e.copy_from_slice(&hash[0..32]);
62 k_m.copy_from_slice(&hash[32..64]);
63
64 (k_e, k_m)
65}
66
67impl ECIES {
68 pub fn electrum_encrypt(
74 plaintext: &[u8],
75 recipient_pub_key: &PublicKey,
76 sender_priv_key: Option<&PrivateKey>,
77 ) -> Result<Vec<u8>, CompatError> {
78 let ephemeral_key = match sender_priv_key {
79 Some(key) => key.clone(),
80 None => PrivateKey::from_random()?,
81 };
82
83 let (iv, k_e, k_m) = derive_electrum_keys(&ephemeral_key, recipient_pub_key);
84
85 let ct = aes_cbc_encrypt(&k_e, &iv, plaintext)?;
87
88 let ephemeral_pub = ephemeral_key.to_public_key();
90 let r_buf = ephemeral_pub.to_der(); let mut payload = Vec::with_capacity(4 + 33 + ct.len() + 32);
93 payload.extend_from_slice(b"BIE1");
94 payload.extend_from_slice(&r_buf);
95 payload.extend_from_slice(&ct);
96
97 let mac = sha256_hmac(&k_m, &payload);
99
100 payload.extend_from_slice(&mac);
101 Ok(payload)
102 }
103
104 pub fn electrum_decrypt(
109 ciphertext: &[u8],
110 recipient_priv_key: &PrivateKey,
111 ) -> Result<Vec<u8>, CompatError> {
112 if ciphertext.len() < 85 {
114 return Err(CompatError::InvalidCiphertext(format!(
115 "electrum ciphertext too short: {} bytes (min 85)",
116 ciphertext.len()
117 )));
118 }
119
120 if &ciphertext[0..4] != b"BIE1" {
122 return Err(CompatError::InvalidMagic);
123 }
124
125 let ephemeral_pub_bytes = &ciphertext[4..37];
127 let hmac_start = ciphertext.len() - 32;
128 let encrypted_data = &ciphertext[37..hmac_start];
129 let mac = &ciphertext[hmac_start..];
130
131 let ephemeral_pub = PublicKey::from_der_bytes(ephemeral_pub_bytes)?;
133
134 let (iv, k_e, k_m) = derive_electrum_keys(recipient_priv_key, &ephemeral_pub);
136
137 let expected_mac = sha256_hmac(&k_m, &ciphertext[0..hmac_start]);
139 if mac != expected_mac {
140 return Err(CompatError::HmacMismatch);
141 }
142
143 let plaintext = aes_cbc_decrypt(&k_e, &iv, encrypted_data)?;
145 Ok(plaintext)
146 }
147
148 pub fn bitcore_encrypt(
155 plaintext: &[u8],
156 recipient_pub_key: &PublicKey,
157 sender_priv_key: Option<&PrivateKey>,
158 ) -> Result<Vec<u8>, CompatError> {
159 let ephemeral_key = match sender_priv_key {
160 Some(key) => key.clone(),
161 None => PrivateKey::from_random()?,
162 };
163
164 let (k_e, k_m) = derive_bitcore_keys(&ephemeral_key, recipient_pub_key);
165
166 let iv_vec = random_bytes(16);
168 let mut iv = [0u8; 16];
169 iv.copy_from_slice(&iv_vec);
170
171 let ct = aes_cbc_encrypt(&k_e, &iv, plaintext)?;
173
174 let mut c = Vec::with_capacity(16 + ct.len());
176 c.extend_from_slice(&iv);
177 c.extend_from_slice(&ct);
178
179 let mac = sha256_hmac(&k_m, &c);
181
182 let r_buf = ephemeral_key.to_public_key().to_der();
184 let mut result = Vec::with_capacity(33 + c.len() + 32);
185 result.extend_from_slice(&r_buf);
186 result.extend_from_slice(&c);
187 result.extend_from_slice(&mac);
188 Ok(result)
189 }
190
191 pub fn bitcore_decrypt(
196 ciphertext: &[u8],
197 recipient_priv_key: &PrivateKey,
198 ) -> Result<Vec<u8>, CompatError> {
199 if ciphertext.len() < 97 {
201 return Err(CompatError::InvalidCiphertext(format!(
202 "bitcore ciphertext too short: {} bytes (min 97)",
203 ciphertext.len()
204 )));
205 }
206
207 let ephemeral_pub_bytes = &ciphertext[0..33];
209 let ephemeral_pub = PublicKey::from_der_bytes(ephemeral_pub_bytes)?;
210
211 let c = &ciphertext[33..ciphertext.len() - 32];
213 let mac = &ciphertext[ciphertext.len() - 32..];
214
215 let (k_e, k_m) = derive_bitcore_keys(recipient_priv_key, &ephemeral_pub);
217
218 let expected_mac = sha256_hmac(&k_m, c);
220 if mac != expected_mac {
221 return Err(CompatError::HmacMismatch);
222 }
223
224 let iv: [u8; 16] = c[0..16]
227 .try_into()
228 .map_err(|_| CompatError::InvalidCiphertext("IV extraction failed".into()))?;
229 let encrypted_data = &c[16..];
230
231 let plaintext = aes_cbc_decrypt(&k_e, &iv, encrypted_data)?;
233 Ok(plaintext)
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::primitives::hash::sha256;
241
242 fn base64_decode(input: &str) -> Vec<u8> {
244 let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
245 let mut result = Vec::new();
246 let mut buf: u32 = 0;
247 let mut bits: u32 = 0;
248
249 for &byte in input.as_bytes() {
250 if byte == b'=' {
251 break;
252 }
253 let val = table.iter().position(|&b| b == byte);
254 if let Some(v) = val {
255 buf = (buf << 6) | (v as u32);
256 bits += 6;
257 if bits >= 8 {
258 bits -= 8;
259 result.push((buf >> bits) as u8);
260 buf &= (1 << bits) - 1;
261 }
262 }
263 }
264 result
265 }
266
267 fn base64_encode(data: &[u8]) -> String {
269 let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
270 let mut result = String::new();
271 let mut i = 0;
272 while i < data.len() {
273 let b0 = data[i] as u32;
274 let b1 = if i + 1 < data.len() {
275 data[i + 1] as u32
276 } else {
277 0
278 };
279 let b2 = if i + 2 < data.len() {
280 data[i + 2] as u32
281 } else {
282 0
283 };
284 let triple = (b0 << 16) | (b1 << 8) | b2;
285 result.push(table[((triple >> 18) & 0x3f) as usize] as char);
286 result.push(table[((triple >> 12) & 0x3f) as usize] as char);
287 if i + 1 < data.len() {
288 result.push(table[((triple >> 6) & 0x3f) as usize] as char);
289 } else {
290 result.push('=');
291 }
292 if i + 2 < data.len() {
293 result.push(table[(triple & 0x3f) as usize] as char);
294 } else {
295 result.push('=');
296 }
297 i += 3;
298 }
299 result
300 }
301
302 #[allow(dead_code)]
303 fn hex_to_bytes(hex: &str) -> Vec<u8> {
304 (0..hex.len())
305 .step_by(2)
306 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
307 .collect()
308 }
309
310 #[allow(dead_code)]
311 fn bytes_to_hex(bytes: &[u8]) -> String {
312 bytes.iter().map(|b| format!("{b:02x}")).collect()
313 }
314
315 #[test]
318 fn test_electrum_encrypt_decrypt_roundtrip() {
319 let sender_key = PrivateKey::from_hex(
320 "77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
321 )
322 .unwrap();
323 let recipient_key = PrivateKey::from_hex(
324 "2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
325 )
326 .unwrap();
327 let recipient_pub = recipient_key.to_public_key();
328
329 let plaintext = b"this is my test message";
330 let encrypted =
331 ECIES::electrum_encrypt(plaintext, &recipient_pub, Some(&sender_key)).unwrap();
332 let decrypted = ECIES::electrum_decrypt(&encrypted, &recipient_key).unwrap();
333
334 assert_eq!(decrypted, plaintext);
335 }
336
337 #[test]
338 fn test_electrum_ciphertext_starts_with_bie1() {
339 let recipient_key = PrivateKey::from_hex(
340 "2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
341 )
342 .unwrap();
343 let recipient_pub = recipient_key.to_public_key();
344
345 let encrypted = ECIES::electrum_encrypt(b"hello", &recipient_pub, None).unwrap();
346 assert_eq!(
347 &encrypted[0..4],
348 b"BIE1",
349 "Electrum ciphertext must start with BIE1"
350 );
351 }
352
353 #[test]
354 fn test_electrum_hmac_rejects_tampered_ciphertext() {
355 let recipient_key = PrivateKey::from_hex(
356 "2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
357 )
358 .unwrap();
359 let sender_key = PrivateKey::from_hex(
360 "77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
361 )
362 .unwrap();
363 let recipient_pub = recipient_key.to_public_key();
364
365 let mut encrypted =
366 ECIES::electrum_encrypt(b"hello", &recipient_pub, Some(&sender_key)).unwrap();
367
368 let mid = encrypted.len() / 2;
370 encrypted[mid] ^= 0xff;
371
372 let result = ECIES::electrum_decrypt(&encrypted, &recipient_key);
373 assert!(result.is_err(), "should reject tampered ciphertext");
374 }
375
376 #[test]
377 fn test_electrum_decrypt_wrong_key_fails() {
378 let sender_key = PrivateKey::from_hex(
379 "77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
380 )
381 .unwrap();
382 let recipient_key = PrivateKey::from_hex(
383 "2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
384 )
385 .unwrap();
386 let wrong_key = PrivateKey::from_hex(
387 "0000000000000000000000000000000000000000000000000000000000000001",
388 )
389 .unwrap();
390
391 let recipient_pub = recipient_key.to_public_key();
392 let encrypted =
393 ECIES::electrum_encrypt(b"secret", &recipient_pub, Some(&sender_key)).unwrap();
394
395 let result = ECIES::electrum_decrypt(&encrypted, &wrong_key);
396 assert!(result.is_err(), "should fail with wrong private key");
397 }
398
399 #[test]
402 fn test_electrum_cross_sdk_decrypt_alice_to_bob() {
403 let bob_key = PrivateKey::from_hex(
405 "2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
406 )
407 .unwrap();
408
409 let ciphertext = base64_decode(
410 "QklFMQM55QTWSSsILaluEejwOXlrBs1IVcEB4kkqbxDz4Fap53XHOt6L3tKmrXho6yj6phfoiMkBOhUldRPnEI4fSZXbvZJHgyAzxA6SoujduvJXv+A9ri3po9veilrmc8p6dwo="
411 );
412
413 let plaintext = ECIES::electrum_decrypt(&ciphertext, &bob_key).unwrap();
414 assert_eq!(
415 std::str::from_utf8(&plaintext).unwrap(),
416 "this is my test message"
417 );
418 }
419
420 #[test]
421 fn test_electrum_cross_sdk_decrypt_bob_to_alice() {
422 let alice_key = PrivateKey::from_hex(
424 "77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
425 )
426 .unwrap();
427
428 let ciphertext = base64_decode(
429 "QklFMQOGFyMXLo9Qv047K3BYJhmnJgt58EC8skYP/R2QU/U0yXXHOt6L3tKmrXho6yj6phfoiMkBOhUldRPnEI4fSZXbiaH4FsxKIOOvzolIFVAS0FplUmib2HnlAM1yP/iiPsU="
430 );
431
432 let plaintext = ECIES::electrum_decrypt(&ciphertext, &alice_key).unwrap();
433 assert_eq!(
434 std::str::from_utf8(&plaintext).unwrap(),
435 "this is my test message"
436 );
437 }
438
439 #[test]
440 fn test_electrum_cross_sdk_encrypt_matches_ts() {
441 let alice_key = PrivateKey::from_hex(
443 "77e06abc52bf065cb5164c5deca839d0276911991a2730be4d8d0a0307de7ceb",
444 )
445 .unwrap();
446 let bob_key = PrivateKey::from_hex(
447 "2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
448 )
449 .unwrap();
450 let bob_pub = bob_key.to_public_key();
451
452 let message = b"this is my test message";
453 let encrypted = ECIES::electrum_encrypt(message, &bob_pub, Some(&alice_key)).unwrap();
454 let expected_b64 = "QklFMQM55QTWSSsILaluEejwOXlrBs1IVcEB4kkqbxDz4Fap53XHOt6L3tKmrXho6yj6phfoiMkBOhUldRPnEI4fSZXbvZJHgyAzxA6SoujduvJXv+A9ri3po9veilrmc8p6dwo=";
455 assert_eq!(base64_encode(&encrypted), expected_b64);
456 }
457
458 #[test]
461 fn test_bitcore_encrypt_decrypt_roundtrip() {
462 let sender_key = PrivateKey::from_hex(
463 "000000000000000000000000000000000000000000000000000000000000002a",
464 )
465 .unwrap();
466 let recipient_key = PrivateKey::from_hex(
467 "0000000000000000000000000000000000000000000000000000000000000058",
468 )
469 .unwrap();
470 let recipient_pub = recipient_key.to_public_key();
471
472 let plaintext = sha256(b"my message is the hash of this string");
474
475 let encrypted =
476 ECIES::bitcore_encrypt(&plaintext, &recipient_pub, Some(&sender_key)).unwrap();
477 let decrypted = ECIES::bitcore_decrypt(&encrypted, &recipient_key).unwrap();
478
479 assert_eq!(decrypted, plaintext.to_vec());
480 }
481
482 #[test]
483 fn test_bitcore_no_bie1_magic() {
484 let recipient_key = PrivateKey::from_hex(
485 "0000000000000000000000000000000000000000000000000000000000000058",
486 )
487 .unwrap();
488 let recipient_pub = recipient_key.to_public_key();
489
490 let encrypted = ECIES::bitcore_encrypt(b"hello", &recipient_pub, None).unwrap();
491 assert_ne!(
492 &encrypted[0..4],
493 b"BIE1",
494 "Bitcore should NOT have BIE1 magic"
495 );
496 assert!(
498 encrypted[0] == 0x02 || encrypted[0] == 0x03,
499 "Bitcore ciphertext should start with compressed pubkey prefix"
500 );
501 }
502
503 #[test]
504 fn test_bitcore_encrypt_decrypt_with_random_ephemeral() {
505 let recipient_key = PrivateKey::from_hex(
506 "0000000000000000000000000000000000000000000000000000000000000058",
507 )
508 .unwrap();
509 let recipient_pub = recipient_key.to_public_key();
510
511 let plaintext = sha256(b"random ephemeral test");
512 let encrypted = ECIES::bitcore_encrypt(&plaintext, &recipient_pub, None).unwrap();
513 let decrypted = ECIES::bitcore_decrypt(&encrypted, &recipient_key).unwrap();
514
515 assert_eq!(decrypted, plaintext.to_vec());
516 }
517
518 #[test]
519 fn test_bitcore_hmac_rejects_tampered() {
520 let sender_key = PrivateKey::from_hex(
521 "000000000000000000000000000000000000000000000000000000000000002a",
522 )
523 .unwrap();
524 let recipient_key = PrivateKey::from_hex(
525 "0000000000000000000000000000000000000000000000000000000000000058",
526 )
527 .unwrap();
528 let recipient_pub = recipient_key.to_public_key();
529
530 let mut encrypted =
531 ECIES::bitcore_encrypt(b"secret data", &recipient_pub, Some(&sender_key)).unwrap();
532
533 let mid = 33 + 8; encrypted[mid] ^= 0xff;
536
537 let result = ECIES::bitcore_decrypt(&encrypted, &recipient_key);
538 assert!(result.is_err(), "should reject tampered bitcore ciphertext");
539 }
540
541 #[test]
542 fn test_electrum_ephemeral_encrypt_decrypt() {
543 let recipient_key = PrivateKey::from_hex(
544 "2b57c7c5e408ce927eef5e2efb49cfdadde77961d342daa72284bb3d6590862d",
545 )
546 .unwrap();
547 let recipient_pub = recipient_key.to_public_key();
548
549 let plaintext = b"ephemeral key test message";
550 let encrypted = ECIES::electrum_encrypt(plaintext, &recipient_pub, None).unwrap();
551 let decrypted = ECIES::electrum_decrypt(&encrypted, &recipient_key).unwrap();
552 assert_eq!(decrypted, plaintext);
553 }
554}