1use ark_ff::{BigInteger, PrimeField};
5use curve25519_dalek::constants::X25519_BASEPOINT;
6use curve25519_dalek::montgomery::MontgomeryPoint;
7use curve25519_dalek::scalar::Scalar;
8use ethers::types::U256;
9use num_bigint::BigUint;
10use rand::RngCore;
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use zeroize::Zeroize;
14
15use darkpool_crypto::Kdf;
16use darkpool_crypto::{
17 CryptoError as BjjCryptoError, PublicKey as BjjPublicKey, SecretKey as BjjSecretKey,
18 SharedSecret, BASE8, SUBGROUP_ORDER,
19};
20
21pub type X25519SecretKey = [u8; 32];
22pub type X25519PublicKey = [u8; 32];
23
24#[allow(clippy::expect_used)]
25static SUBGROUP_ORDER_BIGINT: std::sync::LazyLock<BigUint> = std::sync::LazyLock::new(|| {
26 BigUint::parse_bytes(SUBGROUP_ORDER.as_bytes(), 10).expect("valid constant")
27});
28
29#[derive(Debug, Clone)]
32pub struct BjjKeypair {
33 sk: BjjSecretKey,
34 pk: BjjPublicKey,
35}
36
37impl Serialize for BjjKeypair {
38 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
39 where
40 S: serde::Serializer,
41 {
42 use serde::ser::SerializeStruct;
43 let mut state = serializer.serialize_struct("BjjKeypair", 3)?;
44 state.serialize_field("sk", &self.sk.to_hex())?;
45 state.serialize_field("pk", &self.pk.to_hex())?;
46 state.end()
47 }
48}
49
50impl<'de> Deserialize<'de> for BjjKeypair {
51 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52 where
53 D: serde::Deserializer<'de>,
54 {
55 #[derive(Deserialize)]
56 struct BjjKeypairData {
57 sk: String,
58 pk: String,
59 }
60
61 let data = BjjKeypairData::deserialize(deserializer)?;
62 let sk = BjjSecretKey::from_hex(&data.sk).map_err(serde::de::Error::custom)?;
63 let pk = BjjPublicKey::from_hex(&data.pk).map_err(serde::de::Error::custom)?;
64
65 Ok(Self { sk, pk })
66 }
67}
68
69impl BjjKeypair {
70 #[must_use]
71 pub fn generate() -> Self {
72 let mut rng = rand::rngs::OsRng;
73 let sk = BjjSecretKey::generate(&mut rng);
74 let pk = sk.public_key().unwrap_or_else(|_| {
75 unreachable!("public key derivation cannot fail for random scalar")
76 });
77 Self { sk, pk }
78 }
79
80 pub fn from_seed(seed: &[u8]) -> Result<Self, BjjCryptoError> {
82 let mut hasher = Sha256::new();
83 hasher.update(b"hisoka.bjj.keypair");
84 hasher.update(seed);
85 let hash_bytes = hasher.finalize();
86
87 let hash_bigint = BigUint::from_bytes_be(&hash_bytes);
88 let reduced = hash_bigint % &*SUBGROUP_ORDER_BIGINT;
89
90 let mut sk_bytes = reduced.to_bytes_be();
91 while sk_bytes.len() < 32 {
92 sk_bytes.insert(0, 0);
93 }
94
95 let sk = BjjSecretKey::from_hex(&hex::encode(&sk_bytes))?;
96 let pk = sk.public_key()?;
97 Ok(Self { sk, pk })
98 }
99
100 #[allow(clippy::must_use_candidate)]
101 pub fn public_key(&self) -> &BjjPublicKey {
102 &self.pk
103 }
104
105 #[allow(clippy::must_use_candidate)]
106 pub fn pk_x(&self) -> U256 {
107 let bytes = self.pk.x().into_bigint().to_bytes_be();
108 U256::from_big_endian(&bytes)
109 }
110
111 #[allow(clippy::must_use_candidate)]
112 pub fn pk_y(&self) -> U256 {
113 let bytes = self.pk.y().into_bigint().to_bytes_be();
114 U256::from_big_endian(&bytes)
115 }
116
117 #[allow(clippy::must_use_candidate)]
118 pub fn pk_tuple(&self) -> (U256, U256) {
119 (self.pk_x(), self.pk_y())
120 }
121
122 #[allow(clippy::must_use_candidate)]
123 pub fn sk_as_u256(&self) -> U256 {
124 let bytes = self.sk.0.into_bigint().to_bytes_be();
125 U256::from_big_endian(&bytes)
126 }
127
128 pub fn derive_shared_secret(
129 &self,
130 peer_pk: &BjjPublicKey,
131 ) -> Result<SharedSecret, BjjCryptoError> {
132 self.sk.derive_shared_secret(peer_pk)
133 }
134
135 pub fn derive_shared_secret_x(&self, peer_pk: &BjjPublicKey) -> Result<U256, BjjCryptoError> {
136 let ss = self.derive_shared_secret(peer_pk)?;
137 let bytes = ss.x().into_bigint().to_bytes_be();
138 Ok(U256::from_big_endian(&bytes))
139 }
140
141 #[allow(clippy::must_use_candidate)]
142 pub fn sk_hex(&self) -> String {
143 self.sk.to_hex()
144 }
145
146 #[allow(clippy::must_use_candidate)]
147 pub fn pk_hex(&self) -> String {
148 self.pk.to_hex()
149 }
150}
151
152#[derive(Debug, Clone)]
154pub struct X25519Keypair {
155 pub sk: X25519SecretKey,
156 pub pk: X25519PublicKey,
157}
158
159impl X25519Keypair {
160 #[must_use]
161 pub fn generate() -> Self {
162 let mut rng = rand::rngs::OsRng;
163 let mut sk = [0u8; 32];
164 rng.fill_bytes(&mut sk);
165
166 sk[0] &= 0xF8;
168 sk[31] &= 0x7F;
169 sk[31] |= 0x40;
170
171 let scalar = Scalar::from_bytes_mod_order(sk);
172 let pk_point = X25519_BASEPOINT * scalar;
173
174 Self {
175 sk,
176 pk: pk_point.to_bytes(),
177 }
178 }
179
180 #[must_use]
181 pub fn from_seed(seed: &[u8]) -> Self {
182 let mut hasher = Sha256::new();
183 hasher.update(b"hisoka.x25519.keypair");
184 hasher.update(seed);
185 let mut sk: [u8; 32] = hasher.finalize().into();
186
187 sk[0] &= 0xF8;
188 sk[31] &= 0x7F;
189 sk[31] |= 0x40;
190
191 let scalar = Scalar::from_bytes_mod_order(sk);
192 let pk_point = X25519_BASEPOINT * scalar;
193
194 Self {
195 sk,
196 pk: pk_point.to_bytes(),
197 }
198 }
199
200 #[must_use]
201 pub fn ecdh(&self, other_pk: &X25519PublicKey) -> [u8; 32] {
202 let other_point = MontgomeryPoint(*other_pk);
203 let scalar = Scalar::from_bytes_mod_order(self.sk);
204 (other_point * scalar).to_bytes()
205 }
206}
207
208impl Drop for X25519Keypair {
209 fn drop(&mut self) {
210 self.sk.zeroize();
211 }
212}
213
214#[derive(Debug, Clone)]
215pub struct ClientIdentity {
216 pub bjj: BjjKeypair,
217 pub x25519: X25519Keypair,
218 pub name: String,
219}
220
221impl ClientIdentity {
222 #[must_use]
223 pub fn new(name: &str) -> Self {
224 Self {
225 bjj: BjjKeypair::generate(),
226 x25519: X25519Keypair::generate(),
227 name: name.to_string(),
228 }
229 }
230
231 pub fn from_seed(name: &str, seed: &[u8]) -> Result<Self, BjjCryptoError> {
232 Ok(Self {
233 bjj: BjjKeypair::from_seed(seed)?,
234 x25519: X25519Keypair::from_seed(seed),
235 name: name.to_string(),
236 })
237 }
238
239 pub fn from_signature(name: &str, signature: &[u8]) -> Result<Self, BjjCryptoError> {
240 Self::from_seed(name, signature)
241 }
242}
243
244const BN254_FR_MODULUS: &str =
245 "21888242871839275222246405745257275088548364400416034343698204186575808495617";
246
247#[allow(clippy::expect_used)]
248static BN254_MODULUS_BIGINT: std::sync::LazyLock<BigUint> = std::sync::LazyLock::new(|| {
249 BigUint::parse_bytes(BN254_FR_MODULUS.as_bytes(), 10).expect("valid constant")
250});
251
252#[derive(Debug, Clone)]
263pub struct DarkAccount {
264 sk_root: U256,
265 sk_spend: Option<U256>,
266 sk_view: Option<U256>,
267 vk_master: Option<U256>,
268}
269
270impl Drop for DarkAccount {
272 fn drop(&mut self) {
273 zeroize_u256(&mut self.sk_root);
274 if let Some(ref mut v) = self.sk_spend {
275 zeroize_u256(v);
276 }
277 if let Some(ref mut v) = self.sk_view {
278 zeroize_u256(v);
279 }
280 if let Some(ref mut v) = self.vk_master {
281 zeroize_u256(v);
282 }
283 }
284}
285
286fn zeroize_u256(val: &mut U256) {
287 let ptr = std::ptr::from_mut::<U256>(val);
288 unsafe { std::ptr::write_volatile(ptr, U256::zero()) };
290}
291
292impl DarkAccount {
293 #[must_use]
294 pub fn new(sk_root: U256) -> Self {
295 Self {
296 sk_root,
297 sk_spend: None,
298 sk_view: None,
299 vk_master: None,
300 }
301 }
302
303 #[must_use]
305 pub fn from_signature(signature: &[u8]) -> Self {
306 let sig_bigint = BigUint::from_bytes_be(signature);
307 let reduced = sig_bigint % &*BN254_MODULUS_BIGINT;
308
309 let mut sig_bytes = reduced.to_bytes_be();
310 while sig_bytes.len() < 32 {
311 sig_bytes.insert(0, 0);
312 }
313 let sig_fr = U256::from_big_endian(&sig_bytes[..32.min(sig_bytes.len())]);
314
315 #[allow(clippy::expect_used)]
316 let sk_root = Kdf::derive("hisoka.root", sig_fr, None).expect("valid purpose string");
317 Self::new(sk_root)
318 }
319
320 #[must_use]
322 pub fn from_seed(seed: &[u8]) -> Self {
323 let mut hasher = Sha256::new();
324 hasher.update(b"hisoka.seed");
325 hasher.update(seed);
326 let hash = hasher.finalize();
327
328 let hash_bigint = BigUint::from_bytes_be(&hash);
329 let reduced = hash_bigint % &*BN254_MODULUS_BIGINT;
330
331 let mut bytes = reduced.to_bytes_be();
332 while bytes.len() < 32 {
333 bytes.insert(0, 0);
334 }
335 let seed_fr = U256::from_big_endian(&bytes);
336
337 #[allow(clippy::expect_used)]
338 let sk_root = Kdf::derive("hisoka.root", seed_fr, None).expect("valid purpose string");
339 Self::new(sk_root)
340 }
341
342 #[allow(clippy::must_use_candidate)]
343 pub fn sk_root(&self) -> U256 {
344 self.sk_root
345 }
346
347 #[allow(clippy::expect_used)]
348 pub fn get_spend_key(&mut self) -> U256 {
349 *self.sk_spend.get_or_insert_with(|| {
350 Kdf::derive("hisoka.spend", self.sk_root, None).expect("valid purpose string")
351 })
352 }
353
354 #[allow(clippy::expect_used)]
355 pub fn get_view_key(&mut self) -> U256 {
356 *self.sk_view.get_or_insert_with(|| {
357 Kdf::derive("hisoka.view", self.sk_root, None).expect("valid purpose string")
358 })
359 }
360
361 #[allow(clippy::expect_used)]
362 fn get_vk_master(&mut self) -> U256 {
363 if self.vk_master.is_none() {
364 let sk_view = self.get_view_key();
365 self.vk_master =
366 Some(Kdf::derive("hisoka.ivkMaster", sk_view, None).expect("valid purpose string"));
367 }
368 *self.vk_master.as_ref().unwrap_or_else(|| unreachable!())
369 }
370
371 #[allow(clippy::expect_used)]
374 pub fn get_ephemeral_outgoing_key(&mut self, index: u64) -> U256 {
375 let vk_master = self.get_vk_master();
376 let tweak =
377 Kdf::derive_indexed("hisoka.eskTweak", vk_master, index).expect("valid purpose string");
378 Self::add_mod_subgroup_order(vk_master, tweak)
379 }
380
381 #[allow(clippy::expect_used)]
382 pub fn get_incoming_viewing_key(&mut self, index: u64) -> U256 {
383 let vk_master = self.get_vk_master();
384 let tweak =
385 Kdf::derive_indexed("hisoka.ivkTweak", vk_master, index).expect("valid purpose string");
386 Self::add_mod_subgroup_order(vk_master, tweak)
387 }
388
389 pub fn get_public_ephemeral_key(&mut self, index: u64) -> Result<(U256, U256), BjjCryptoError> {
390 let esk = self.get_ephemeral_outgoing_key(index);
391 Self::scalar_mul_base8(esk)
392 }
393
394 pub fn get_public_incoming_key(&mut self, index: u64) -> Result<(U256, U256), BjjCryptoError> {
395 let ivk = self.get_incoming_viewing_key(index);
396 Self::scalar_mul_base8(ivk)
397 }
398
399 fn add_mod_subgroup_order(a: U256, b: U256) -> U256 {
402 let a_bigint = BigUint::from_bytes_be(&{
403 let mut bytes = [0u8; 32];
404 a.to_big_endian(&mut bytes);
405 bytes
406 });
407 let b_bigint = BigUint::from_bytes_be(&{
408 let mut bytes = [0u8; 32];
409 b.to_big_endian(&mut bytes);
410 bytes
411 });
412
413 let sum = (a_bigint + b_bigint) % &*SUBGROUP_ORDER_BIGINT;
414 let mut sum_bytes = sum.to_bytes_be();
415 while sum_bytes.len() < 32 {
416 sum_bytes.insert(0, 0);
417 }
418 U256::from_big_endian(&sum_bytes)
419 }
420
421 fn scalar_mul_base8(scalar: U256) -> Result<(U256, U256), BjjCryptoError> {
422 use ark_ff::BigInteger;
423
424 let mut scalar_bytes = [0u8; 32];
425 scalar.to_big_endian(&mut scalar_bytes);
426 scalar_bytes.reverse(); let result = BASE8.mul_scalar(&scalar_bytes)?;
429 let x_bytes = result.x().into_bigint().to_bytes_be();
430 let y_bytes = result.y().into_bigint().to_bytes_be();
431
432 Ok((
433 U256::from_big_endian(&x_bytes),
434 U256::from_big_endian(&y_bytes),
435 ))
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn test_bjj_keypair_generation() {
445 let kp1 = BjjKeypair::generate();
446 let kp2 = BjjKeypair::generate();
447 assert_ne!(kp1.sk_hex(), kp2.sk_hex());
448 assert_ne!(kp1.pk_hex(), kp2.pk_hex());
449 }
450
451 #[test]
452 fn test_bjj_from_seed_deterministic() {
453 let seed = b"alice_secret_seed";
454 let kp1 = BjjKeypair::from_seed(seed).unwrap();
455 let kp2 = BjjKeypair::from_seed(seed).unwrap();
456 assert_eq!(kp1.sk_hex(), kp2.sk_hex());
457 assert_eq!(kp1.pk_hex(), kp2.pk_hex());
458 }
459
460 #[test]
461 fn test_bjj_subgroup_reduction() {
462 let seed = [0xffu8; 64];
463 let kp = BjjKeypair::from_seed(&seed).unwrap();
464 let sk_u256 = kp.sk_as_u256();
465 let subgroup_order = U256::from_dec_str(SUBGROUP_ORDER).unwrap();
466 assert!(sk_u256 < subgroup_order);
467 }
468
469 #[test]
470 fn test_bjj_ecdh() {
471 let alice = BjjKeypair::generate();
472 let bob = BjjKeypair::generate();
473 let ss_alice = alice.derive_shared_secret_x(bob.public_key()).unwrap();
474 let ss_bob = bob.derive_shared_secret_x(alice.public_key()).unwrap();
475 assert_eq!(ss_alice, ss_bob);
476 }
477
478 #[test]
479 fn test_x25519_ecdh() {
480 let alice = X25519Keypair::generate();
481 let bob = X25519Keypair::generate();
482 assert_eq!(alice.ecdh(&bob.pk), bob.ecdh(&alice.pk));
483 }
484
485 #[test]
486 fn test_bjj_serialization_roundtrip() {
487 let kp = BjjKeypair::generate();
488 let json = serde_json::to_string(&kp).unwrap();
489 let kp2: BjjKeypair = serde_json::from_str(&json).unwrap();
490 assert_eq!(kp.sk_hex(), kp2.sk_hex());
491 assert_eq!(kp.pk_hex(), kp2.pk_hex());
492 }
493
494 #[test]
495 fn test_dark_account_from_seed_deterministic() {
496 let seed = b"alice_secret_seed_for_dark_account";
497 let mut account1 = DarkAccount::from_seed(seed);
498 let mut account2 = DarkAccount::from_seed(seed);
499 assert_eq!(account1.sk_root(), account2.sk_root());
500 assert_eq!(account1.get_spend_key(), account2.get_spend_key());
501 assert_eq!(account1.get_view_key(), account2.get_view_key());
502 }
503
504 #[test]
505 fn test_dark_account_key_hierarchy() {
506 let mut account = DarkAccount::from_seed(b"test_hierarchy");
507 let sk_root = account.sk_root();
508 let sk_spend = account.get_spend_key();
509 let sk_view = account.get_view_key();
510
511 assert!(!sk_root.is_zero());
512 assert!(!sk_spend.is_zero());
513 assert!(!sk_view.is_zero());
514 assert_ne!(sk_root, sk_spend);
515 assert_ne!(sk_root, sk_view);
516 assert_ne!(sk_spend, sk_view);
517 }
518
519 #[test]
520 fn test_dark_account_per_index_keys() {
521 let mut account = DarkAccount::from_seed(b"test_per_index");
522
523 let esk_0 = account.get_ephemeral_outgoing_key(0);
524 let esk_1 = account.get_ephemeral_outgoing_key(1);
525 let esk_2 = account.get_ephemeral_outgoing_key(2);
526 assert_ne!(esk_0, esk_1);
527 assert_ne!(esk_1, esk_2);
528
529 let ivk_0 = account.get_incoming_viewing_key(0);
530 let ivk_1 = account.get_incoming_viewing_key(1);
531 assert_ne!(ivk_0, ivk_1);
532 assert_ne!(esk_0, ivk_0);
533 }
534
535 #[test]
536 fn test_dark_account_public_keys() {
537 let mut account = DarkAccount::from_seed(b"test_public_keys");
538
539 let (epk_x, epk_y) = account.get_public_ephemeral_key(0).unwrap();
540 let (ivk_x, ivk_y) = account.get_public_incoming_key(0).unwrap();
541 assert!(!epk_x.is_zero() || !epk_y.is_zero());
542 assert!(!ivk_x.is_zero() || !ivk_y.is_zero());
543
544 let (epk1_x, epk1_y) = account.get_public_ephemeral_key(1).unwrap();
545 assert!(epk_x != epk1_x || epk_y != epk1_y);
546 }
547
548 #[test]
549 fn test_dark_account_from_signature() {
550 let signature = hex::decode(
551 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\
552 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01",
553 )
554 .unwrap();
555
556 let mut account = DarkAccount::from_signature(&signature);
557 assert!(!account.sk_root().is_zero());
558 assert!(!account.get_spend_key().is_zero());
559 }
560
561 #[test]
562 fn test_dark_account_caching() {
563 let mut account = DarkAccount::from_seed(b"test_caching");
564 let sk_spend_1 = account.get_spend_key();
565 let sk_spend_2 = account.get_spend_key();
566 assert_eq!(sk_spend_1, sk_spend_2);
567
568 let sk_view_1 = account.get_view_key();
569 let sk_view_2 = account.get_view_key();
570 assert_eq!(sk_view_1, sk_view_2);
571 }
572}