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::private_key::PrivateKey;
12use crate::primitives::public_key::PublicKey;
13use crate::primitives::signature::Signature;
14use crate::wallet::error::WalletError;
15use crate::wallet::interfaces::{
16    AbortActionArgs, AbortActionResult, AcquireCertificateArgs, AuthenticatedResult, Certificate,
17    CreateActionArgs, CreateActionResult, CreateHmacArgs, CreateHmacResult, CreateSignatureArgs,
18    CreateSignatureResult, DecryptArgs, DecryptResult, DiscoverByAttributesArgs,
19    DiscoverByIdentityKeyArgs, DiscoverCertificatesResult, EncryptArgs, EncryptResult,
20    GetHeaderArgs, GetHeaderResult, GetHeightResult, GetNetworkResult, GetPublicKeyArgs,
21    GetPublicKeyResult, GetVersionResult, InternalizeActionArgs, InternalizeActionResult,
22    ListActionsArgs, ListActionsResult, ListCertificatesArgs, ListCertificatesResult,
23    ListOutputsArgs, ListOutputsResult, ProveCertificateArgs, ProveCertificateResult,
24    RelinquishCertificateArgs, RelinquishCertificateResult, RelinquishOutputArgs,
25    RelinquishOutputResult, RevealCounterpartyKeyLinkageArgs, RevealCounterpartyKeyLinkageResult,
26    RevealSpecificKeyLinkageArgs, RevealSpecificKeyLinkageResult, SignActionArgs, SignActionResult,
27    VerifyHmacArgs, VerifyHmacResult, VerifySignatureArgs, VerifySignatureResult, WalletInterface,
28};
29use crate::wallet::key_deriver::KeyDeriver;
30use crate::wallet::types::{Counterparty, CounterpartyType, Protocol};
31
32/// Result of revealing counterparty key linkage.
33pub struct RevealCounterpartyResult {
34    /// The prover's identity public key.
35    pub prover: PublicKey,
36    /// The counterparty's public key.
37    pub counterparty: PublicKey,
38    /// The verifier's public key (who can decrypt the revelation).
39    pub verifier: PublicKey,
40    /// ISO 8601 timestamp of the revelation.
41    pub revelation_time: String,
42    /// Encrypted shared secret (linkage), encrypted for the verifier.
43    pub encrypted_linkage: Vec<u8>,
44    /// Encrypted proof of the linkage, encrypted for the verifier.
45    pub encrypted_linkage_proof: Vec<u8>,
46}
47
48/// Result of revealing specific key linkage.
49pub struct RevealSpecificResult {
50    /// Encrypted specific secret, encrypted for the verifier.
51    pub encrypted_linkage: Vec<u8>,
52    /// Encrypted proof bytes, encrypted for the verifier.
53    pub encrypted_linkage_proof: Vec<u8>,
54    /// The prover's identity public key.
55    pub prover: PublicKey,
56    /// The verifier's public key.
57    pub verifier: PublicKey,
58    /// The counterparty's public key.
59    pub counterparty: PublicKey,
60    /// The protocol used for this specific derivation.
61    pub protocol: Protocol,
62    /// The key ID used for this specific derivation.
63    pub key_id: String,
64    /// Proof type (0 = no proof for specific linkage).
65    pub proof_type: u8,
66}
67
68/// ProtoWallet is a crypto-only wallet wrapping KeyDeriver.
69///
70/// It provides foundational cryptographic operations: key derivation,
71/// signing, verification, encryption, decryption, HMAC, and key linkage
72/// revelation. Unlike a full wallet, it does not create transactions,
73/// manage outputs, or interact with the blockchain.
74pub struct ProtoWallet {
75    key_deriver: KeyDeriver,
76}
77
78impl ProtoWallet {
79    /// Create a new ProtoWallet from a private key.
80    pub fn new(private_key: PrivateKey) -> Self {
81        ProtoWallet {
82            key_deriver: KeyDeriver::new(private_key),
83        }
84    }
85
86    /// Create a new ProtoWallet from an existing KeyDeriver.
87    pub fn from_key_deriver(kd: KeyDeriver) -> Self {
88        ProtoWallet { key_deriver: kd }
89    }
90
91    /// Create an "anyone" ProtoWallet using the special anyone key (PrivateKey(1)).
92    pub fn anyone() -> Self {
93        ProtoWallet {
94            key_deriver: KeyDeriver::new_anyone(),
95        }
96    }
97
98    /// Get a public key, either the identity key or a derived key.
99    ///
100    /// If `identity_key` is true, returns the root identity public key
101    /// (protocol, key_id, counterparty, for_self are ignored).
102    /// Otherwise, derives a public key using the given parameters.
103    /// If counterparty is Uninitialized, it defaults to Self_.
104    pub fn get_public_key_sync(
105        &self,
106        protocol: &Protocol,
107        key_id: &str,
108        counterparty: &Counterparty,
109        for_self: bool,
110        identity_key: bool,
111    ) -> Result<PublicKey, WalletError> {
112        if identity_key {
113            return Ok(self.key_deriver.identity_key());
114        }
115
116        if protocol.protocol.is_empty() || key_id.is_empty() {
117            return Err(WalletError::InvalidParameter(
118                "protocolID and keyID are required if identityKey is false".to_string(),
119            ));
120        }
121
122        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
123        self.key_deriver
124            .derive_public_key(protocol, key_id, &effective, for_self)
125    }
126
127    /// Create an ECDSA signature over data.
128    ///
129    /// Hashes data with SHA-256, then signs the hash with a derived private key.
130    /// Returns the DER-encoded signature bytes.
131    pub fn create_signature_sync(
132        &self,
133        data: &[u8],
134        protocol: &Protocol,
135        key_id: &str,
136        counterparty: &Counterparty,
137    ) -> Result<Vec<u8>, WalletError> {
138        let effective = self.default_counterparty(counterparty, CounterpartyType::Anyone);
139        let derived_key = self
140            .key_deriver
141            .derive_private_key(protocol, key_id, &effective)?;
142
143        let data_hash = sha256(data);
144        // Use ecdsa_sign directly with the hash to avoid double-hashing
145        // (PrivateKey.sign() would hash again internally).
146        let sig = ecdsa_sign(&data_hash, derived_key.bn(), true)?;
147        Ok(sig.to_der())
148    }
149
150    /// Verify an ECDSA signature over data.
151    ///
152    /// Hashes data with SHA-256, parses the DER signature, and verifies
153    /// against the derived public key.
154    pub fn verify_signature_sync(
155        &self,
156        data: &[u8],
157        signature: &[u8],
158        protocol: &Protocol,
159        key_id: &str,
160        counterparty: &Counterparty,
161        for_self: bool,
162    ) -> Result<bool, WalletError> {
163        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
164        let derived_pub = self
165            .key_deriver
166            .derive_public_key(protocol, key_id, &effective, for_self)?;
167
168        let sig = Signature::from_der(signature)?;
169        let data_hash = sha256(data);
170        // Use ecdsa_verify directly with the hash to match create_signature behavior.
171        Ok(ecdsa_verify(&data_hash, &sig, derived_pub.point()))
172    }
173
174    /// Encrypt plaintext using a derived symmetric key (AES-GCM).
175    ///
176    /// Derives a symmetric key from the protocol, key ID, and counterparty,
177    /// then encrypts the plaintext. Returns IV || ciphertext || auth tag.
178    pub fn encrypt_sync(
179        &self,
180        plaintext: &[u8],
181        protocol: &Protocol,
182        key_id: &str,
183        counterparty: &Counterparty,
184    ) -> Result<Vec<u8>, WalletError> {
185        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
186        let sym_key = self
187            .key_deriver
188            .derive_symmetric_key(protocol, key_id, &effective)?;
189        Ok(sym_key.encrypt(plaintext)?)
190    }
191
192    /// Decrypt ciphertext using a derived symmetric key (AES-GCM).
193    ///
194    /// Derives the same symmetric key used for encryption and decrypts.
195    /// Expects format: IV(32) || ciphertext || auth tag(16).
196    pub fn decrypt_sync(
197        &self,
198        ciphertext: &[u8],
199        protocol: &Protocol,
200        key_id: &str,
201        counterparty: &Counterparty,
202    ) -> Result<Vec<u8>, WalletError> {
203        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
204        let sym_key = self
205            .key_deriver
206            .derive_symmetric_key(protocol, key_id, &effective)?;
207        Ok(sym_key.decrypt(ciphertext)?)
208    }
209
210    /// Create an HMAC-SHA256 over data using a derived symmetric key.
211    ///
212    /// Returns a 32-byte HMAC value.
213    pub fn create_hmac_sync(
214        &self,
215        data: &[u8],
216        protocol: &Protocol,
217        key_id: &str,
218        counterparty: &Counterparty,
219    ) -> Result<Vec<u8>, WalletError> {
220        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
221        let sym_key = self
222            .key_deriver
223            .derive_symmetric_key(protocol, key_id, &effective)?;
224        let key_bytes = sym_key.to_bytes();
225        let hmac = sha256_hmac(&key_bytes, data);
226        Ok(hmac.to_vec())
227    }
228
229    /// Verify an HMAC-SHA256 value over data using a derived symmetric key.
230    ///
231    /// Computes the expected HMAC and compares it with the provided value
232    /// using constant-time comparison.
233    pub fn verify_hmac_sync(
234        &self,
235        data: &[u8],
236        hmac_value: &[u8],
237        protocol: &Protocol,
238        key_id: &str,
239        counterparty: &Counterparty,
240    ) -> Result<bool, WalletError> {
241        let expected = self.create_hmac_sync(data, protocol, key_id, counterparty)?;
242        // Constant-time comparison to prevent timing attacks
243        Ok(constant_time_eq(&expected, hmac_value))
244    }
245
246    /// Reveal counterparty key linkage to a verifier.
247    ///
248    /// Creates an encrypted revelation of the shared secret between this wallet
249    /// and the counterparty, along with an encrypted HMAC proof. Both are
250    /// encrypted for the verifier using the "counterparty linkage revelation" protocol.
251    pub fn reveal_counterparty_key_linkage_sync(
252        &self,
253        counterparty: &Counterparty,
254        verifier: &PublicKey,
255    ) -> Result<RevealCounterpartyResult, WalletError> {
256        // Get the shared secret point as a public key
257        let linkage_point = self.key_deriver.reveal_counterparty_secret(counterparty)?;
258        let linkage_bytes = linkage_point.to_der(); // compressed 33 bytes
259
260        let prover = self.key_deriver.identity_key();
261
262        // Create a revelation timestamp
263        // Use a simple UTC timestamp format
264        let revelation_time = current_utc_timestamp();
265
266        let verifier_counterparty = Counterparty {
267            counterparty_type: CounterpartyType::Other,
268            public_key: Some(verifier.clone()),
269        };
270
271        let linkage_protocol = Protocol {
272            security_level: 2,
273            protocol: "counterparty linkage revelation".to_string(),
274        };
275
276        // Encrypt the linkage bytes for the verifier
277        let encrypted_linkage = self.encrypt_sync(
278            &linkage_bytes,
279            &linkage_protocol,
280            &revelation_time,
281            &verifier_counterparty,
282        )?;
283
284        // Create HMAC proof of the linkage and encrypt it for the verifier
285        let proof = self.create_hmac_sync(
286            &linkage_bytes,
287            &linkage_protocol,
288            &revelation_time,
289            &verifier_counterparty,
290        )?;
291        let encrypted_proof = self.encrypt_sync(
292            &proof,
293            &linkage_protocol,
294            &revelation_time,
295            &verifier_counterparty,
296        )?;
297
298        // Extract the counterparty public key for the result
299        let counterparty_pub = match &counterparty.public_key {
300            Some(pk) => pk.clone(),
301            None => {
302                return Err(WalletError::InvalidParameter(
303                    "counterparty public key required for linkage revelation".to_string(),
304                ))
305            }
306        };
307
308        Ok(RevealCounterpartyResult {
309            prover,
310            counterparty: counterparty_pub,
311            verifier: verifier.clone(),
312            revelation_time,
313            encrypted_linkage,
314            encrypted_linkage_proof: encrypted_proof,
315        })
316    }
317
318    /// Reveal specific key linkage for a given protocol and key ID to a verifier.
319    ///
320    /// Encrypts the specific secret and a proof byte for the verifier using a
321    /// special "specific linkage revelation" protocol.
322    pub fn reveal_specific_key_linkage_sync(
323        &self,
324        counterparty: &Counterparty,
325        verifier: &PublicKey,
326        protocol: &Protocol,
327        key_id: &str,
328    ) -> Result<RevealSpecificResult, WalletError> {
329        // Get the specific secret (HMAC of shared secret + invoice number)
330        let linkage = self
331            .key_deriver
332            .reveal_specific_secret(counterparty, protocol, key_id)?;
333
334        let prover = self.key_deriver.identity_key();
335
336        let verifier_counterparty = Counterparty {
337            counterparty_type: CounterpartyType::Other,
338            public_key: Some(verifier.clone()),
339        };
340
341        // Build the special protocol for specific linkage revelation
342        let encrypt_protocol = Protocol {
343            security_level: 2,
344            protocol: format!(
345                "specific linkage revelation {} {}",
346                protocol.security_level, protocol.protocol
347            ),
348        };
349
350        // Encrypt the linkage for the verifier
351        let encrypted_linkage =
352            self.encrypt_sync(&linkage, &encrypt_protocol, key_id, &verifier_counterparty)?;
353
354        // Encrypt proof type byte (0 = no proof) for the verifier
355        let proof_bytes: [u8; 1] = [0];
356        let encrypted_proof = self.encrypt_sync(
357            &proof_bytes,
358            &encrypt_protocol,
359            key_id,
360            &verifier_counterparty,
361        )?;
362
363        // Extract the counterparty public key
364        let counterparty_pub = match &counterparty.public_key {
365            Some(pk) => pk.clone(),
366            None => {
367                return Err(WalletError::InvalidParameter(
368                    "counterparty public key required for linkage revelation".to_string(),
369                ))
370            }
371        };
372
373        Ok(RevealSpecificResult {
374            encrypted_linkage,
375            encrypted_linkage_proof: encrypted_proof,
376            prover,
377            verifier: verifier.clone(),
378            counterparty: counterparty_pub,
379            protocol: protocol.clone(),
380            key_id: key_id.to_string(),
381            proof_type: 0,
382        })
383    }
384
385    /// Default an Uninitialized counterparty to the given type.
386    fn default_counterparty(
387        &self,
388        counterparty: &Counterparty,
389        default_type: CounterpartyType,
390    ) -> Counterparty {
391        if counterparty.counterparty_type == CounterpartyType::Uninitialized {
392            Counterparty {
393                counterparty_type: default_type,
394                public_key: None,
395            }
396        } else {
397            counterparty.clone()
398        }
399    }
400}
401
402// ---------------------------------------------------------------------------
403// WalletInterface implementation
404// ---------------------------------------------------------------------------
405//
406// Matches TS SDK CompletedProtoWallet: crypto methods delegate to existing
407// ProtoWallet logic; all other methods return NotImplemented.
408
409#[async_trait::async_trait]
410impl WalletInterface for ProtoWallet {
411    // -- Action methods (not supported) --
412
413    async fn create_action(
414        &self,
415        _args: CreateActionArgs,
416        _originator: Option<&str>,
417    ) -> Result<CreateActionResult, WalletError> {
418        Err(WalletError::NotImplemented("createAction".to_string()))
419    }
420
421    async fn sign_action(
422        &self,
423        _args: SignActionArgs,
424        _originator: Option<&str>,
425    ) -> Result<SignActionResult, WalletError> {
426        Err(WalletError::NotImplemented("signAction".to_string()))
427    }
428
429    async fn abort_action(
430        &self,
431        _args: AbortActionArgs,
432        _originator: Option<&str>,
433    ) -> Result<AbortActionResult, WalletError> {
434        Err(WalletError::NotImplemented("abortAction".to_string()))
435    }
436
437    async fn list_actions(
438        &self,
439        _args: ListActionsArgs,
440        _originator: Option<&str>,
441    ) -> Result<ListActionsResult, WalletError> {
442        Err(WalletError::NotImplemented("listActions".to_string()))
443    }
444
445    async fn internalize_action(
446        &self,
447        _args: InternalizeActionArgs,
448        _originator: Option<&str>,
449    ) -> Result<InternalizeActionResult, WalletError> {
450        Err(WalletError::NotImplemented("internalizeAction".to_string()))
451    }
452
453    // -- Output methods (not supported) --
454
455    async fn list_outputs(
456        &self,
457        _args: ListOutputsArgs,
458        _originator: Option<&str>,
459    ) -> Result<ListOutputsResult, WalletError> {
460        Err(WalletError::NotImplemented("listOutputs".to_string()))
461    }
462
463    async fn relinquish_output(
464        &self,
465        _args: RelinquishOutputArgs,
466        _originator: Option<&str>,
467    ) -> Result<RelinquishOutputResult, WalletError> {
468        Err(WalletError::NotImplemented("relinquishOutput".to_string()))
469    }
470
471    // -- Key/Crypto methods (supported — delegates to ProtoWallet methods) --
472
473    async fn get_public_key(
474        &self,
475        args: GetPublicKeyArgs,
476        _originator: Option<&str>,
477    ) -> Result<GetPublicKeyResult, WalletError> {
478        if args.privileged {
479            return Err(WalletError::NotImplemented(
480                "privileged key access not supported by ProtoWallet".to_string(),
481            ));
482        }
483        let protocol = args.protocol_id.unwrap_or(Protocol {
484            security_level: 0,
485            protocol: String::new(),
486        });
487        let key_id = args.key_id.unwrap_or_default();
488        let counterparty = args.counterparty.unwrap_or(Counterparty {
489            counterparty_type: CounterpartyType::Uninitialized,
490            public_key: None,
491        });
492        let for_self = args.for_self.unwrap_or(false);
493        let pk = self.get_public_key_sync(
494            &protocol,
495            &key_id,
496            &counterparty,
497            for_self,
498            args.identity_key,
499        )?;
500        Ok(GetPublicKeyResult { public_key: pk })
501    }
502
503    async fn reveal_counterparty_key_linkage(
504        &self,
505        args: RevealCounterpartyKeyLinkageArgs,
506        _originator: Option<&str>,
507    ) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> {
508        let counterparty = Counterparty {
509            counterparty_type: CounterpartyType::Other,
510            public_key: Some(args.counterparty),
511        };
512        let result = self.reveal_counterparty_key_linkage_sync(&counterparty, &args.verifier)?;
513        Ok(RevealCounterpartyKeyLinkageResult {
514            prover: result.prover,
515            counterparty: result.counterparty,
516            verifier: result.verifier,
517            revelation_time: result.revelation_time,
518            encrypted_linkage: result.encrypted_linkage,
519            encrypted_linkage_proof: result.encrypted_linkage_proof,
520        })
521    }
522
523    async fn reveal_specific_key_linkage(
524        &self,
525        args: RevealSpecificKeyLinkageArgs,
526        _originator: Option<&str>,
527    ) -> Result<RevealSpecificKeyLinkageResult, WalletError> {
528        let result = self.reveal_specific_key_linkage_sync(
529            &args.counterparty,
530            &args.verifier,
531            &args.protocol_id,
532            &args.key_id,
533        )?;
534        Ok(RevealSpecificKeyLinkageResult {
535            encrypted_linkage: result.encrypted_linkage,
536            encrypted_linkage_proof: result.encrypted_linkage_proof,
537            prover: result.prover,
538            verifier: result.verifier,
539            counterparty: result.counterparty,
540            protocol_id: result.protocol.clone(),
541            key_id: result.key_id.clone(),
542            proof_type: result.proof_type,
543        })
544    }
545
546    async fn encrypt(
547        &self,
548        args: EncryptArgs,
549        _originator: Option<&str>,
550    ) -> Result<EncryptResult, WalletError> {
551        let ciphertext = self.encrypt_sync(
552            &args.plaintext,
553            &args.protocol_id,
554            &args.key_id,
555            &args.counterparty,
556        )?;
557        Ok(EncryptResult { ciphertext })
558    }
559
560    async fn decrypt(
561        &self,
562        args: DecryptArgs,
563        _originator: Option<&str>,
564    ) -> Result<DecryptResult, WalletError> {
565        let plaintext = self.decrypt_sync(
566            &args.ciphertext,
567            &args.protocol_id,
568            &args.key_id,
569            &args.counterparty,
570        )?;
571        Ok(DecryptResult { plaintext })
572    }
573
574    async fn create_hmac(
575        &self,
576        args: CreateHmacArgs,
577        _originator: Option<&str>,
578    ) -> Result<CreateHmacResult, WalletError> {
579        let hmac = self.create_hmac_sync(
580            &args.data,
581            &args.protocol_id,
582            &args.key_id,
583            &args.counterparty,
584        )?;
585        Ok(CreateHmacResult { hmac })
586    }
587
588    async fn verify_hmac(
589        &self,
590        args: VerifyHmacArgs,
591        _originator: Option<&str>,
592    ) -> Result<VerifyHmacResult, WalletError> {
593        let valid = self.verify_hmac_sync(
594            &args.data,
595            &args.hmac,
596            &args.protocol_id,
597            &args.key_id,
598            &args.counterparty,
599        )?;
600        Ok(VerifyHmacResult { valid })
601    }
602
603    async fn create_signature(
604        &self,
605        args: CreateSignatureArgs,
606        _originator: Option<&str>,
607    ) -> Result<CreateSignatureResult, WalletError> {
608        let signature = self.create_signature_sync(
609            &args.data,
610            &args.protocol_id,
611            &args.key_id,
612            &args.counterparty,
613        )?;
614        Ok(CreateSignatureResult { signature })
615    }
616
617    async fn verify_signature(
618        &self,
619        args: VerifySignatureArgs,
620        _originator: Option<&str>,
621    ) -> Result<VerifySignatureResult, WalletError> {
622        let for_self = args.for_self.unwrap_or(false);
623        let valid = self.verify_signature_sync(
624            &args.data,
625            &args.signature,
626            &args.protocol_id,
627            &args.key_id,
628            &args.counterparty,
629            for_self,
630        )?;
631        Ok(VerifySignatureResult { valid })
632    }
633
634    // -- Certificate methods (not supported) --
635
636    async fn acquire_certificate(
637        &self,
638        _args: AcquireCertificateArgs,
639        _originator: Option<&str>,
640    ) -> Result<Certificate, WalletError> {
641        Err(WalletError::NotImplemented(
642            "acquireCertificate".to_string(),
643        ))
644    }
645
646    async fn list_certificates(
647        &self,
648        _args: ListCertificatesArgs,
649        _originator: Option<&str>,
650    ) -> Result<ListCertificatesResult, WalletError> {
651        Err(WalletError::NotImplemented("listCertificates".to_string()))
652    }
653
654    async fn prove_certificate(
655        &self,
656        _args: ProveCertificateArgs,
657        _originator: Option<&str>,
658    ) -> Result<ProveCertificateResult, WalletError> {
659        Err(WalletError::NotImplemented("proveCertificate".to_string()))
660    }
661
662    async fn relinquish_certificate(
663        &self,
664        _args: RelinquishCertificateArgs,
665        _originator: Option<&str>,
666    ) -> Result<RelinquishCertificateResult, WalletError> {
667        Err(WalletError::NotImplemented(
668            "relinquishCertificate".to_string(),
669        ))
670    }
671
672    // -- Discovery methods (not supported) --
673
674    async fn discover_by_identity_key(
675        &self,
676        _args: DiscoverByIdentityKeyArgs,
677        _originator: Option<&str>,
678    ) -> Result<DiscoverCertificatesResult, WalletError> {
679        Err(WalletError::NotImplemented(
680            "discoverByIdentityKey".to_string(),
681        ))
682    }
683
684    async fn discover_by_attributes(
685        &self,
686        _args: DiscoverByAttributesArgs,
687        _originator: Option<&str>,
688    ) -> Result<DiscoverCertificatesResult, WalletError> {
689        Err(WalletError::NotImplemented(
690            "discoverByAttributes".to_string(),
691        ))
692    }
693
694    // -- Auth/Info methods (not supported) --
695
696    async fn is_authenticated(
697        &self,
698        _originator: Option<&str>,
699    ) -> Result<AuthenticatedResult, WalletError> {
700        Err(WalletError::NotImplemented("isAuthenticated".to_string()))
701    }
702
703    async fn wait_for_authentication(
704        &self,
705        _originator: Option<&str>,
706    ) -> Result<AuthenticatedResult, WalletError> {
707        Err(WalletError::NotImplemented(
708            "waitForAuthentication".to_string(),
709        ))
710    }
711
712    async fn get_height(&self, _originator: Option<&str>) -> Result<GetHeightResult, WalletError> {
713        Err(WalletError::NotImplemented("getHeight".to_string()))
714    }
715
716    async fn get_header_for_height(
717        &self,
718        _args: GetHeaderArgs,
719        _originator: Option<&str>,
720    ) -> Result<GetHeaderResult, WalletError> {
721        Err(WalletError::NotImplemented(
722            "getHeaderForHeight".to_string(),
723        ))
724    }
725
726    async fn get_network(
727        &self,
728        _originator: Option<&str>,
729    ) -> Result<GetNetworkResult, WalletError> {
730        Err(WalletError::NotImplemented("getNetwork".to_string()))
731    }
732
733    async fn get_version(
734        &self,
735        _originator: Option<&str>,
736    ) -> Result<GetVersionResult, WalletError> {
737        Err(WalletError::NotImplemented("getVersion".to_string()))
738    }
739}
740
741/// Constant-time byte comparison to prevent timing attacks.
742fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
743    if a.len() != b.len() {
744        return false;
745    }
746    let mut diff: u8 = 0;
747    for (x, y) in a.iter().zip(b.iter()) {
748        diff |= x ^ y;
749    }
750    diff == 0
751}
752
753/// Returns a UTC timestamp string suitable for use as a key ID.
754fn current_utc_timestamp() -> String {
755    // Use a simple epoch-based timestamp to avoid external dependencies.
756    // Format: seconds since epoch as a string.
757    use std::time::{SystemTime, UNIX_EPOCH};
758    let now = SystemTime::now()
759        .duration_since(UNIX_EPOCH)
760        .unwrap_or_default();
761    format!("{}", now.as_secs())
762}
763
764#[cfg(test)]
765mod tests {
766    use super::*;
767    use crate::wallet::types::{BooleanDefaultFalse, BooleanDefaultTrue};
768
769    fn test_protocol() -> Protocol {
770        Protocol {
771            security_level: 2,
772            protocol: "test proto wallet".to_string(),
773        }
774    }
775
776    fn self_counterparty() -> Counterparty {
777        Counterparty {
778            counterparty_type: CounterpartyType::Self_,
779            public_key: None,
780        }
781    }
782
783    fn test_private_key() -> PrivateKey {
784        PrivateKey::from_hex("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
785            .unwrap()
786    }
787
788    #[test]
789    fn test_new_creates_wallet_with_correct_identity_key() {
790        let pk = test_private_key();
791        let expected_pub = pk.to_public_key();
792        let wallet = ProtoWallet::new(pk);
793        let identity = wallet
794            .get_public_key_sync(&test_protocol(), "1", &self_counterparty(), false, true)
795            .unwrap();
796        assert_eq!(identity.to_der_hex(), expected_pub.to_der_hex());
797    }
798
799    #[test]
800    fn test_get_public_key_identity_key_true() {
801        let pk = test_private_key();
802        let expected = pk.to_public_key().to_der_hex();
803        let wallet = ProtoWallet::new(pk);
804        let result = wallet
805            .get_public_key_sync(&test_protocol(), "1", &self_counterparty(), false, true)
806            .unwrap();
807        assert_eq!(result.to_der_hex(), expected);
808    }
809
810    #[test]
811    fn test_get_public_key_derived() {
812        let wallet = ProtoWallet::new(test_private_key());
813        let protocol = test_protocol();
814        let pub1 = wallet
815            .get_public_key_sync(&protocol, "key1", &self_counterparty(), true, false)
816            .unwrap();
817        let pub2 = wallet
818            .get_public_key_sync(&protocol, "key2", &self_counterparty(), true, false)
819            .unwrap();
820        // Different key IDs should produce different derived keys
821        assert_ne!(pub1.to_der_hex(), pub2.to_der_hex());
822    }
823
824    #[test]
825    fn test_create_and_verify_signature_roundtrip() {
826        let wallet = ProtoWallet::new(test_private_key());
827        let protocol = test_protocol();
828        let counterparty = self_counterparty();
829        let data = b"hello world signature test";
830
831        let sig = wallet
832            .create_signature_sync(data, &protocol, "sig1", &counterparty)
833            .unwrap();
834        assert!(!sig.is_empty());
835
836        let valid = wallet
837            .verify_signature_sync(data, &sig, &protocol, "sig1", &counterparty, true)
838            .unwrap();
839        assert!(valid, "signature should verify");
840    }
841
842    #[test]
843    fn test_verify_signature_rejects_wrong_data() {
844        let wallet = ProtoWallet::new(test_private_key());
845        let protocol = test_protocol();
846        let counterparty = self_counterparty();
847
848        let sig = wallet
849            .create_signature_sync(b"correct data", &protocol, "sig2", &counterparty)
850            .unwrap();
851        let valid = wallet
852            .verify_signature_sync(b"wrong data", &sig, &protocol, "sig2", &counterparty, true)
853            .unwrap();
854        assert!(!valid, "signature should not verify for wrong data");
855    }
856
857    #[test]
858    fn test_encrypt_decrypt_roundtrip() {
859        let wallet = ProtoWallet::new(test_private_key());
860        let protocol = test_protocol();
861        let counterparty = self_counterparty();
862        let plaintext = b"secret message for encryption";
863
864        let ciphertext = wallet
865            .encrypt_sync(plaintext, &protocol, "enc1", &counterparty)
866            .unwrap();
867        assert_ne!(ciphertext.as_slice(), plaintext);
868
869        let decrypted = wallet
870            .decrypt_sync(&ciphertext, &protocol, "enc1", &counterparty)
871            .unwrap();
872        assert_eq!(decrypted, plaintext);
873    }
874
875    #[test]
876    fn test_encrypt_decrypt_empty_plaintext() {
877        let wallet = ProtoWallet::new(test_private_key());
878        let protocol = test_protocol();
879        let counterparty = self_counterparty();
880
881        let ciphertext = wallet
882            .encrypt_sync(b"", &protocol, "enc2", &counterparty)
883            .unwrap();
884        let decrypted = wallet
885            .decrypt_sync(&ciphertext, &protocol, "enc2", &counterparty)
886            .unwrap();
887        assert!(decrypted.is_empty());
888    }
889
890    #[test]
891    fn test_create_and_verify_hmac_roundtrip() {
892        let wallet = ProtoWallet::new(test_private_key());
893        let protocol = test_protocol();
894        let counterparty = self_counterparty();
895        let data = b"hmac test data";
896
897        let hmac = wallet
898            .create_hmac_sync(data, &protocol, "hmac1", &counterparty)
899            .unwrap();
900        assert_eq!(hmac.len(), 32);
901
902        let valid = wallet
903            .verify_hmac_sync(data, &hmac, &protocol, "hmac1", &counterparty)
904            .unwrap();
905        assert!(valid, "HMAC should verify");
906    }
907
908    #[test]
909    fn test_verify_hmac_rejects_wrong_data() {
910        let wallet = ProtoWallet::new(test_private_key());
911        let protocol = test_protocol();
912        let counterparty = self_counterparty();
913
914        let hmac = wallet
915            .create_hmac_sync(b"correct", &protocol, "hmac2", &counterparty)
916            .unwrap();
917        let valid = wallet
918            .verify_hmac_sync(b"wrong", &hmac, &protocol, "hmac2", &counterparty)
919            .unwrap();
920        assert!(!valid, "HMAC should not verify for wrong data");
921    }
922
923    #[test]
924    fn test_hmac_deterministic() {
925        let wallet = ProtoWallet::new(test_private_key());
926        let protocol = test_protocol();
927        let counterparty = self_counterparty();
928        let data = b"deterministic hmac";
929
930        let hmac1 = wallet
931            .create_hmac_sync(data, &protocol, "hmac3", &counterparty)
932            .unwrap();
933        let hmac2 = wallet
934            .create_hmac_sync(data, &protocol, "hmac3", &counterparty)
935            .unwrap();
936        assert_eq!(hmac1, hmac2);
937    }
938
939    #[test]
940    fn test_anyone_wallet_encrypt_decrypt() {
941        let anyone = ProtoWallet::anyone();
942        let other_key = test_private_key();
943        let other_pub = other_key.to_public_key();
944
945        let counterparty = Counterparty {
946            counterparty_type: CounterpartyType::Other,
947            public_key: Some(other_pub),
948        };
949        let protocol = test_protocol();
950        let plaintext = b"message from anyone";
951
952        let ciphertext = anyone
953            .encrypt_sync(plaintext, &protocol, "anon1", &counterparty)
954            .unwrap();
955        let decrypted = anyone
956            .decrypt_sync(&ciphertext, &protocol, "anon1", &counterparty)
957            .unwrap();
958        assert_eq!(decrypted, plaintext);
959    }
960
961    #[test]
962    fn test_uninitialized_counterparty_defaults_to_self_for_encrypt() {
963        let wallet = ProtoWallet::new(test_private_key());
964        let protocol = test_protocol();
965        let uninit = Counterparty {
966            counterparty_type: CounterpartyType::Uninitialized,
967            public_key: None,
968        };
969        let self_cp = self_counterparty();
970
971        let ct_uninit = wallet.encrypt_sync(b"test", &protocol, "def1", &uninit).unwrap();
972        // Both should decrypt with Self_ counterparty
973        let decrypted = wallet
974            .decrypt_sync(&ct_uninit, &protocol, "def1", &self_cp)
975            .unwrap();
976        assert_eq!(decrypted, b"test");
977    }
978
979    #[test]
980    fn test_reveal_specific_key_linkage() {
981        let wallet_a = ProtoWallet::new(test_private_key());
982        let verifier_key = PrivateKey::from_hex("ff").unwrap();
983        let verifier_pub = verifier_key.to_public_key();
984
985        let counterparty_key = PrivateKey::from_hex("bb").unwrap();
986        let counterparty_pub = counterparty_key.to_public_key();
987
988        let counterparty = Counterparty {
989            counterparty_type: CounterpartyType::Other,
990            public_key: Some(counterparty_pub),
991        };
992
993        let protocol = test_protocol();
994        let result = wallet_a
995            .reveal_specific_key_linkage_sync(&counterparty, &verifier_pub, &protocol, "link1")
996            .unwrap();
997
998        assert!(!result.encrypted_linkage.is_empty());
999        assert!(!result.encrypted_linkage_proof.is_empty());
1000        assert_eq!(result.proof_type, 0);
1001        assert_eq!(result.key_id, "link1");
1002    }
1003
1004    #[test]
1005    fn test_reveal_counterparty_key_linkage() {
1006        let wallet = ProtoWallet::new(test_private_key());
1007        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1008        let verifier_pub = verifier_key.to_public_key();
1009
1010        let counterparty_key = PrivateKey::from_hex("cc").unwrap();
1011        let counterparty_pub = counterparty_key.to_public_key();
1012
1013        let counterparty = Counterparty {
1014            counterparty_type: CounterpartyType::Other,
1015            public_key: Some(counterparty_pub.clone()),
1016        };
1017
1018        let result = wallet
1019            .reveal_counterparty_key_linkage_sync(&counterparty, &verifier_pub)
1020            .unwrap();
1021
1022        assert!(!result.encrypted_linkage.is_empty());
1023        assert!(!result.encrypted_linkage_proof.is_empty());
1024        assert_eq!(
1025            result.counterparty.to_der_hex(),
1026            counterparty_pub.to_der_hex()
1027        );
1028        assert_eq!(result.verifier.to_der_hex(), verifier_pub.to_der_hex());
1029        assert!(!result.revelation_time.is_empty());
1030    }
1031
1032    // -----------------------------------------------------------------------
1033    // WalletInterface trait tests
1034    // -----------------------------------------------------------------------
1035
1036    /// Helper: call WalletInterface method through trait to verify dispatch.
1037    async fn get_pub_key_via_trait<W: WalletInterface + ?Sized>(
1038        w: &W,
1039        args: GetPublicKeyArgs,
1040    ) -> Result<GetPublicKeyResult, WalletError> {
1041        w.get_public_key(args, None).await
1042    }
1043
1044    #[tokio::test]
1045    async fn test_wallet_interface_get_public_key_identity() {
1046        let pk = test_private_key();
1047        let expected = pk.to_public_key().to_der_hex();
1048        let wallet = ProtoWallet::new(pk);
1049
1050        let result = get_pub_key_via_trait(
1051            &wallet,
1052            GetPublicKeyArgs {
1053                identity_key: true,
1054                protocol_id: None,
1055                key_id: None,
1056                counterparty: None,
1057                privileged: false,
1058                privileged_reason: None,
1059                for_self: None,
1060                seek_permission: None,
1061            },
1062        )
1063        .await
1064        .unwrap();
1065
1066        assert_eq!(result.public_key.to_der_hex(), expected);
1067    }
1068
1069    #[tokio::test]
1070    async fn test_wallet_interface_get_public_key_derived() {
1071        let wallet = ProtoWallet::new(test_private_key());
1072
1073        let result = get_pub_key_via_trait(
1074            &wallet,
1075            GetPublicKeyArgs {
1076                identity_key: false,
1077                protocol_id: Some(test_protocol()),
1078                key_id: Some("derived1".to_string()),
1079                counterparty: Some(self_counterparty()),
1080                privileged: false,
1081                privileged_reason: None,
1082                for_self: Some(true),
1083                seek_permission: None,
1084            },
1085        )
1086        .await
1087        .unwrap();
1088
1089        // Should match the direct method call
1090        let direct = wallet
1091            .get_public_key_sync(
1092                &test_protocol(),
1093                "derived1",
1094                &self_counterparty(),
1095                true,
1096                false,
1097            )
1098            .unwrap();
1099        assert_eq!(result.public_key.to_der_hex(), direct.to_der_hex());
1100    }
1101
1102    #[tokio::test]
1103    async fn test_wallet_interface_privileged_rejected() {
1104        let wallet = ProtoWallet::new(test_private_key());
1105        let err = WalletInterface::get_public_key(
1106            &wallet,
1107            GetPublicKeyArgs {
1108                identity_key: true,
1109                protocol_id: None,
1110                key_id: None,
1111                counterparty: None,
1112                privileged: true,
1113                privileged_reason: Some("test".to_string()),
1114                for_self: None,
1115                seek_permission: None,
1116            },
1117            None,
1118        )
1119        .await;
1120
1121        assert!(err.is_err());
1122        let msg = format!("{}", err.unwrap_err());
1123        assert!(msg.contains("not implemented"), "got: {}", msg);
1124    }
1125
1126    #[tokio::test]
1127    async fn test_wallet_interface_create_verify_signature() {
1128        let wallet = ProtoWallet::new(test_private_key());
1129        let data = b"test data for wallet interface sig".to_vec();
1130
1131        let sig_result = WalletInterface::create_signature(
1132            &wallet,
1133            CreateSignatureArgs {
1134                protocol_id: test_protocol(),
1135                key_id: "wsig1".to_string(),
1136                counterparty: self_counterparty(),
1137                data: data.clone(),
1138                privileged: false,
1139                privileged_reason: None,
1140                seek_permission: None,
1141            },
1142            None,
1143        )
1144        .await
1145        .unwrap();
1146
1147        let verify_result = WalletInterface::verify_signature(
1148            &wallet,
1149            VerifySignatureArgs {
1150                protocol_id: test_protocol(),
1151                key_id: "wsig1".to_string(),
1152                counterparty: self_counterparty(),
1153                data,
1154                signature: sig_result.signature,
1155                for_self: Some(true),
1156                privileged: false,
1157                privileged_reason: None,
1158                seek_permission: None,
1159            },
1160            None,
1161        )
1162        .await
1163        .unwrap();
1164
1165        assert!(verify_result.valid);
1166    }
1167
1168    #[tokio::test]
1169    async fn test_wallet_interface_encrypt_decrypt() {
1170        let wallet = ProtoWallet::new(test_private_key());
1171        let plaintext = b"wallet interface encrypt test".to_vec();
1172
1173        let enc = WalletInterface::encrypt(
1174            &wallet,
1175            EncryptArgs {
1176                protocol_id: test_protocol(),
1177                key_id: "wenc1".to_string(),
1178                counterparty: self_counterparty(),
1179                plaintext: plaintext.clone(),
1180                privileged: false,
1181                privileged_reason: None,
1182                seek_permission: None,
1183            },
1184            None,
1185        )
1186        .await
1187        .unwrap();
1188
1189        let dec = WalletInterface::decrypt(
1190            &wallet,
1191            DecryptArgs {
1192                protocol_id: test_protocol(),
1193                key_id: "wenc1".to_string(),
1194                counterparty: self_counterparty(),
1195                ciphertext: enc.ciphertext,
1196                privileged: false,
1197                privileged_reason: None,
1198                seek_permission: None,
1199            },
1200            None,
1201        )
1202        .await
1203        .unwrap();
1204
1205        assert_eq!(dec.plaintext, plaintext);
1206    }
1207
1208    #[tokio::test]
1209    async fn test_wallet_interface_hmac_roundtrip() {
1210        let wallet = ProtoWallet::new(test_private_key());
1211        let data = b"wallet interface hmac test".to_vec();
1212
1213        let hmac_result = WalletInterface::create_hmac(
1214            &wallet,
1215            CreateHmacArgs {
1216                protocol_id: test_protocol(),
1217                key_id: "whmac1".to_string(),
1218                counterparty: self_counterparty(),
1219                data: data.clone(),
1220                privileged: false,
1221                privileged_reason: None,
1222                seek_permission: None,
1223            },
1224            None,
1225        )
1226        .await
1227        .unwrap();
1228
1229        assert_eq!(hmac_result.hmac.len(), 32);
1230
1231        let verify = WalletInterface::verify_hmac(
1232            &wallet,
1233            VerifyHmacArgs {
1234                protocol_id: test_protocol(),
1235                key_id: "whmac1".to_string(),
1236                counterparty: self_counterparty(),
1237                data,
1238                hmac: hmac_result.hmac,
1239                privileged: false,
1240                privileged_reason: None,
1241                seek_permission: None,
1242            },
1243            None,
1244        )
1245        .await
1246        .unwrap();
1247
1248        assert!(verify.valid);
1249    }
1250
1251    #[tokio::test]
1252    async fn test_wallet_interface_unsupported_methods_return_not_implemented() {
1253        use crate::wallet::interfaces::*;
1254        let wallet = ProtoWallet::new(test_private_key());
1255
1256        // Each unsupported method should return NotImplemented, matching TS SDK
1257        // CompletedProtoWallet which throws "not implemented" for these.
1258        let err = WalletInterface::is_authenticated(&wallet, None).await;
1259        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1260
1261        let err = WalletInterface::wait_for_authentication(&wallet, None).await;
1262        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1263
1264        let err = WalletInterface::get_network(&wallet, None).await;
1265        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1266
1267        let err = WalletInterface::get_version(&wallet, None).await;
1268        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1269
1270        let err = WalletInterface::get_height(&wallet, None).await;
1271        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1272
1273        let err =
1274            WalletInterface::get_header_for_height(&wallet, GetHeaderArgs { height: 0 }, None)
1275                .await;
1276        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1277
1278        let err = WalletInterface::list_outputs(
1279            &wallet,
1280            ListOutputsArgs {
1281                basket: "test".to_string(),
1282                tags: vec![],
1283                tag_query_mode: None,
1284                include: None,
1285                include_custom_instructions: BooleanDefaultFalse(None),
1286                include_tags: BooleanDefaultFalse(None),
1287                include_labels: BooleanDefaultFalse(None),
1288                limit: Some(10),
1289                offset: None,
1290                seek_permission: BooleanDefaultTrue(None),
1291            },
1292            None,
1293        )
1294        .await;
1295        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1296    }
1297
1298    #[tokio::test]
1299    async fn test_wallet_interface_reveal_counterparty_key_linkage() {
1300        let wallet = ProtoWallet::new(test_private_key());
1301        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1302        let counterparty_key = PrivateKey::from_hex("cc").unwrap();
1303
1304        let result = WalletInterface::reveal_counterparty_key_linkage(
1305            &wallet,
1306            RevealCounterpartyKeyLinkageArgs {
1307                counterparty: counterparty_key.to_public_key(),
1308                verifier: verifier_key.to_public_key(),
1309                privileged: None,
1310                privileged_reason: None,
1311            },
1312            None,
1313        )
1314        .await
1315        .unwrap();
1316
1317        assert!(!result.encrypted_linkage.is_empty());
1318        assert!(!result.encrypted_linkage_proof.is_empty());
1319        assert_eq!(
1320            result.counterparty.to_der_hex(),
1321            counterparty_key.to_public_key().to_der_hex()
1322        );
1323        assert!(!result.revelation_time.is_empty());
1324    }
1325
1326    #[tokio::test]
1327    async fn test_wallet_interface_reveal_specific_key_linkage() {
1328        let wallet = ProtoWallet::new(test_private_key());
1329        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1330        let counterparty_key = PrivateKey::from_hex("bb").unwrap();
1331
1332        let result = WalletInterface::reveal_specific_key_linkage(
1333            &wallet,
1334            RevealSpecificKeyLinkageArgs {
1335                counterparty: Counterparty {
1336                    counterparty_type: CounterpartyType::Other,
1337                    public_key: Some(counterparty_key.to_public_key()),
1338                },
1339                verifier: verifier_key.to_public_key(),
1340                protocol_id: test_protocol(),
1341                key_id: "wlink1".to_string(),
1342                privileged: None,
1343                privileged_reason: None,
1344            },
1345            None,
1346        )
1347        .await
1348        .unwrap();
1349
1350        assert!(!result.encrypted_linkage.is_empty());
1351        assert_eq!(result.proof_type, 0);
1352        assert_eq!(result.key_id, "wlink1");
1353    }
1354}