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
25pub 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
38pub mod send;
40
41#[cfg(test)]
42mod tests;
43
44#[derive(Clone, PartialEq, Eq, Zeroize)]
45struct SharedKeyDerivations {
46 view_tag: u8,
48 shared_key: Scalar,
50}
51
52impl SharedKeyDerivations {
53 fn uniqueness(inputs: &[Input]) -> [u8; 32] {
55 let mut u = b"uniqueness".to_vec();
56 for input in inputs {
57 match input {
58 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 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 {
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 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 #[expect(clippy::needless_pass_by_value)]
102 fn payment_id_xor(ecdh: Zeroizing<Point>) -> [u8; 8] {
103 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 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}