monero_wallet/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![expect(unexpected_cfgs)]
3#![cfg_attr(monero_oxide_rust_nightly, feature(variant_count))]
4#![doc = include_str!("../README.md")]
5#![cfg_attr(not(feature = "std"), no_std)]
6
7use core::ops::Deref as _;
8use std_shims::vec::Vec;
9
10use zeroize::{Zeroize, Zeroizing};
11
12use monero_oxide::{
13  io::VarInt, ed25519::*, primitives::keccak256, ringct::EncryptedAmount, transaction::Input,
14};
15
16pub use monero_oxide::*;
17
18pub use monero_interface as interface;
19
20pub use monero_address as address;
21
22mod view_pair;
23pub use view_pair::{ViewPairError, ViewPair, GuaranteedViewPair};
24
25/// Structures and functionality for working with transactions' extra fields.
26pub mod extra;
27pub(crate) use extra::{PaymentId, Extra};
28
29pub(crate) mod output;
30pub use output::WalletOutput;
31
32mod scan;
33pub use scan::{Timelocked, ScanError, Scanner, GuaranteedScanner};
34
35mod decoys;
36pub use decoys::OutputWithDecoys;
37
38/// Structs and functionality for sending transactions.
39pub mod send;
40
41#[cfg(test)]
42mod tests;
43
44#[derive(Clone, PartialEq, Eq, Zeroize)]
45struct SharedKeyDerivations {
46  // Hs("view_tag" || 8Ra || o)
47  view_tag: u8,
48  // Hs(uniqueness || 8Ra || o) where uniqueness may be empty
49  shared_key: Scalar,
50}
51
52impl SharedKeyDerivations {
53  // https://gist.github.com/kayabaNerve/8066c13f1fe1573286ba7a2fd79f6100
54  fn uniqueness(inputs: &[Input]) -> [u8; 32] {
55    let mut u = b"uniqueness".to_vec();
56    for input in inputs {
57      match input {
58        // If Gen, this should be the only input, making this loop somewhat pointless
59        // This works and even if there were somehow multiple inputs, it'd be a false negative
60        Input::Gen(height) => {
61          VarInt::write(height, &mut u).expect("write failed but <Vec as io::Write> doesn't fail");
62        }
63        Input::ToKey { key_image, .. } => u.extend(key_image.to_bytes()),
64      }
65    }
66    keccak256(u)
67  }
68
69  #[expect(clippy::needless_pass_by_value)]
70  fn output_derivations(
71    uniqueness: Option<[u8; 32]>,
72    ecdh: Zeroizing<Point>,
73    o: usize,
74  ) -> Zeroizing<SharedKeyDerivations> {
75    // 8Ra
76    let mut output_derivation = Zeroizing::new(
77      Zeroizing::new(Zeroizing::new((*ecdh).into().mul_by_cofactor()).compress().to_bytes())
78        .to_vec(),
79    );
80
81    // || o
82    {
83      let output_derivation: &mut Vec<u8> = output_derivation.as_mut();
84      VarInt::write(&o, output_derivation)
85        .expect("write failed but <Vec as io::Write> doesn't fail");
86    }
87
88    let view_tag = keccak256([b"view_tag".as_slice(), &output_derivation].concat())[0];
89
90    // uniqueness ||
91    let output_derivation = if let Some(uniqueness) = uniqueness {
92      Zeroizing::new([uniqueness.as_slice(), &output_derivation].concat())
93    } else {
94      output_derivation
95    };
96
97    Zeroizing::new(SharedKeyDerivations { view_tag, shared_key: Scalar::hash(&output_derivation) })
98  }
99
100  // H(8Ra || 0x8d)
101  #[expect(clippy::needless_pass_by_value)]
102  fn payment_id_xor(ecdh: Zeroizing<Point>) -> [u8; 8] {
103    // 8Ra
104    let output_derivation = Zeroizing::new(
105      Zeroizing::new(Zeroizing::new((*ecdh).into().mul_by_cofactor()).compress().to_bytes())
106        .to_vec(),
107    );
108
109    let mut payment_id_xor = [0; 8];
110    payment_id_xor
111      .copy_from_slice(&keccak256([output_derivation.as_slice(), &[0x8d]].concat())[.. 8]);
112    payment_id_xor
113  }
114
115  fn commitment_mask(&self) -> Scalar {
116    let mut mask = b"commitment_mask".to_vec();
117    mask.extend(&<[u8; 32]>::from(self.shared_key));
118    let res = Scalar::hash(&mask);
119    mask.zeroize();
120    res
121  }
122
123  fn compact_amount_encryption(&self, amount: u64) -> [u8; 8] {
124    let mut amount_mask = Zeroizing::new(b"amount".to_vec());
125    amount_mask.extend(<[u8; 32]>::from(self.shared_key));
126    let mut amount_mask = keccak256(&amount_mask);
127
128    let mut amount_mask_8 = [0; 8];
129    amount_mask_8.copy_from_slice(&amount_mask[.. 8]);
130    amount_mask.zeroize();
131
132    (amount ^ u64::from_le_bytes(amount_mask_8)).to_le_bytes()
133  }
134
135  fn decrypt(&self, enc_amount: &EncryptedAmount) -> Commitment {
136    match enc_amount {
137      EncryptedAmount::Original { mask, amount } => {
138        let mask_shared_sec_scalar =
139          Zeroizing::new(Scalar::hash(Zeroizing::new(<[u8; 32]>::from(self.shared_key))));
140        let amount_shared_sec_scalar =
141          Zeroizing::new(Scalar::hash(<[u8; 32]>::from(*mask_shared_sec_scalar)));
142
143        let mask =
144          curve25519_dalek::Scalar::from_bytes_mod_order(*mask) - (*mask_shared_sec_scalar).into();
145        let amount_scalar = Zeroizing::new(
146          curve25519_dalek::Scalar::from_bytes_mod_order(*amount) -
147            (*amount_shared_sec_scalar).into(),
148        );
149
150        // d2b from rctTypes.cpp
151        let amount = u64::from_le_bytes(
152          Zeroizing::new(amount_scalar.to_bytes()).deref()[.. 8]
153            .try_into()
154            .expect("32-byte array couldn't have an 8-byte slice taken"),
155        );
156
157        Commitment::new(Scalar::from(mask), amount)
158      }
159      EncryptedAmount::Compact { amount } => Commitment::new(
160        self.commitment_mask(),
161        u64::from_le_bytes(self.compact_amount_encryption(u64::from_le_bytes(*amount))),
162      ),
163    }
164  }
165}