Skip to main content

bsv/wallet/
proto_wallet.rs

1//! ProtoWallet: crypto-only wallet wrapping KeyDeriver.
2//!
3//! ProtoWallet provides sign, verify, encrypt, decrypt, HMAC, and key linkage
4//! revelation operations. It implements WalletInterface so it can be used
5//! anywhere a wallet is needed (e.g. auth, certificates, testing).
6//! Unsupported methods (transactions, outputs, certificates, blockchain queries)
7//! return `WalletError::NotImplemented`.
8
9use crate::primitives::ecdsa::{ecdsa_sign, ecdsa_verify};
10use crate::primitives::hash::{sha256, sha256_hmac};
11use crate::primitives::point::Point;
12use crate::primitives::private_key::PrivateKey;
13use crate::primitives::public_key::PublicKey;
14use crate::primitives::schnorr::schnorr_generate_proof;
15use crate::primitives::signature::Signature;
16use crate::wallet::error::WalletError;
17use crate::wallet::interfaces::{
18    AbortActionArgs, AbortActionResult, AcquireCertificateArgs, AuthenticatedResult, Certificate,
19    CreateActionArgs, CreateActionResult, CreateHmacArgs, CreateHmacResult, CreateSignatureArgs,
20    CreateSignatureResult, DecryptArgs, DecryptResult, DiscoverByAttributesArgs,
21    DiscoverByIdentityKeyArgs, DiscoverCertificatesResult, EncryptArgs, EncryptResult,
22    GetHeaderArgs, GetHeaderResult, GetHeightResult, GetNetworkResult, GetPublicKeyArgs,
23    GetPublicKeyResult, GetVersionResult, InternalizeActionArgs, InternalizeActionResult,
24    ListActionsArgs, ListActionsResult, ListCertificatesArgs, ListCertificatesResult,
25    ListOutputsArgs, ListOutputsResult, ProveCertificateArgs, ProveCertificateResult,
26    RelinquishCertificateArgs, RelinquishCertificateResult, RelinquishOutputArgs,
27    RelinquishOutputResult, RevealCounterpartyKeyLinkageArgs, RevealCounterpartyKeyLinkageResult,
28    RevealSpecificKeyLinkageArgs, RevealSpecificKeyLinkageResult, SignActionArgs, SignActionResult,
29    VerifyHmacArgs, VerifyHmacResult, VerifySignatureArgs, VerifySignatureResult, WalletInterface,
30};
31use crate::wallet::key_deriver::KeyDeriver;
32use crate::wallet::types::{Counterparty, CounterpartyType, Protocol};
33
34/// Result of revealing counterparty key linkage.
35pub struct RevealCounterpartyResult {
36    /// The prover's identity public key.
37    pub prover: PublicKey,
38    /// The counterparty's public key.
39    pub counterparty: PublicKey,
40    /// The verifier's public key (who can decrypt the revelation).
41    pub verifier: PublicKey,
42    /// ISO 8601 timestamp of the revelation.
43    pub revelation_time: String,
44    /// Encrypted shared secret (linkage), encrypted for the verifier.
45    pub encrypted_linkage: Vec<u8>,
46    /// Encrypted proof of the linkage, encrypted for the verifier.
47    pub encrypted_linkage_proof: Vec<u8>,
48}
49
50/// Result of revealing specific key linkage.
51pub struct RevealSpecificResult {
52    /// Encrypted specific secret, encrypted for the verifier.
53    pub encrypted_linkage: Vec<u8>,
54    /// Encrypted proof bytes, encrypted for the verifier.
55    pub encrypted_linkage_proof: Vec<u8>,
56    /// The prover's identity public key.
57    pub prover: PublicKey,
58    /// The verifier's public key.
59    pub verifier: PublicKey,
60    /// The counterparty's public key.
61    pub counterparty: PublicKey,
62    /// The protocol used for this specific derivation.
63    pub protocol: Protocol,
64    /// The key ID used for this specific derivation.
65    pub key_id: String,
66    /// Proof type (0 = no proof for specific linkage).
67    pub proof_type: u8,
68}
69
70/// ProtoWallet is a crypto-only wallet wrapping KeyDeriver.
71///
72/// It provides foundational cryptographic operations: key derivation,
73/// signing, verification, encryption, decryption, HMAC, and key linkage
74/// revelation. Unlike a full wallet, it does not create transactions,
75/// manage outputs, or interact with the blockchain.
76pub struct ProtoWallet {
77    key_deriver: KeyDeriver,
78}
79
80impl ProtoWallet {
81    /// Create a new ProtoWallet from a private key.
82    pub fn new(private_key: PrivateKey) -> Self {
83        ProtoWallet {
84            key_deriver: KeyDeriver::new(private_key),
85        }
86    }
87
88    /// Create a new ProtoWallet from an existing KeyDeriver.
89    pub fn from_key_deriver(kd: KeyDeriver) -> Self {
90        ProtoWallet { key_deriver: kd }
91    }
92
93    /// Create an "anyone" ProtoWallet using the special anyone key (PrivateKey(1)).
94    pub fn anyone() -> Self {
95        ProtoWallet {
96            key_deriver: KeyDeriver::new_anyone(),
97        }
98    }
99
100    /// Get a public key, either the identity key or a derived key.
101    ///
102    /// If `identity_key` is true, returns the root identity public key
103    /// (protocol, key_id, counterparty, for_self are ignored).
104    /// Otherwise, derives a public key using the given parameters.
105    /// If counterparty is Uninitialized, it defaults to Self_.
106    pub fn get_public_key_sync(
107        &self,
108        protocol: &Protocol,
109        key_id: &str,
110        counterparty: &Counterparty,
111        for_self: bool,
112        identity_key: bool,
113    ) -> Result<PublicKey, WalletError> {
114        if identity_key {
115            return Ok(self.key_deriver.identity_key());
116        }
117
118        if protocol.protocol.is_empty() || key_id.is_empty() {
119            return Err(WalletError::InvalidParameter(
120                "protocolID and keyID are required if identityKey is false".to_string(),
121            ));
122        }
123
124        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
125        self.key_deriver
126            .derive_public_key(protocol, key_id, &effective, for_self)
127    }
128
129    /// Create an ECDSA signature over data or a pre-computed hash.
130    ///
131    /// If `data` is provided, hashes it with SHA-256 then signs.
132    /// If `hash_to_directly_sign` is provided, signs it directly.
133    /// Returns the DER-encoded signature bytes.
134    pub fn create_signature_sync(
135        &self,
136        data: Option<&[u8]>,
137        hash_to_directly_sign: Option<&[u8]>,
138        protocol: &Protocol,
139        key_id: &str,
140        counterparty: &Counterparty,
141    ) -> Result<Vec<u8>, WalletError> {
142        let effective = self.default_counterparty(counterparty, CounterpartyType::Anyone);
143        let derived_key = self
144            .key_deriver
145            .derive_private_key(protocol, key_id, &effective)?;
146
147        let hash = if let Some(h) = hash_to_directly_sign {
148            if h.len() != 32 {
149                return Err(WalletError::InvalidParameter(
150                    "hash_to_directly_sign must be exactly 32 bytes".to_string(),
151                ));
152            }
153            let mut arr = [0u8; 32];
154            arr.copy_from_slice(h);
155            arr
156        } else if let Some(d) = data {
157            sha256(d)
158        } else {
159            return Err(WalletError::InvalidParameter(
160                "either data or hash_to_directly_sign must be provided".to_string(),
161            ));
162        };
163        let sig = ecdsa_sign(&hash, derived_key.bn(), true)?;
164        Ok(sig.to_der())
165    }
166
167    /// Verify an ECDSA signature over data or a pre-computed hash.
168    ///
169    /// If `data` is provided, hashes it with SHA-256 then verifies.
170    /// If `hash_to_directly_verify` is provided, verifies against it directly.
171    #[allow(clippy::too_many_arguments)]
172    pub fn verify_signature_sync(
173        &self,
174        data: Option<&[u8]>,
175        hash_to_directly_verify: Option<&[u8]>,
176        signature: &[u8],
177        protocol: &Protocol,
178        key_id: &str,
179        counterparty: &Counterparty,
180        for_self: bool,
181    ) -> Result<bool, WalletError> {
182        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
183        let derived_pub = self
184            .key_deriver
185            .derive_public_key(protocol, key_id, &effective, for_self)?;
186
187        let sig = Signature::from_der(signature)?;
188        let hash = if let Some(h) = hash_to_directly_verify {
189            if h.len() != 32 {
190                return Err(WalletError::InvalidParameter(
191                    "hash_to_directly_verify must be exactly 32 bytes".to_string(),
192                ));
193            }
194            let mut arr = [0u8; 32];
195            arr.copy_from_slice(h);
196            arr
197        } else if let Some(d) = data {
198            sha256(d)
199        } else {
200            return Err(WalletError::InvalidParameter(
201                "either data or hash_to_directly_verify must be provided".to_string(),
202            ));
203        };
204        Ok(ecdsa_verify(&hash, &sig, derived_pub.point()))
205    }
206
207    /// Encrypt plaintext using a derived symmetric key (AES-GCM).
208    ///
209    /// Derives a symmetric key from the protocol, key ID, and counterparty,
210    /// then encrypts the plaintext. Returns IV || ciphertext || auth tag.
211    pub fn encrypt_sync(
212        &self,
213        plaintext: &[u8],
214        protocol: &Protocol,
215        key_id: &str,
216        counterparty: &Counterparty,
217    ) -> Result<Vec<u8>, WalletError> {
218        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
219        let sym_key = self
220            .key_deriver
221            .derive_symmetric_key(protocol, key_id, &effective)?;
222        Ok(sym_key.encrypt(plaintext)?)
223    }
224
225    /// Test-only deterministic encryption with a caller-supplied 32-byte IV.
226    ///
227    /// **Production code MUST use [`encrypt_sync`] (or the async [`encrypt`]
228    /// trait method).** Reusing an IV with the same key breaks AES-GCM
229    /// authenticity. This entry point exists so cross-impl conformance vectors
230    /// can byte-lock the canonical wire format. See MPC-Spec §06.16 / ADR-0030.
231    ///
232    /// Returns IV(32) || ciphertext || auth tag(16) — identical layout to
233    /// [`encrypt_sync`]. Key derivation path is identical; only the IV source
234    /// differs.
235    pub fn encrypt_with_iv_sync(
236        &self,
237        plaintext: &[u8],
238        protocol: &Protocol,
239        key_id: &str,
240        counterparty: &Counterparty,
241        iv: &[u8; 32],
242    ) -> Result<Vec<u8>, WalletError> {
243        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
244        let sym_key = self
245            .key_deriver
246            .derive_symmetric_key(protocol, key_id, &effective)?;
247        Ok(sym_key.encrypt_with_iv(plaintext, iv)?)
248    }
249
250    /// Decrypt ciphertext using a derived symmetric key (AES-GCM).
251    ///
252    /// Derives the same symmetric key used for encryption and decrypts.
253    /// Expects format: IV(32) || ciphertext || auth tag(16).
254    pub fn decrypt_sync(
255        &self,
256        ciphertext: &[u8],
257        protocol: &Protocol,
258        key_id: &str,
259        counterparty: &Counterparty,
260    ) -> Result<Vec<u8>, WalletError> {
261        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
262        let sym_key = self
263            .key_deriver
264            .derive_symmetric_key(protocol, key_id, &effective)?;
265        Ok(sym_key.decrypt(ciphertext)?)
266    }
267
268    /// Create an HMAC-SHA256 over data using a derived symmetric key.
269    ///
270    /// Returns a 32-byte HMAC value.
271    pub fn create_hmac_sync(
272        &self,
273        data: &[u8],
274        protocol: &Protocol,
275        key_id: &str,
276        counterparty: &Counterparty,
277    ) -> Result<Vec<u8>, WalletError> {
278        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
279        let sym_key = self
280            .key_deriver
281            .derive_symmetric_key(protocol, key_id, &effective)?;
282        let key_bytes = sym_key.to_bytes();
283        let hmac = sha256_hmac(&key_bytes, data);
284        Ok(hmac.to_vec())
285    }
286
287    /// Verify an HMAC-SHA256 value over data using a derived symmetric key.
288    ///
289    /// Computes the expected HMAC and compares it with the provided value
290    /// using constant-time comparison.
291    pub fn verify_hmac_sync(
292        &self,
293        data: &[u8],
294        hmac_value: &[u8],
295        protocol: &Protocol,
296        key_id: &str,
297        counterparty: &Counterparty,
298    ) -> Result<bool, WalletError> {
299        let expected = self.create_hmac_sync(data, protocol, key_id, counterparty)?;
300        // Constant-time comparison to prevent timing attacks
301        Ok(constant_time_eq(&expected, hmac_value))
302    }
303
304    /// Reveal counterparty key linkage to a verifier.
305    ///
306    /// Creates an encrypted revelation of the shared secret between this wallet
307    /// and the counterparty, along with an encrypted HMAC proof. Both are
308    /// encrypted for the verifier using the "counterparty linkage revelation" protocol.
309    pub fn reveal_counterparty_key_linkage_sync(
310        &self,
311        counterparty: &Counterparty,
312        verifier: &PublicKey,
313    ) -> Result<RevealCounterpartyResult, WalletError> {
314        // Get the shared secret point as a public key
315        let linkage_point = self.key_deriver.reveal_counterparty_secret(counterparty)?;
316        let linkage_bytes = linkage_point.to_der(); // compressed 33 bytes
317
318        let prover = self.key_deriver.identity_key();
319
320        // Create a revelation timestamp
321        // Use a simple UTC timestamp format
322        let revelation_time = current_utc_timestamp();
323
324        let verifier_counterparty = Counterparty {
325            counterparty_type: CounterpartyType::Other,
326            public_key: Some(verifier.clone()),
327        };
328
329        let linkage_protocol = Protocol {
330            security_level: 2,
331            protocol: "counterparty linkage revelation".to_string(),
332        };
333
334        // Encrypt the linkage bytes for the verifier
335        let encrypted_linkage = self.encrypt_sync(
336            &linkage_bytes,
337            &linkage_protocol,
338            &revelation_time,
339            &verifier_counterparty,
340        )?;
341
342        // Create Schnorr DLEQ proof (matching TS SDK ProtoWallet.ts)
343        // Proves knowledge of private key `a` such that A = a*G and S = a*B
344        let linkage_point = Point::from_der(&linkage_bytes)
345            .map_err(|e| WalletError::Internal(format!("invalid linkage point: {}", e)))?;
346        let counterparty_pub = match &counterparty.public_key {
347            Some(pk) => pk.clone(),
348            None => {
349                return Err(WalletError::InvalidParameter(
350                    "counterparty public key required for linkage revelation".to_string(),
351                ))
352            }
353        };
354        let schnorr_proof = schnorr_generate_proof(
355            self.key_deriver.root_key(),
356            &self.key_deriver.identity_key(),
357            &counterparty_pub,
358            &linkage_point,
359        )?;
360        // Serialize proof as R(33) || S'(33) || z(variable) matching TS SDK format
361        let mut proof_bin = Vec::with_capacity(33 + 33 + 32);
362        proof_bin.extend_from_slice(&schnorr_proof.r_point.to_der(true));
363        proof_bin.extend_from_slice(&schnorr_proof.s_prime.to_der(true));
364        proof_bin.extend_from_slice(&schnorr_proof.z.to_bytes());
365        let encrypted_proof = self.encrypt_sync(
366            &proof_bin,
367            &linkage_protocol,
368            &revelation_time,
369            &verifier_counterparty,
370        )?;
371
372        Ok(RevealCounterpartyResult {
373            prover,
374            counterparty: counterparty_pub,
375            verifier: verifier.clone(),
376            revelation_time,
377            encrypted_linkage,
378            encrypted_linkage_proof: encrypted_proof,
379        })
380    }
381
382    /// Reveal specific key linkage for a given protocol and key ID to a verifier.
383    ///
384    /// Encrypts the specific secret and a proof byte for the verifier using a
385    /// special "specific linkage revelation" protocol.
386    pub fn reveal_specific_key_linkage_sync(
387        &self,
388        counterparty: &Counterparty,
389        verifier: &PublicKey,
390        protocol: &Protocol,
391        key_id: &str,
392    ) -> Result<RevealSpecificResult, WalletError> {
393        // Get the specific secret (HMAC of shared secret + invoice number)
394        let linkage = self
395            .key_deriver
396            .reveal_specific_secret(counterparty, protocol, key_id)?;
397
398        let prover = self.key_deriver.identity_key();
399
400        let verifier_counterparty = Counterparty {
401            counterparty_type: CounterpartyType::Other,
402            public_key: Some(verifier.clone()),
403        };
404
405        // Build the special protocol for specific linkage revelation
406        let encrypt_protocol = Protocol {
407            security_level: 2,
408            protocol: format!(
409                "specific linkage revelation {} {}",
410                protocol.security_level, protocol.protocol
411            ),
412        };
413
414        // Encrypt the linkage for the verifier
415        let encrypted_linkage =
416            self.encrypt_sync(&linkage, &encrypt_protocol, key_id, &verifier_counterparty)?;
417
418        // Encrypt proof type byte (0 = no proof) for the verifier
419        let proof_bytes: [u8; 1] = [0];
420        let encrypted_proof = self.encrypt_sync(
421            &proof_bytes,
422            &encrypt_protocol,
423            key_id,
424            &verifier_counterparty,
425        )?;
426
427        // Extract the counterparty public key
428        let counterparty_pub = match &counterparty.public_key {
429            Some(pk) => pk.clone(),
430            None => {
431                return Err(WalletError::InvalidParameter(
432                    "counterparty public key required for linkage revelation".to_string(),
433                ))
434            }
435        };
436
437        Ok(RevealSpecificResult {
438            encrypted_linkage,
439            encrypted_linkage_proof: encrypted_proof,
440            prover,
441            verifier: verifier.clone(),
442            counterparty: counterparty_pub,
443            protocol: protocol.clone(),
444            key_id: key_id.to_string(),
445            proof_type: 0,
446        })
447    }
448
449    /// Default an Uninitialized counterparty to the given type.
450    fn default_counterparty(
451        &self,
452        counterparty: &Counterparty,
453        default_type: CounterpartyType,
454    ) -> Counterparty {
455        if counterparty.counterparty_type == CounterpartyType::Uninitialized {
456            Counterparty {
457                counterparty_type: default_type,
458                public_key: None,
459            }
460        } else {
461            counterparty.clone()
462        }
463    }
464}
465
466// ---------------------------------------------------------------------------
467// WalletInterface implementation
468// ---------------------------------------------------------------------------
469//
470// Matches TS SDK CompletedProtoWallet: crypto methods delegate to existing
471// ProtoWallet logic; all other methods return NotImplemented.
472
473#[async_trait::async_trait]
474impl WalletInterface for ProtoWallet {
475    // -- Action methods (not supported) --
476
477    async fn create_action(
478        &self,
479        _args: CreateActionArgs,
480        _originator: Option<&str>,
481    ) -> Result<CreateActionResult, WalletError> {
482        Err(WalletError::NotImplemented("createAction".to_string()))
483    }
484
485    async fn sign_action(
486        &self,
487        _args: SignActionArgs,
488        _originator: Option<&str>,
489    ) -> Result<SignActionResult, WalletError> {
490        Err(WalletError::NotImplemented("signAction".to_string()))
491    }
492
493    async fn abort_action(
494        &self,
495        _args: AbortActionArgs,
496        _originator: Option<&str>,
497    ) -> Result<AbortActionResult, WalletError> {
498        Err(WalletError::NotImplemented("abortAction".to_string()))
499    }
500
501    async fn list_actions(
502        &self,
503        _args: ListActionsArgs,
504        _originator: Option<&str>,
505    ) -> Result<ListActionsResult, WalletError> {
506        Err(WalletError::NotImplemented("listActions".to_string()))
507    }
508
509    async fn internalize_action(
510        &self,
511        _args: InternalizeActionArgs,
512        _originator: Option<&str>,
513    ) -> Result<InternalizeActionResult, WalletError> {
514        Err(WalletError::NotImplemented("internalizeAction".to_string()))
515    }
516
517    // -- Output methods (not supported) --
518
519    async fn list_outputs(
520        &self,
521        _args: ListOutputsArgs,
522        _originator: Option<&str>,
523    ) -> Result<ListOutputsResult, WalletError> {
524        Err(WalletError::NotImplemented("listOutputs".to_string()))
525    }
526
527    async fn relinquish_output(
528        &self,
529        _args: RelinquishOutputArgs,
530        _originator: Option<&str>,
531    ) -> Result<RelinquishOutputResult, WalletError> {
532        Err(WalletError::NotImplemented("relinquishOutput".to_string()))
533    }
534
535    // -- Key/Crypto methods (supported — delegates to ProtoWallet methods) --
536
537    async fn get_public_key(
538        &self,
539        args: GetPublicKeyArgs,
540        _originator: Option<&str>,
541    ) -> Result<GetPublicKeyResult, WalletError> {
542        if args.privileged {
543            return Err(WalletError::NotImplemented(
544                "privileged key access not supported by ProtoWallet".to_string(),
545            ));
546        }
547        let protocol = args.protocol_id.unwrap_or(Protocol {
548            security_level: 0,
549            protocol: String::new(),
550        });
551        let key_id = args.key_id.unwrap_or_default();
552        let counterparty = args.counterparty.unwrap_or(Counterparty {
553            counterparty_type: CounterpartyType::Uninitialized,
554            public_key: None,
555        });
556        let for_self = args.for_self.unwrap_or(false);
557        let pk = self.get_public_key_sync(
558            &protocol,
559            &key_id,
560            &counterparty,
561            for_self,
562            args.identity_key,
563        )?;
564        Ok(GetPublicKeyResult { public_key: pk })
565    }
566
567    async fn reveal_counterparty_key_linkage(
568        &self,
569        args: RevealCounterpartyKeyLinkageArgs,
570        _originator: Option<&str>,
571    ) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> {
572        let counterparty = Counterparty {
573            counterparty_type: CounterpartyType::Other,
574            public_key: Some(args.counterparty),
575        };
576        let result = self.reveal_counterparty_key_linkage_sync(&counterparty, &args.verifier)?;
577        Ok(RevealCounterpartyKeyLinkageResult {
578            prover: result.prover,
579            counterparty: result.counterparty,
580            verifier: result.verifier,
581            revelation_time: result.revelation_time,
582            encrypted_linkage: result.encrypted_linkage,
583            encrypted_linkage_proof: result.encrypted_linkage_proof,
584        })
585    }
586
587    async fn reveal_specific_key_linkage(
588        &self,
589        args: RevealSpecificKeyLinkageArgs,
590        _originator: Option<&str>,
591    ) -> Result<RevealSpecificKeyLinkageResult, WalletError> {
592        let result = self.reveal_specific_key_linkage_sync(
593            &args.counterparty,
594            &args.verifier,
595            &args.protocol_id,
596            &args.key_id,
597        )?;
598        Ok(RevealSpecificKeyLinkageResult {
599            encrypted_linkage: result.encrypted_linkage,
600            encrypted_linkage_proof: result.encrypted_linkage_proof,
601            prover: result.prover,
602            verifier: result.verifier,
603            counterparty: result.counterparty,
604            protocol_id: result.protocol.clone(),
605            key_id: result.key_id.clone(),
606            proof_type: result.proof_type,
607        })
608    }
609
610    async fn encrypt(
611        &self,
612        args: EncryptArgs,
613        _originator: Option<&str>,
614    ) -> Result<EncryptResult, WalletError> {
615        let ciphertext = self.encrypt_sync(
616            &args.plaintext,
617            &args.protocol_id,
618            &args.key_id,
619            &args.counterparty,
620        )?;
621        Ok(EncryptResult { ciphertext })
622    }
623
624    async fn decrypt(
625        &self,
626        args: DecryptArgs,
627        _originator: Option<&str>,
628    ) -> Result<DecryptResult, WalletError> {
629        let plaintext = self.decrypt_sync(
630            &args.ciphertext,
631            &args.protocol_id,
632            &args.key_id,
633            &args.counterparty,
634        )?;
635        Ok(DecryptResult { plaintext })
636    }
637
638    async fn create_hmac(
639        &self,
640        args: CreateHmacArgs,
641        _originator: Option<&str>,
642    ) -> Result<CreateHmacResult, WalletError> {
643        let hmac = self.create_hmac_sync(
644            &args.data,
645            &args.protocol_id,
646            &args.key_id,
647            &args.counterparty,
648        )?;
649        Ok(CreateHmacResult { hmac })
650    }
651
652    async fn verify_hmac(
653        &self,
654        args: VerifyHmacArgs,
655        _originator: Option<&str>,
656    ) -> Result<VerifyHmacResult, WalletError> {
657        let valid = self.verify_hmac_sync(
658            &args.data,
659            &args.hmac,
660            &args.protocol_id,
661            &args.key_id,
662            &args.counterparty,
663        )?;
664        // Match TS SDK behavior: throw error on invalid HMAC instead of returning false
665        if !valid {
666            return Err(WalletError::InvalidHmac);
667        }
668        Ok(VerifyHmacResult { valid: true })
669    }
670
671    async fn create_signature(
672        &self,
673        args: CreateSignatureArgs,
674        _originator: Option<&str>,
675    ) -> Result<CreateSignatureResult, WalletError> {
676        let signature = self.create_signature_sync(
677            args.data.as_deref(),
678            args.hash_to_directly_sign.as_deref(),
679            &args.protocol_id,
680            &args.key_id,
681            &args.counterparty,
682        )?;
683        Ok(CreateSignatureResult { signature })
684    }
685
686    async fn verify_signature(
687        &self,
688        args: VerifySignatureArgs,
689        _originator: Option<&str>,
690    ) -> Result<VerifySignatureResult, WalletError> {
691        let for_self = args.for_self.unwrap_or(false);
692        let valid = self.verify_signature_sync(
693            args.data.as_deref(),
694            args.hash_to_directly_verify.as_deref(),
695            &args.signature,
696            &args.protocol_id,
697            &args.key_id,
698            &args.counterparty,
699            for_self,
700        )?;
701        // Match TS SDK behavior: throw error on invalid signature instead of returning false
702        if !valid {
703            return Err(WalletError::InvalidSignature);
704        }
705        Ok(VerifySignatureResult { valid: true })
706    }
707
708    // -- Certificate methods (not supported) --
709
710    async fn acquire_certificate(
711        &self,
712        _args: AcquireCertificateArgs,
713        _originator: Option<&str>,
714    ) -> Result<Certificate, WalletError> {
715        Err(WalletError::NotImplemented(
716            "acquireCertificate".to_string(),
717        ))
718    }
719
720    async fn list_certificates(
721        &self,
722        _args: ListCertificatesArgs,
723        _originator: Option<&str>,
724    ) -> Result<ListCertificatesResult, WalletError> {
725        Err(WalletError::NotImplemented("listCertificates".to_string()))
726    }
727
728    async fn prove_certificate(
729        &self,
730        _args: ProveCertificateArgs,
731        _originator: Option<&str>,
732    ) -> Result<ProveCertificateResult, WalletError> {
733        Err(WalletError::NotImplemented("proveCertificate".to_string()))
734    }
735
736    async fn relinquish_certificate(
737        &self,
738        _args: RelinquishCertificateArgs,
739        _originator: Option<&str>,
740    ) -> Result<RelinquishCertificateResult, WalletError> {
741        Err(WalletError::NotImplemented(
742            "relinquishCertificate".to_string(),
743        ))
744    }
745
746    // -- Discovery methods (not supported) --
747
748    async fn discover_by_identity_key(
749        &self,
750        _args: DiscoverByIdentityKeyArgs,
751        _originator: Option<&str>,
752    ) -> Result<DiscoverCertificatesResult, WalletError> {
753        Err(WalletError::NotImplemented(
754            "discoverByIdentityKey".to_string(),
755        ))
756    }
757
758    async fn discover_by_attributes(
759        &self,
760        _args: DiscoverByAttributesArgs,
761        _originator: Option<&str>,
762    ) -> Result<DiscoverCertificatesResult, WalletError> {
763        Err(WalletError::NotImplemented(
764            "discoverByAttributes".to_string(),
765        ))
766    }
767
768    // -- Auth/Info methods (not supported) --
769
770    async fn is_authenticated(
771        &self,
772        _originator: Option<&str>,
773    ) -> Result<AuthenticatedResult, WalletError> {
774        Err(WalletError::NotImplemented("isAuthenticated".to_string()))
775    }
776
777    async fn wait_for_authentication(
778        &self,
779        _originator: Option<&str>,
780    ) -> Result<AuthenticatedResult, WalletError> {
781        Err(WalletError::NotImplemented(
782            "waitForAuthentication".to_string(),
783        ))
784    }
785
786    async fn get_height(&self, _originator: Option<&str>) -> Result<GetHeightResult, WalletError> {
787        Err(WalletError::NotImplemented("getHeight".to_string()))
788    }
789
790    async fn get_header_for_height(
791        &self,
792        _args: GetHeaderArgs,
793        _originator: Option<&str>,
794    ) -> Result<GetHeaderResult, WalletError> {
795        Err(WalletError::NotImplemented(
796            "getHeaderForHeight".to_string(),
797        ))
798    }
799
800    async fn get_network(
801        &self,
802        _originator: Option<&str>,
803    ) -> Result<GetNetworkResult, WalletError> {
804        Err(WalletError::NotImplemented("getNetwork".to_string()))
805    }
806
807    async fn get_version(
808        &self,
809        _originator: Option<&str>,
810    ) -> Result<GetVersionResult, WalletError> {
811        Err(WalletError::NotImplemented("getVersion".to_string()))
812    }
813}
814
815/// Constant-time byte comparison to prevent timing attacks.
816fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
817    if a.len() != b.len() {
818        return false;
819    }
820    let mut diff: u8 = 0;
821    for (x, y) in a.iter().zip(b.iter()) {
822        diff |= x ^ y;
823    }
824    diff == 0
825}
826
827/// Returns a UTC timestamp string suitable for use as a key ID.
828fn current_utc_timestamp() -> String {
829    // Use a simple epoch-based timestamp to avoid external dependencies.
830    // Format: seconds since epoch as a string.
831    use std::time::{SystemTime, UNIX_EPOCH};
832    let now = SystemTime::now()
833        .duration_since(UNIX_EPOCH)
834        .unwrap_or_default();
835    format!("{}", now.as_secs())
836}
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841    use crate::wallet::types::{BooleanDefaultFalse, BooleanDefaultTrue};
842
843    fn test_protocol() -> Protocol {
844        Protocol {
845            security_level: 2,
846            protocol: "test proto wallet".to_string(),
847        }
848    }
849
850    fn self_counterparty() -> Counterparty {
851        Counterparty {
852            counterparty_type: CounterpartyType::Self_,
853            public_key: None,
854        }
855    }
856
857    fn test_private_key() -> PrivateKey {
858        PrivateKey::from_hex("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
859            .unwrap()
860    }
861
862    #[test]
863    fn test_new_creates_wallet_with_correct_identity_key() {
864        let pk = test_private_key();
865        let expected_pub = pk.to_public_key();
866        let wallet = ProtoWallet::new(pk);
867        let identity = wallet
868            .get_public_key_sync(&test_protocol(), "1", &self_counterparty(), false, true)
869            .unwrap();
870        assert_eq!(identity.to_der_hex(), expected_pub.to_der_hex());
871    }
872
873    #[test]
874    fn test_get_public_key_identity_key_true() {
875        let pk = test_private_key();
876        let expected = pk.to_public_key().to_der_hex();
877        let wallet = ProtoWallet::new(pk);
878        let result = wallet
879            .get_public_key_sync(&test_protocol(), "1", &self_counterparty(), false, true)
880            .unwrap();
881        assert_eq!(result.to_der_hex(), expected);
882    }
883
884    #[test]
885    fn test_get_public_key_derived() {
886        let wallet = ProtoWallet::new(test_private_key());
887        let protocol = test_protocol();
888        let pub1 = wallet
889            .get_public_key_sync(&protocol, "key1", &self_counterparty(), true, false)
890            .unwrap();
891        let pub2 = wallet
892            .get_public_key_sync(&protocol, "key2", &self_counterparty(), true, false)
893            .unwrap();
894        // Different key IDs should produce different derived keys
895        assert_ne!(pub1.to_der_hex(), pub2.to_der_hex());
896    }
897
898    #[test]
899    fn test_create_and_verify_signature_roundtrip() {
900        let wallet = ProtoWallet::new(test_private_key());
901        let protocol = test_protocol();
902        let counterparty = self_counterparty();
903        let data = b"hello world signature test";
904
905        let sig = wallet
906            .create_signature_sync(Some(data), None, &protocol, "sig1", &counterparty)
907            .unwrap();
908        assert!(!sig.is_empty());
909
910        let valid = wallet
911            .verify_signature_sync(
912                Some(data),
913                None,
914                &sig,
915                &protocol,
916                "sig1",
917                &counterparty,
918                true,
919            )
920            .unwrap();
921        assert!(valid, "signature should verify");
922    }
923
924    #[test]
925    fn test_verify_signature_rejects_wrong_data() {
926        let wallet = ProtoWallet::new(test_private_key());
927        let protocol = test_protocol();
928        let counterparty = self_counterparty();
929
930        let sig = wallet
931            .create_signature_sync(
932                Some(b"correct data"),
933                None,
934                &protocol,
935                "sig2",
936                &counterparty,
937            )
938            .unwrap();
939        let valid = wallet
940            .verify_signature_sync(
941                Some(b"wrong data"),
942                None,
943                &sig,
944                &protocol,
945                "sig2",
946                &counterparty,
947                true,
948            )
949            .unwrap();
950        assert!(!valid, "signature should not verify for wrong data");
951    }
952
953    #[test]
954    fn test_encrypt_decrypt_roundtrip() {
955        let wallet = ProtoWallet::new(test_private_key());
956        let protocol = test_protocol();
957        let counterparty = self_counterparty();
958        let plaintext = b"secret message for encryption";
959
960        let ciphertext = wallet
961            .encrypt_sync(plaintext, &protocol, "enc1", &counterparty)
962            .unwrap();
963        assert_ne!(ciphertext.as_slice(), plaintext);
964
965        let decrypted = wallet
966            .decrypt_sync(&ciphertext, &protocol, "enc1", &counterparty)
967            .unwrap();
968        assert_eq!(decrypted, plaintext);
969    }
970
971    #[test]
972    fn test_encrypt_decrypt_empty_plaintext() {
973        let wallet = ProtoWallet::new(test_private_key());
974        let protocol = test_protocol();
975        let counterparty = self_counterparty();
976
977        let ciphertext = wallet
978            .encrypt_sync(b"", &protocol, "enc2", &counterparty)
979            .unwrap();
980        let decrypted = wallet
981            .decrypt_sync(&ciphertext, &protocol, "enc2", &counterparty)
982            .unwrap();
983        assert!(decrypted.is_empty());
984    }
985
986    #[test]
987    fn test_create_and_verify_hmac_roundtrip() {
988        let wallet = ProtoWallet::new(test_private_key());
989        let protocol = test_protocol();
990        let counterparty = self_counterparty();
991        let data = b"hmac test data";
992
993        let hmac = wallet
994            .create_hmac_sync(data, &protocol, "hmac1", &counterparty)
995            .unwrap();
996        assert_eq!(hmac.len(), 32);
997
998        let valid = wallet
999            .verify_hmac_sync(data, &hmac, &protocol, "hmac1", &counterparty)
1000            .unwrap();
1001        assert!(valid, "HMAC should verify");
1002    }
1003
1004    #[test]
1005    fn test_verify_hmac_rejects_wrong_data() {
1006        let wallet = ProtoWallet::new(test_private_key());
1007        let protocol = test_protocol();
1008        let counterparty = self_counterparty();
1009
1010        let hmac = wallet
1011            .create_hmac_sync(b"correct", &protocol, "hmac2", &counterparty)
1012            .unwrap();
1013        let valid = wallet
1014            .verify_hmac_sync(b"wrong", &hmac, &protocol, "hmac2", &counterparty)
1015            .unwrap();
1016        assert!(!valid, "HMAC should not verify for wrong data");
1017    }
1018
1019    #[test]
1020    fn test_hmac_deterministic() {
1021        let wallet = ProtoWallet::new(test_private_key());
1022        let protocol = test_protocol();
1023        let counterparty = self_counterparty();
1024        let data = b"deterministic hmac";
1025
1026        let hmac1 = wallet
1027            .create_hmac_sync(data, &protocol, "hmac3", &counterparty)
1028            .unwrap();
1029        let hmac2 = wallet
1030            .create_hmac_sync(data, &protocol, "hmac3", &counterparty)
1031            .unwrap();
1032        assert_eq!(hmac1, hmac2);
1033    }
1034
1035    #[test]
1036    fn test_anyone_wallet_encrypt_decrypt() {
1037        let anyone = ProtoWallet::anyone();
1038        let other_key = test_private_key();
1039        let other_pub = other_key.to_public_key();
1040
1041        let counterparty = Counterparty {
1042            counterparty_type: CounterpartyType::Other,
1043            public_key: Some(other_pub),
1044        };
1045        let protocol = test_protocol();
1046        let plaintext = b"message from anyone";
1047
1048        let ciphertext = anyone
1049            .encrypt_sync(plaintext, &protocol, "anon1", &counterparty)
1050            .unwrap();
1051        let decrypted = anyone
1052            .decrypt_sync(&ciphertext, &protocol, "anon1", &counterparty)
1053            .unwrap();
1054        assert_eq!(decrypted, plaintext);
1055    }
1056
1057    #[test]
1058    fn test_uninitialized_counterparty_defaults_to_self_for_encrypt() {
1059        let wallet = ProtoWallet::new(test_private_key());
1060        let protocol = test_protocol();
1061        let uninit = Counterparty {
1062            counterparty_type: CounterpartyType::Uninitialized,
1063            public_key: None,
1064        };
1065        let self_cp = self_counterparty();
1066
1067        let ct_uninit = wallet
1068            .encrypt_sync(b"test", &protocol, "def1", &uninit)
1069            .unwrap();
1070        // Both should decrypt with Self_ counterparty
1071        let decrypted = wallet
1072            .decrypt_sync(&ct_uninit, &protocol, "def1", &self_cp)
1073            .unwrap();
1074        assert_eq!(decrypted, b"test");
1075    }
1076
1077    #[test]
1078    fn test_reveal_specific_key_linkage() {
1079        let wallet_a = ProtoWallet::new(test_private_key());
1080        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1081        let verifier_pub = verifier_key.to_public_key();
1082
1083        let counterparty_key = PrivateKey::from_hex("bb").unwrap();
1084        let counterparty_pub = counterparty_key.to_public_key();
1085
1086        let counterparty = Counterparty {
1087            counterparty_type: CounterpartyType::Other,
1088            public_key: Some(counterparty_pub),
1089        };
1090
1091        let protocol = test_protocol();
1092        let result = wallet_a
1093            .reveal_specific_key_linkage_sync(&counterparty, &verifier_pub, &protocol, "link1")
1094            .unwrap();
1095
1096        assert!(!result.encrypted_linkage.is_empty());
1097        assert!(!result.encrypted_linkage_proof.is_empty());
1098        assert_eq!(result.proof_type, 0);
1099        assert_eq!(result.key_id, "link1");
1100    }
1101
1102    #[test]
1103    fn test_reveal_counterparty_key_linkage() {
1104        let wallet = ProtoWallet::new(test_private_key());
1105        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1106        let verifier_pub = verifier_key.to_public_key();
1107
1108        let counterparty_key = PrivateKey::from_hex("cc").unwrap();
1109        let counterparty_pub = counterparty_key.to_public_key();
1110
1111        let counterparty = Counterparty {
1112            counterparty_type: CounterpartyType::Other,
1113            public_key: Some(counterparty_pub.clone()),
1114        };
1115
1116        let result = wallet
1117            .reveal_counterparty_key_linkage_sync(&counterparty, &verifier_pub)
1118            .unwrap();
1119
1120        assert!(!result.encrypted_linkage.is_empty());
1121        assert!(!result.encrypted_linkage_proof.is_empty());
1122        assert_eq!(
1123            result.counterparty.to_der_hex(),
1124            counterparty_pub.to_der_hex()
1125        );
1126        assert_eq!(result.verifier.to_der_hex(), verifier_pub.to_der_hex());
1127        assert!(!result.revelation_time.is_empty());
1128    }
1129
1130    // -----------------------------------------------------------------------
1131    // WalletInterface trait tests
1132    // -----------------------------------------------------------------------
1133
1134    /// Helper: call WalletInterface method through trait to verify dispatch.
1135    async fn get_pub_key_via_trait<W: WalletInterface + ?Sized>(
1136        w: &W,
1137        args: GetPublicKeyArgs,
1138    ) -> Result<GetPublicKeyResult, WalletError> {
1139        w.get_public_key(args, None).await
1140    }
1141
1142    #[tokio::test]
1143    async fn test_wallet_interface_get_public_key_identity() {
1144        let pk = test_private_key();
1145        let expected = pk.to_public_key().to_der_hex();
1146        let wallet = ProtoWallet::new(pk);
1147
1148        let result = get_pub_key_via_trait(
1149            &wallet,
1150            GetPublicKeyArgs {
1151                identity_key: true,
1152                protocol_id: None,
1153                key_id: None,
1154                counterparty: None,
1155                privileged: false,
1156                privileged_reason: None,
1157                for_self: None,
1158                seek_permission: None,
1159            },
1160        )
1161        .await
1162        .unwrap();
1163
1164        assert_eq!(result.public_key.to_der_hex(), expected);
1165    }
1166
1167    #[tokio::test]
1168    async fn test_wallet_interface_get_public_key_derived() {
1169        let wallet = ProtoWallet::new(test_private_key());
1170
1171        let result = get_pub_key_via_trait(
1172            &wallet,
1173            GetPublicKeyArgs {
1174                identity_key: false,
1175                protocol_id: Some(test_protocol()),
1176                key_id: Some("derived1".to_string()),
1177                counterparty: Some(self_counterparty()),
1178                privileged: false,
1179                privileged_reason: None,
1180                for_self: Some(true),
1181                seek_permission: None,
1182            },
1183        )
1184        .await
1185        .unwrap();
1186
1187        // Should match the direct method call
1188        let direct = wallet
1189            .get_public_key_sync(
1190                &test_protocol(),
1191                "derived1",
1192                &self_counterparty(),
1193                true,
1194                false,
1195            )
1196            .unwrap();
1197        assert_eq!(result.public_key.to_der_hex(), direct.to_der_hex());
1198    }
1199
1200    #[tokio::test]
1201    async fn test_wallet_interface_privileged_rejected() {
1202        let wallet = ProtoWallet::new(test_private_key());
1203        let err = WalletInterface::get_public_key(
1204            &wallet,
1205            GetPublicKeyArgs {
1206                identity_key: true,
1207                protocol_id: None,
1208                key_id: None,
1209                counterparty: None,
1210                privileged: true,
1211                privileged_reason: Some("test".to_string()),
1212                for_self: None,
1213                seek_permission: None,
1214            },
1215            None,
1216        )
1217        .await;
1218
1219        assert!(err.is_err());
1220        let msg = format!("{}", err.unwrap_err());
1221        assert!(msg.contains("not implemented"), "got: {}", msg);
1222    }
1223
1224    #[tokio::test]
1225    async fn test_wallet_interface_create_verify_signature() {
1226        let wallet = ProtoWallet::new(test_private_key());
1227        let data = b"test data for wallet interface sig".to_vec();
1228
1229        let sig_result = WalletInterface::create_signature(
1230            &wallet,
1231            CreateSignatureArgs {
1232                protocol_id: test_protocol(),
1233                key_id: "wsig1".to_string(),
1234                counterparty: self_counterparty(),
1235                data: Some(data.clone()),
1236                hash_to_directly_sign: None,
1237                privileged: false,
1238                privileged_reason: None,
1239                seek_permission: None,
1240            },
1241            None,
1242        )
1243        .await
1244        .unwrap();
1245
1246        let verify_result = WalletInterface::verify_signature(
1247            &wallet,
1248            VerifySignatureArgs {
1249                protocol_id: test_protocol(),
1250                key_id: "wsig1".to_string(),
1251                counterparty: self_counterparty(),
1252                data: Some(data),
1253                hash_to_directly_verify: None,
1254                signature: sig_result.signature,
1255                for_self: Some(true),
1256                privileged: false,
1257                privileged_reason: None,
1258                seek_permission: None,
1259            },
1260            None,
1261        )
1262        .await
1263        .unwrap();
1264
1265        assert!(verify_result.valid);
1266    }
1267
1268    #[tokio::test]
1269    async fn test_wallet_interface_encrypt_decrypt() {
1270        let wallet = ProtoWallet::new(test_private_key());
1271        let plaintext = b"wallet interface encrypt test".to_vec();
1272
1273        let enc = WalletInterface::encrypt(
1274            &wallet,
1275            EncryptArgs {
1276                protocol_id: test_protocol(),
1277                key_id: "wenc1".to_string(),
1278                counterparty: self_counterparty(),
1279                plaintext: plaintext.clone(),
1280                privileged: false,
1281                privileged_reason: None,
1282                seek_permission: None,
1283            },
1284            None,
1285        )
1286        .await
1287        .unwrap();
1288
1289        let dec = WalletInterface::decrypt(
1290            &wallet,
1291            DecryptArgs {
1292                protocol_id: test_protocol(),
1293                key_id: "wenc1".to_string(),
1294                counterparty: self_counterparty(),
1295                ciphertext: enc.ciphertext,
1296                privileged: false,
1297                privileged_reason: None,
1298                seek_permission: None,
1299            },
1300            None,
1301        )
1302        .await
1303        .unwrap();
1304
1305        assert_eq!(dec.plaintext, plaintext);
1306    }
1307
1308    #[tokio::test]
1309    async fn test_wallet_interface_hmac_roundtrip() {
1310        let wallet = ProtoWallet::new(test_private_key());
1311        let data = b"wallet interface hmac test".to_vec();
1312
1313        let hmac_result = WalletInterface::create_hmac(
1314            &wallet,
1315            CreateHmacArgs {
1316                protocol_id: test_protocol(),
1317                key_id: "whmac1".to_string(),
1318                counterparty: self_counterparty(),
1319                data: data.clone(),
1320                privileged: false,
1321                privileged_reason: None,
1322                seek_permission: None,
1323            },
1324            None,
1325        )
1326        .await
1327        .unwrap();
1328
1329        assert_eq!(hmac_result.hmac.len(), 32);
1330
1331        let verify = WalletInterface::verify_hmac(
1332            &wallet,
1333            VerifyHmacArgs {
1334                protocol_id: test_protocol(),
1335                key_id: "whmac1".to_string(),
1336                counterparty: self_counterparty(),
1337                data,
1338                hmac: hmac_result.hmac,
1339                privileged: false,
1340                privileged_reason: None,
1341                seek_permission: None,
1342            },
1343            None,
1344        )
1345        .await
1346        .unwrap();
1347
1348        assert!(verify.valid);
1349    }
1350
1351    #[tokio::test]
1352    async fn test_wallet_interface_unsupported_methods_return_not_implemented() {
1353        use crate::wallet::interfaces::*;
1354        let wallet = ProtoWallet::new(test_private_key());
1355
1356        // Each unsupported method should return NotImplemented, matching TS SDK
1357        // CompletedProtoWallet which throws "not implemented" for these.
1358        let err = WalletInterface::is_authenticated(&wallet, None).await;
1359        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1360
1361        let err = WalletInterface::wait_for_authentication(&wallet, None).await;
1362        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1363
1364        let err = WalletInterface::get_network(&wallet, None).await;
1365        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1366
1367        let err = WalletInterface::get_version(&wallet, None).await;
1368        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1369
1370        let err = WalletInterface::get_height(&wallet, None).await;
1371        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1372
1373        let err =
1374            WalletInterface::get_header_for_height(&wallet, GetHeaderArgs { height: 0 }, None)
1375                .await;
1376        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1377
1378        let err = WalletInterface::list_outputs(
1379            &wallet,
1380            ListOutputsArgs {
1381                basket: "test".to_string(),
1382                tags: vec![],
1383                tag_query_mode: None,
1384                include: None,
1385                include_custom_instructions: BooleanDefaultFalse(None),
1386                include_tags: BooleanDefaultFalse(None),
1387                include_labels: BooleanDefaultFalse(None),
1388                limit: Some(10),
1389                offset: None,
1390                seek_permission: BooleanDefaultTrue(None),
1391            },
1392            None,
1393        )
1394        .await;
1395        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1396    }
1397
1398    #[tokio::test]
1399    async fn test_wallet_interface_reveal_counterparty_key_linkage() {
1400        let wallet = ProtoWallet::new(test_private_key());
1401        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1402        let counterparty_key = PrivateKey::from_hex("cc").unwrap();
1403
1404        let result = WalletInterface::reveal_counterparty_key_linkage(
1405            &wallet,
1406            RevealCounterpartyKeyLinkageArgs {
1407                counterparty: counterparty_key.to_public_key(),
1408                verifier: verifier_key.to_public_key(),
1409                privileged: None,
1410                privileged_reason: None,
1411            },
1412            None,
1413        )
1414        .await
1415        .unwrap();
1416
1417        assert!(!result.encrypted_linkage.is_empty());
1418        assert!(!result.encrypted_linkage_proof.is_empty());
1419        assert_eq!(
1420            result.counterparty.to_der_hex(),
1421            counterparty_key.to_public_key().to_der_hex()
1422        );
1423        assert!(!result.revelation_time.is_empty());
1424    }
1425
1426    #[tokio::test]
1427    async fn test_wallet_interface_reveal_specific_key_linkage() {
1428        let wallet = ProtoWallet::new(test_private_key());
1429        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1430        let counterparty_key = PrivateKey::from_hex("bb").unwrap();
1431
1432        let result = WalletInterface::reveal_specific_key_linkage(
1433            &wallet,
1434            RevealSpecificKeyLinkageArgs {
1435                counterparty: Counterparty {
1436                    counterparty_type: CounterpartyType::Other,
1437                    public_key: Some(counterparty_key.to_public_key()),
1438                },
1439                verifier: verifier_key.to_public_key(),
1440                protocol_id: test_protocol(),
1441                key_id: "wlink1".to_string(),
1442                privileged: None,
1443                privileged_reason: None,
1444            },
1445            None,
1446        )
1447        .await
1448        .unwrap();
1449
1450        assert!(!result.encrypted_linkage.is_empty());
1451        assert_eq!(result.proof_type, 0);
1452        assert_eq!(result.key_id, "wlink1");
1453    }
1454
1455    // -- Counterparty default-dispatch regression tests (C1) --
1456    //
1457    // These guard against a class of silent cross-SDK interop bugs: the TS SDK
1458    // (ProtoWallet.ts) defaults a missing `counterparty` to "self" for every
1459    // crypto op *except* createSignature, which defaults to "anyone". The
1460    // Rust side mirrors this via `ProtoWallet::default_counterparty()`, which
1461    // substitutes the per-op default only when the value is
1462    // `CounterpartyType::Uninitialized`. Therefore `Counterparty::default()`
1463    // MUST return `Uninitialized`, not a concrete value like `Self_` — otherwise
1464    // `serde(default)` on `CreateSignatureArgs.counterparty` would derive a
1465    // signature against the wrong key and fail cross-SDK verification silently.
1466
1467    #[test]
1468    fn test_counterparty_default_is_uninitialized() {
1469        // C1: guards against a regression where Counterparty::default() returns
1470        // a concrete value, which would bypass per-op default dispatch in
1471        // `default_counterparty()`.
1472        let cp = Counterparty::default();
1473        assert_eq!(cp.counterparty_type, CounterpartyType::Uninitialized);
1474        assert!(cp.public_key.is_none());
1475    }
1476
1477    #[test]
1478    fn test_create_signature_defaults_uninitialized_to_anyone() {
1479        // C1 end-to-end: calling create_signature_sync with an Uninitialized
1480        // counterparty must derive against the 'anyone' key, matching the TS
1481        // SDK's `createSignature` default. Verified by:
1482        //   (1) the signature verifies when re-derived as counterparty=Anyone
1483        //   (2) the signature does NOT verify when re-derived as counterparty=Self_
1484        //       (which is what the TS SDK would produce for all *other* ops)
1485        // Before the fix, Counterparty::default() returned Self_, causing
1486        // default_counterparty(Self_, Anyone) to pass Self_ through unchanged
1487        // and silently derive against the wrong key.
1488        let wallet = ProtoWallet::new(test_private_key());
1489        let protocol = test_protocol();
1490        let data = b"cross-sdk-interop-canary";
1491
1492        let uninit = Counterparty::default();
1493        assert_eq!(uninit.counterparty_type, CounterpartyType::Uninitialized);
1494
1495        let sig = wallet
1496            .create_signature_sync(Some(data), None, &protocol, "sig-c1", &uninit)
1497            .unwrap();
1498
1499        // (1) Must verify when the verifier explicitly uses Anyone.
1500        //     verify_signature_sync defaults Uninitialized→Self_, so we pass
1501        //     the concrete Anyone value to bypass the default and exercise
1502        //     the anyone-derived key path deliberately. for_self=true matches
1503        //     the existing self-verify test pattern — since this wallet knows
1504        //     its own private key, the local derivation yields the public
1505        //     key that matches the private key used to sign.
1506        let anyone = Counterparty {
1507            counterparty_type: CounterpartyType::Anyone,
1508            public_key: None,
1509        };
1510        let valid_anyone = wallet
1511            .verify_signature_sync(Some(data), None, &sig, &protocol, "sig-c1", &anyone, true)
1512            .unwrap();
1513        assert!(
1514            valid_anyone,
1515            "signature from Uninitialized counterparty must verify against 'anyone' derived key"
1516        );
1517
1518        // (2) Must NOT verify when the verifier uses Self_ at the same
1519        //     (protocol, key_id). This is the regression guard: if
1520        //     Counterparty::default() ever goes back to Self_, the signature
1521        //     would be produced under Self_ and (2) would start passing,
1522        //     indicating the per-op default dispatch was bypassed.
1523        let self_cp = self_counterparty();
1524        let valid_self = wallet
1525            .verify_signature_sync(Some(data), None, &sig, &protocol, "sig-c1", &self_cp, true)
1526            .unwrap_or(false);
1527        assert!(
1528            !valid_self,
1529            "signature from Uninitialized counterparty must NOT verify against 'self' derived key — \
1530             if this assertion fails, Counterparty::default() has regressed and createSignature \
1531             is no longer defaulting to 'anyone'"
1532        );
1533    }
1534
1535    #[test]
1536    fn test_encrypt_defaults_uninitialized_to_self() {
1537        // C1 dispatch counterpart: encrypt_sync defaults Uninitialized to Self_.
1538        // Demonstrates that default_counterparty() dispatches per-op rather
1539        // than via Counterparty::default(), so the fix to return Uninitialized
1540        // from Default does not regress the five non-createSignature ops.
1541        let wallet = ProtoWallet::new(test_private_key());
1542        let protocol = test_protocol();
1543        let plaintext = b"dispatch-to-self canary";
1544
1545        let ciphertext_uninit = wallet
1546            .encrypt_sync(plaintext, &protocol, "enc-c1", &Counterparty::default())
1547            .unwrap();
1548        let ciphertext_self = wallet
1549            .encrypt_sync(plaintext, &protocol, "enc-c1", &self_counterparty())
1550            .unwrap();
1551
1552        // Both should decrypt with Self_ (different ciphertexts because ECIES is
1553        // randomized, but both must round-trip through Self_).
1554        let pt1 = wallet
1555            .decrypt_sync(
1556                &ciphertext_uninit,
1557                &protocol,
1558                "enc-c1",
1559                &self_counterparty(),
1560            )
1561            .unwrap();
1562        let pt2 = wallet
1563            .decrypt_sync(&ciphertext_self, &protocol, "enc-c1", &self_counterparty())
1564            .unwrap();
1565        assert_eq!(pt1, plaintext);
1566        assert_eq!(pt2, plaintext);
1567    }
1568
1569    #[test]
1570    fn test_encrypt_with_iv_sync_byte_locks_under_fixed_inputs() {
1571        // Proves the wallet-layer key-derivation path is byte-identical between
1572        // encrypt_sync and encrypt_with_iv_sync — only the IV source differs.
1573        let identity = PrivateKey::from_bytes(&[0x01u8; 32]).unwrap();
1574        let wallet = ProtoWallet::new(identity);
1575        let protocol = Protocol {
1576            security_level: 2,
1577            protocol: "mpcpresig".to_string(),
1578        };
1579        let key_id = "byte-lock-test-001";
1580        let counterparty = Counterparty {
1581            counterparty_type: CounterpartyType::Self_,
1582            public_key: None,
1583        };
1584        let plaintext = b"presig spare plaintext 32 bytesx";
1585        let iv = [0x0au8; 32];
1586
1587        let ct1 = wallet
1588            .encrypt_with_iv_sync(plaintext, &protocol, key_id, &counterparty, &iv)
1589            .unwrap();
1590        let ct2 = wallet
1591            .encrypt_with_iv_sync(plaintext, &protocol, key_id, &counterparty, &iv)
1592            .unwrap();
1593
1594        // Determinism
1595        assert_eq!(ct1, ct2);
1596        // Layout: IV(32) || ciphertext(plaintext.len()) || tag(16)
1597        assert_eq!(ct1.len(), 32 + plaintext.len() + 16);
1598        // IV is prepended verbatim
1599        assert_eq!(&ct1[..32], &iv);
1600        // Decrypts via the production sync path (same key derivation)
1601        let pt = wallet
1602            .decrypt_sync(&ct1, &protocol, key_id, &counterparty)
1603            .unwrap();
1604        assert_eq!(pt.as_slice(), plaintext.as_slice());
1605
1606        // Hard-coded byte-lock: captures the canonical wire format so any
1607        // regression in key derivation or AES-GCM output is caught immediately.
1608        // Format: IV(32 bytes = 0x0a*32) || ciphertext(32 bytes) || tag(16 bytes).
1609        // Any change to key derivation (BRC-42 ECDH-self, HMAC offset, child-key
1610        // x-coord) or AES-GCM will break this assertion. See MPC-Spec §06.16.
1611        const EXPECTED_HEX: &str = "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a5ecbac1cba9e03ccd3599f6e862677d0e4b2bc96b6a58f9d82b04cee442dfe61bbaab30e7674c83b7139e447f7c30c94";
1612        assert_eq!(
1613            hex::encode(&ct1),
1614            EXPECTED_HEX,
1615            "byte-lock failed: key derivation or AES-GCM output has changed"
1616        );
1617    }
1618}