monero_primitives/unreduced_scalar.rs
1use core::cmp::Ordering;
2use std_shims::{
3 sync::LazyLock,
4 io::{self, *},
5};
6
7use zeroize::Zeroize;
8
9use curve25519_dalek::scalar::Scalar;
10
11use monero_io::*;
12
13// Precomputed scalars used to recover an incorrectly reduced scalar.
14static PRECOMPUTED_SCALARS: LazyLock<[Scalar; 8]> = LazyLock::new(|| {
15 let mut precomputed_scalars = [Scalar::ONE; 8];
16 for (i, scalar) in precomputed_scalars.iter_mut().enumerate().skip(1) {
17 *scalar = Scalar::from(u8::try_from((i * 2) + 1).unwrap());
18 }
19 precomputed_scalars
20});
21
22/// An unreduced scalar.
23///
24/// While most of modern Monero enforces scalars be reduced, certain legacy parts of the code did
25/// not. These section can generally simply be read as a scalar/reduced into a scalar when the time
26/// comes, yet a couple have non-standard reductions performed.
27///
28/// This struct delays scalar conversions and offers the non-standard reduction.
29#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
30pub struct UnreducedScalar(pub [u8; 32]);
31
32impl UnreducedScalar {
33 /// Write an UnreducedScalar.
34 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
35 w.write_all(&self.0)
36 }
37
38 /// Read an UnreducedScalar.
39 pub fn read<R: Read>(r: &mut R) -> io::Result<UnreducedScalar> {
40 Ok(UnreducedScalar(read_bytes(r)?))
41 }
42
43 fn as_bits(&self) -> [u8; 256] {
44 let mut bits = [0; 256];
45 for (i, bit) in bits.iter_mut().enumerate() {
46 *bit = core::hint::black_box(1 & (self.0[i / 8] >> (i % 8)))
47 }
48
49 bits
50 }
51
52 // Computes the non-adjacent form of this scalar with width 5.
53 //
54 // This matches Monero's `slide` function and intentionally gives incorrect outputs under
55 // certain conditions in order to match Monero.
56 //
57 // This function does not execute in constant time.
58 fn non_adjacent_form(&self) -> [i8; 256] {
59 let bits = self.as_bits();
60 let mut naf = [0i8; 256];
61 for (b, bit) in bits.into_iter().enumerate() {
62 naf[b] = i8::try_from(bit).unwrap();
63 }
64
65 for i in 0 .. 256 {
66 if naf[i] != 0 {
67 // if the bit is a one, work our way up through the window
68 // combining the bits with this bit.
69 for b in 1 .. 6 {
70 if (i + b) >= 256 {
71 // if we are at the length of the array then break out
72 // the loop.
73 break;
74 }
75 // potential_carry - the value of the bit at i+b compared to the bit at i
76 let potential_carry = naf[i + b] << b;
77
78 if potential_carry != 0 {
79 if (naf[i] + potential_carry) <= 15 {
80 // if our current "bit" plus the potential carry is less than 16
81 // add it to our current "bit" and set the potential carry bit to 0.
82 naf[i] += potential_carry;
83 naf[i + b] = 0;
84 } else if (naf[i] - potential_carry) >= -15 {
85 // else if our current "bit" minus the potential carry is more than -16
86 // take it away from our current "bit".
87 // we then work our way up through the bits setting ones to zero, when
88 // we hit the first zero we change it to one then stop, this is to factor
89 // in the minus.
90 naf[i] -= potential_carry;
91 #[allow(clippy::needless_range_loop)]
92 for k in (i + b) .. 256 {
93 if naf[k] == 0 {
94 naf[k] = 1;
95 break;
96 }
97 naf[k] = 0;
98 }
99 } else {
100 break;
101 }
102 }
103 }
104 }
105 }
106
107 naf
108 }
109
110 /// Recover the scalar that an array of bytes was incorrectly interpreted as by Monero's `slide`
111 /// function.
112 ///
113 /// In Borromean range proofs, Monero was not checking that the scalars used were
114 /// reduced. This lead to the scalar stored being interpreted as a different scalar.
115 /// This function recovers that scalar.
116 ///
117 /// See <https://github.com/monero-project/monero/issues/8438> for more info.
118 pub fn recover_monero_slide_scalar(&self) -> Scalar {
119 if self.0[31] & 128 == 0 {
120 // Computing the w-NAF of a number can only give an output with 1 more bit than
121 // the number, so even if the number isn't reduced, the `slide` function will be
122 // correct when the last bit isn't set.
123 return Scalar::from_bytes_mod_order(self.0);
124 }
125
126 let mut recovered = Scalar::ZERO;
127 for &numb in self.non_adjacent_form().iter().rev() {
128 recovered += recovered;
129 match numb.cmp(&0) {
130 Ordering::Greater => recovered += PRECOMPUTED_SCALARS[usize::try_from(numb).unwrap() / 2],
131 Ordering::Less => recovered -= PRECOMPUTED_SCALARS[usize::try_from(-numb).unwrap() / 2],
132 Ordering::Equal => (),
133 }
134 }
135 recovered
136 }
137}