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(
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(
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(
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(
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(
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(
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(
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(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(
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(
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(
286            &linkage_bytes,
287            &linkage_protocol,
288            &revelation_time,
289            &verifier_counterparty,
290        )?;
291        let encrypted_proof = self.encrypt(
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(
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(&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(
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(
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(&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(
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(
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(
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(
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(
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(
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(
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
768    fn test_protocol() -> Protocol {
769        Protocol {
770            security_level: 2,
771            protocol: "test proto wallet".to_string(),
772        }
773    }
774
775    fn self_counterparty() -> Counterparty {
776        Counterparty {
777            counterparty_type: CounterpartyType::Self_,
778            public_key: None,
779        }
780    }
781
782    fn test_private_key() -> PrivateKey {
783        PrivateKey::from_hex("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
784            .unwrap()
785    }
786
787    #[test]
788    fn test_new_creates_wallet_with_correct_identity_key() {
789        let pk = test_private_key();
790        let expected_pub = pk.to_public_key();
791        let wallet = ProtoWallet::new(pk);
792        let identity = wallet
793            .get_public_key(&test_protocol(), "1", &self_counterparty(), false, true)
794            .unwrap();
795        assert_eq!(identity.to_der_hex(), expected_pub.to_der_hex());
796    }
797
798    #[test]
799    fn test_get_public_key_identity_key_true() {
800        let pk = test_private_key();
801        let expected = pk.to_public_key().to_der_hex();
802        let wallet = ProtoWallet::new(pk);
803        let result = wallet
804            .get_public_key(&test_protocol(), "1", &self_counterparty(), false, true)
805            .unwrap();
806        assert_eq!(result.to_der_hex(), expected);
807    }
808
809    #[test]
810    fn test_get_public_key_derived() {
811        let wallet = ProtoWallet::new(test_private_key());
812        let protocol = test_protocol();
813        let pub1 = wallet
814            .get_public_key(&protocol, "key1", &self_counterparty(), true, false)
815            .unwrap();
816        let pub2 = wallet
817            .get_public_key(&protocol, "key2", &self_counterparty(), true, false)
818            .unwrap();
819        // Different key IDs should produce different derived keys
820        assert_ne!(pub1.to_der_hex(), pub2.to_der_hex());
821    }
822
823    #[test]
824    fn test_create_and_verify_signature_roundtrip() {
825        let wallet = ProtoWallet::new(test_private_key());
826        let protocol = test_protocol();
827        let counterparty = self_counterparty();
828        let data = b"hello world signature test";
829
830        let sig = wallet
831            .create_signature(data, &protocol, "sig1", &counterparty)
832            .unwrap();
833        assert!(!sig.is_empty());
834
835        let valid = wallet
836            .verify_signature(data, &sig, &protocol, "sig1", &counterparty, true)
837            .unwrap();
838        assert!(valid, "signature should verify");
839    }
840
841    #[test]
842    fn test_verify_signature_rejects_wrong_data() {
843        let wallet = ProtoWallet::new(test_private_key());
844        let protocol = test_protocol();
845        let counterparty = self_counterparty();
846
847        let sig = wallet
848            .create_signature(b"correct data", &protocol, "sig2", &counterparty)
849            .unwrap();
850        let valid = wallet
851            .verify_signature(b"wrong data", &sig, &protocol, "sig2", &counterparty, true)
852            .unwrap();
853        assert!(!valid, "signature should not verify for wrong data");
854    }
855
856    #[test]
857    fn test_encrypt_decrypt_roundtrip() {
858        let wallet = ProtoWallet::new(test_private_key());
859        let protocol = test_protocol();
860        let counterparty = self_counterparty();
861        let plaintext = b"secret message for encryption";
862
863        let ciphertext = wallet
864            .encrypt(plaintext, &protocol, "enc1", &counterparty)
865            .unwrap();
866        assert_ne!(ciphertext.as_slice(), plaintext);
867
868        let decrypted = wallet
869            .decrypt(&ciphertext, &protocol, "enc1", &counterparty)
870            .unwrap();
871        assert_eq!(decrypted, plaintext);
872    }
873
874    #[test]
875    fn test_encrypt_decrypt_empty_plaintext() {
876        let wallet = ProtoWallet::new(test_private_key());
877        let protocol = test_protocol();
878        let counterparty = self_counterparty();
879
880        let ciphertext = wallet
881            .encrypt(b"", &protocol, "enc2", &counterparty)
882            .unwrap();
883        let decrypted = wallet
884            .decrypt(&ciphertext, &protocol, "enc2", &counterparty)
885            .unwrap();
886        assert!(decrypted.is_empty());
887    }
888
889    #[test]
890    fn test_create_and_verify_hmac_roundtrip() {
891        let wallet = ProtoWallet::new(test_private_key());
892        let protocol = test_protocol();
893        let counterparty = self_counterparty();
894        let data = b"hmac test data";
895
896        let hmac = wallet
897            .create_hmac(data, &protocol, "hmac1", &counterparty)
898            .unwrap();
899        assert_eq!(hmac.len(), 32);
900
901        let valid = wallet
902            .verify_hmac(data, &hmac, &protocol, "hmac1", &counterparty)
903            .unwrap();
904        assert!(valid, "HMAC should verify");
905    }
906
907    #[test]
908    fn test_verify_hmac_rejects_wrong_data() {
909        let wallet = ProtoWallet::new(test_private_key());
910        let protocol = test_protocol();
911        let counterparty = self_counterparty();
912
913        let hmac = wallet
914            .create_hmac(b"correct", &protocol, "hmac2", &counterparty)
915            .unwrap();
916        let valid = wallet
917            .verify_hmac(b"wrong", &hmac, &protocol, "hmac2", &counterparty)
918            .unwrap();
919        assert!(!valid, "HMAC should not verify for wrong data");
920    }
921
922    #[test]
923    fn test_hmac_deterministic() {
924        let wallet = ProtoWallet::new(test_private_key());
925        let protocol = test_protocol();
926        let counterparty = self_counterparty();
927        let data = b"deterministic hmac";
928
929        let hmac1 = wallet
930            .create_hmac(data, &protocol, "hmac3", &counterparty)
931            .unwrap();
932        let hmac2 = wallet
933            .create_hmac(data, &protocol, "hmac3", &counterparty)
934            .unwrap();
935        assert_eq!(hmac1, hmac2);
936    }
937
938    #[test]
939    fn test_anyone_wallet_encrypt_decrypt() {
940        let anyone = ProtoWallet::anyone();
941        let other_key = test_private_key();
942        let other_pub = other_key.to_public_key();
943
944        let counterparty = Counterparty {
945            counterparty_type: CounterpartyType::Other,
946            public_key: Some(other_pub),
947        };
948        let protocol = test_protocol();
949        let plaintext = b"message from anyone";
950
951        let ciphertext = anyone
952            .encrypt(plaintext, &protocol, "anon1", &counterparty)
953            .unwrap();
954        let decrypted = anyone
955            .decrypt(&ciphertext, &protocol, "anon1", &counterparty)
956            .unwrap();
957        assert_eq!(decrypted, plaintext);
958    }
959
960    #[test]
961    fn test_uninitialized_counterparty_defaults_to_self_for_encrypt() {
962        let wallet = ProtoWallet::new(test_private_key());
963        let protocol = test_protocol();
964        let uninit = Counterparty {
965            counterparty_type: CounterpartyType::Uninitialized,
966            public_key: None,
967        };
968        let self_cp = self_counterparty();
969
970        let ct_uninit = wallet.encrypt(b"test", &protocol, "def1", &uninit).unwrap();
971        // Both should decrypt with Self_ counterparty
972        let decrypted = wallet
973            .decrypt(&ct_uninit, &protocol, "def1", &self_cp)
974            .unwrap();
975        assert_eq!(decrypted, b"test");
976    }
977
978    #[test]
979    fn test_reveal_specific_key_linkage() {
980        let wallet_a = ProtoWallet::new(test_private_key());
981        let verifier_key = PrivateKey::from_hex("ff").unwrap();
982        let verifier_pub = verifier_key.to_public_key();
983
984        let counterparty_key = PrivateKey::from_hex("bb").unwrap();
985        let counterparty_pub = counterparty_key.to_public_key();
986
987        let counterparty = Counterparty {
988            counterparty_type: CounterpartyType::Other,
989            public_key: Some(counterparty_pub),
990        };
991
992        let protocol = test_protocol();
993        let result = wallet_a
994            .reveal_specific_key_linkage(&counterparty, &verifier_pub, &protocol, "link1")
995            .unwrap();
996
997        assert!(!result.encrypted_linkage.is_empty());
998        assert!(!result.encrypted_linkage_proof.is_empty());
999        assert_eq!(result.proof_type, 0);
1000        assert_eq!(result.key_id, "link1");
1001    }
1002
1003    #[test]
1004    fn test_reveal_counterparty_key_linkage() {
1005        let wallet = ProtoWallet::new(test_private_key());
1006        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1007        let verifier_pub = verifier_key.to_public_key();
1008
1009        let counterparty_key = PrivateKey::from_hex("cc").unwrap();
1010        let counterparty_pub = counterparty_key.to_public_key();
1011
1012        let counterparty = Counterparty {
1013            counterparty_type: CounterpartyType::Other,
1014            public_key: Some(counterparty_pub.clone()),
1015        };
1016
1017        let result = wallet
1018            .reveal_counterparty_key_linkage(&counterparty, &verifier_pub)
1019            .unwrap();
1020
1021        assert!(!result.encrypted_linkage.is_empty());
1022        assert!(!result.encrypted_linkage_proof.is_empty());
1023        assert_eq!(
1024            result.counterparty.to_der_hex(),
1025            counterparty_pub.to_der_hex()
1026        );
1027        assert_eq!(result.verifier.to_der_hex(), verifier_pub.to_der_hex());
1028        assert!(!result.revelation_time.is_empty());
1029    }
1030
1031    // -----------------------------------------------------------------------
1032    // WalletInterface trait tests
1033    // -----------------------------------------------------------------------
1034
1035    /// Helper: call WalletInterface method through trait to verify dispatch.
1036    async fn get_pub_key_via_trait<W: WalletInterface>(
1037        w: &W,
1038        args: GetPublicKeyArgs,
1039    ) -> Result<GetPublicKeyResult, WalletError> {
1040        w.get_public_key(args, None).await
1041    }
1042
1043    #[tokio::test]
1044    async fn test_wallet_interface_get_public_key_identity() {
1045        let pk = test_private_key();
1046        let expected = pk.to_public_key().to_der_hex();
1047        let wallet = ProtoWallet::new(pk);
1048
1049        let result = get_pub_key_via_trait(
1050            &wallet,
1051            GetPublicKeyArgs {
1052                identity_key: true,
1053                protocol_id: None,
1054                key_id: None,
1055                counterparty: None,
1056                privileged: false,
1057                privileged_reason: None,
1058                for_self: None,
1059                seek_permission: None,
1060            },
1061        )
1062        .await
1063        .unwrap();
1064
1065        assert_eq!(result.public_key.to_der_hex(), expected);
1066    }
1067
1068    #[tokio::test]
1069    async fn test_wallet_interface_get_public_key_derived() {
1070        let wallet = ProtoWallet::new(test_private_key());
1071
1072        let result = get_pub_key_via_trait(
1073            &wallet,
1074            GetPublicKeyArgs {
1075                identity_key: false,
1076                protocol_id: Some(test_protocol()),
1077                key_id: Some("derived1".to_string()),
1078                counterparty: Some(self_counterparty()),
1079                privileged: false,
1080                privileged_reason: None,
1081                for_self: Some(true),
1082                seek_permission: None,
1083            },
1084        )
1085        .await
1086        .unwrap();
1087
1088        // Should match the direct method call
1089        let direct = wallet
1090            .get_public_key(
1091                &test_protocol(),
1092                "derived1",
1093                &self_counterparty(),
1094                true,
1095                false,
1096            )
1097            .unwrap();
1098        assert_eq!(result.public_key.to_der_hex(), direct.to_der_hex());
1099    }
1100
1101    #[tokio::test]
1102    async fn test_wallet_interface_privileged_rejected() {
1103        let wallet = ProtoWallet::new(test_private_key());
1104        let err = WalletInterface::get_public_key(
1105            &wallet,
1106            GetPublicKeyArgs {
1107                identity_key: true,
1108                protocol_id: None,
1109                key_id: None,
1110                counterparty: None,
1111                privileged: true,
1112                privileged_reason: Some("test".to_string()),
1113                for_self: None,
1114                seek_permission: None,
1115            },
1116            None,
1117        )
1118        .await;
1119
1120        assert!(err.is_err());
1121        let msg = format!("{}", err.unwrap_err());
1122        assert!(msg.contains("not implemented"), "got: {}", msg);
1123    }
1124
1125    #[tokio::test]
1126    async fn test_wallet_interface_create_verify_signature() {
1127        let wallet = ProtoWallet::new(test_private_key());
1128        let data = b"test data for wallet interface sig".to_vec();
1129
1130        let sig_result = WalletInterface::create_signature(
1131            &wallet,
1132            CreateSignatureArgs {
1133                protocol_id: test_protocol(),
1134                key_id: "wsig1".to_string(),
1135                counterparty: self_counterparty(),
1136                data: data.clone(),
1137                privileged: false,
1138                privileged_reason: None,
1139                seek_permission: None,
1140            },
1141            None,
1142        )
1143        .await
1144        .unwrap();
1145
1146        let verify_result = WalletInterface::verify_signature(
1147            &wallet,
1148            VerifySignatureArgs {
1149                protocol_id: test_protocol(),
1150                key_id: "wsig1".to_string(),
1151                counterparty: self_counterparty(),
1152                data,
1153                signature: sig_result.signature,
1154                for_self: Some(true),
1155                privileged: false,
1156                privileged_reason: None,
1157                seek_permission: None,
1158            },
1159            None,
1160        )
1161        .await
1162        .unwrap();
1163
1164        assert!(verify_result.valid);
1165    }
1166
1167    #[tokio::test]
1168    async fn test_wallet_interface_encrypt_decrypt() {
1169        let wallet = ProtoWallet::new(test_private_key());
1170        let plaintext = b"wallet interface encrypt test".to_vec();
1171
1172        let enc = WalletInterface::encrypt(
1173            &wallet,
1174            EncryptArgs {
1175                protocol_id: test_protocol(),
1176                key_id: "wenc1".to_string(),
1177                counterparty: self_counterparty(),
1178                plaintext: plaintext.clone(),
1179                privileged: false,
1180                privileged_reason: None,
1181                seek_permission: None,
1182            },
1183            None,
1184        )
1185        .await
1186        .unwrap();
1187
1188        let dec = WalletInterface::decrypt(
1189            &wallet,
1190            DecryptArgs {
1191                protocol_id: test_protocol(),
1192                key_id: "wenc1".to_string(),
1193                counterparty: self_counterparty(),
1194                ciphertext: enc.ciphertext,
1195                privileged: false,
1196                privileged_reason: None,
1197                seek_permission: None,
1198            },
1199            None,
1200        )
1201        .await
1202        .unwrap();
1203
1204        assert_eq!(dec.plaintext, plaintext);
1205    }
1206
1207    #[tokio::test]
1208    async fn test_wallet_interface_hmac_roundtrip() {
1209        let wallet = ProtoWallet::new(test_private_key());
1210        let data = b"wallet interface hmac test".to_vec();
1211
1212        let hmac_result = WalletInterface::create_hmac(
1213            &wallet,
1214            CreateHmacArgs {
1215                protocol_id: test_protocol(),
1216                key_id: "whmac1".to_string(),
1217                counterparty: self_counterparty(),
1218                data: data.clone(),
1219                privileged: false,
1220                privileged_reason: None,
1221                seek_permission: None,
1222            },
1223            None,
1224        )
1225        .await
1226        .unwrap();
1227
1228        assert_eq!(hmac_result.hmac.len(), 32);
1229
1230        let verify = WalletInterface::verify_hmac(
1231            &wallet,
1232            VerifyHmacArgs {
1233                protocol_id: test_protocol(),
1234                key_id: "whmac1".to_string(),
1235                counterparty: self_counterparty(),
1236                data,
1237                hmac: hmac_result.hmac,
1238                privileged: false,
1239                privileged_reason: None,
1240                seek_permission: None,
1241            },
1242            None,
1243        )
1244        .await
1245        .unwrap();
1246
1247        assert!(verify.valid);
1248    }
1249
1250    #[tokio::test]
1251    async fn test_wallet_interface_unsupported_methods_return_not_implemented() {
1252        use crate::wallet::interfaces::*;
1253        let wallet = ProtoWallet::new(test_private_key());
1254
1255        // Each unsupported method should return NotImplemented, matching TS SDK
1256        // CompletedProtoWallet which throws "not implemented" for these.
1257        let err = WalletInterface::is_authenticated(&wallet, None).await;
1258        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1259
1260        let err = WalletInterface::wait_for_authentication(&wallet, None).await;
1261        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1262
1263        let err = WalletInterface::get_network(&wallet, None).await;
1264        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1265
1266        let err = WalletInterface::get_version(&wallet, None).await;
1267        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1268
1269        let err = WalletInterface::get_height(&wallet, None).await;
1270        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1271
1272        let err =
1273            WalletInterface::get_header_for_height(&wallet, GetHeaderArgs { height: 0 }, None)
1274                .await;
1275        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1276
1277        let err = WalletInterface::list_outputs(
1278            &wallet,
1279            ListOutputsArgs {
1280                basket: "test".to_string(),
1281                tags: vec![],
1282                tag_query_mode: None,
1283                include: None,
1284                include_custom_instructions: None,
1285                include_tags: None,
1286                include_labels: None,
1287                limit: Some(10),
1288                offset: None,
1289                seek_permission: None,
1290            },
1291            None,
1292        )
1293        .await;
1294        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1295    }
1296
1297    #[tokio::test]
1298    async fn test_wallet_interface_reveal_counterparty_key_linkage() {
1299        let wallet = ProtoWallet::new(test_private_key());
1300        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1301        let counterparty_key = PrivateKey::from_hex("cc").unwrap();
1302
1303        let result = WalletInterface::reveal_counterparty_key_linkage(
1304            &wallet,
1305            RevealCounterpartyKeyLinkageArgs {
1306                counterparty: counterparty_key.to_public_key(),
1307                verifier: verifier_key.to_public_key(),
1308                privileged: None,
1309                privileged_reason: None,
1310            },
1311            None,
1312        )
1313        .await
1314        .unwrap();
1315
1316        assert!(!result.encrypted_linkage.is_empty());
1317        assert!(!result.encrypted_linkage_proof.is_empty());
1318        assert_eq!(
1319            result.counterparty.to_der_hex(),
1320            counterparty_key.to_public_key().to_der_hex()
1321        );
1322        assert!(!result.revelation_time.is_empty());
1323    }
1324
1325    #[tokio::test]
1326    async fn test_wallet_interface_reveal_specific_key_linkage() {
1327        let wallet = ProtoWallet::new(test_private_key());
1328        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1329        let counterparty_key = PrivateKey::from_hex("bb").unwrap();
1330
1331        let result = WalletInterface::reveal_specific_key_linkage(
1332            &wallet,
1333            RevealSpecificKeyLinkageArgs {
1334                counterparty: Counterparty {
1335                    counterparty_type: CounterpartyType::Other,
1336                    public_key: Some(counterparty_key.to_public_key()),
1337                },
1338                verifier: verifier_key.to_public_key(),
1339                protocol_id: test_protocol(),
1340                key_id: "wlink1".to_string(),
1341                privileged: None,
1342                privileged_reason: None,
1343            },
1344            None,
1345        )
1346        .await
1347        .unwrap();
1348
1349        assert!(!result.encrypted_linkage.is_empty());
1350        assert_eq!(result.proof_type, 0);
1351        assert_eq!(result.key_id, "wlink1");
1352    }
1353}