tor_hscrypto/ope.rs
1//! A simple order-preserving encryption function.
2//!
3//! This function is used to generate revision counters for onion service
4//! descriptors. It is not suitable for other purposes.
5//!
6//! The scheme here is the one described in the specifications
7//! as "Encrypted Time In Period".
8//!
9//! It is loosely based on the scheme first described in
10//! G. Bebek. "Anti-tamper database research: Inference control
11//! techniques."" Technical Report EECS 433 Final Report, Case
12//! Western Reserve University, November 2002.
13
14// NOTE:
15//
16// We use the same algorithm here as in C tor, not because it is a great
17// algorithm, but because there has been a community of onion service operators
18// who try to achieve load balancing by running multiple onion services with the
19// same keys, and letting them "race" to publish at the HsDirs. This only
20// works if all the onion service instances produce the same revision counters.
21
22use cipher::{KeyIvInit as _, StreamCipher as _};
23use digest::Digest as _;
24
25use tor_llcrypto::{cipher::aes::Aes256Ctr, d::Sha3_256};
26use zeroize::Zeroizing;
27
28/// Key for a simple order-preserving encryption on the offset from the start of an SRV protocol
29/// run.
30///
31/// The algorithm here is chosen to be the same as used in the C tor
32/// implementation.
33#[derive(Clone, Debug)]
34pub struct AesOpeKey {
35 /// The key for our counter-mode cipher.
36 key: Zeroizing<[u8; 32]>,
37}
38
39/// A prefix used when deriving an AES key for this purpose.
40const DERIVATION_PREFIX: &[u8] = b"rev-counter-generation";
41
42impl AesOpeKey {
43 /// Construct a new [`AesOpeKey`] from a given secret.
44 ///
45 /// The secret should be unpredictable by an adversary.
46 pub fn from_secret(secret: &[u8]) -> Self {
47 let mut h = Sha3_256::new();
48 h.update(DERIVATION_PREFIX);
49 h.update(secret);
50 let key = Zeroizing::new(h.finalize().into());
51 AesOpeKey { key }
52 }
53
54 /// Encrypt `offset` to a 64-bit number.
55 ///
56 /// (We do not implement a decryption operation.)
57 ///
58 /// # Limitations
59 ///
60 /// Like all order-preserving encryption, this scheme leaks information by
61 /// its nature. It also leaks more information than necessary: (the
62 /// adversary can get a rough estimate for our input by dividing the output
63 /// by 0x8001). The only security property that this algorithm tries to
64 /// provide is that it prevents an adversary from inferring our clock skew.
65 ///
66 /// This algorithm is also not very efficient in its implementation.
67 /// We expect that the result will become unacceptable if the time period is
68 /// ever larger than a few days.
69 pub fn encrypt(&self, offset: SrvPeriodOffset) -> u64 {
70 // We add "1" here per the spec, since the encryption of 0 is 0.
71 self.encrypt_inner(offset.0.saturating_add(1))
72 }
73
74 /// Implementation for the order-preserving encryption algorithm:
75 ///
76 /// For security, requires that `n` is at least 1.
77 fn encrypt_inner(&self, n: u32) -> u64 {
78 let iv = [0; 16].into();
79 let mut ctr = Aes256Ctr::new((&*self.key).into(), &iv);
80
81 /// Number of u16s to create at once.
82 const BUF_LEN: usize = 8 * 1024;
83 /// Number of bytes in a u16
84 const STEP: usize = 2;
85
86 // We start our accumulator at `n` because we want every increase in the
87 // input to increase our output by at least 1, but it is otherwise
88 // possible for one of our randomly generated u16s to be 0x0000.
89 let mut result = u64::from(n);
90 let mut n = n as usize;
91 let mut buf = [0_u8; BUF_LEN * STEP];
92 while n >= BUF_LEN {
93 buf.fill(0);
94 ctr.apply_keystream(&mut buf[..]);
95 result += add_slice_as_le_u16(&buf[..]);
96 n -= BUF_LEN;
97 }
98 if n > 0 {
99 buf.fill(0);
100 ctr.apply_keystream(&mut buf[..n * STEP]);
101 result += add_slice_as_le_u16(&buf[..n * STEP]);
102 }
103 result
104 }
105}
106
107/// An opaque offset within an SRV period.
108///
109/// Used by onion services to compute a HsDesc revision counter.
110#[derive(Copy, Clone, Debug, PartialEq, derive_more::From)]
111pub struct SrvPeriodOffset(
112 // An offset, in seconds.
113 pub(crate) u32,
114);
115
116/// Treating `slice` as a sequence of little-endian 2-byte words,
117/// add them into a u64.
118///
119/// # Panics
120///
121/// Panics if slice is not even in size.
122fn add_slice_as_le_u16(slice: &[u8]) -> u64 {
123 assert_eq!(slice.len() % 2, 0);
124 let s = slice
125 .chunks_exact(2)
126 .map(|bytepair| {
127 let a: [u8; 2] = bytepair.try_into().expect("chunk was not of size 2!");
128 u64::from(u16::from_le_bytes(a))
129 })
130 .sum();
131 s
132}
133
134#[cfg(test)]
135mod test {
136 use super::*;
137 use hex_literal::hex;
138
139 #[test]
140 fn add_slice() {
141 assert_eq!(6, add_slice_as_le_u16(&[1, 0, 2, 0, 3, 0]));
142 assert_eq!(0x600, add_slice_as_le_u16(&[0, 1, 0, 2, 0, 3]));
143 assert_eq!(
144 419477,
145 add_slice_as_le_u16(b"This is a string of moderate length!")
146 );
147 }
148
149 #[test]
150 fn test_vec() {
151 let key = hex!("19e05891d55232c08c2cad91d612fdb9cbd6691949a0742434a76c80bc6992fe");
152 let key = AesOpeKey { key: key.into() };
153
154 // Test vectors taken from C tor.
155 for (inp, expected) in [
156 (82283, 2695743564_u64),
157 (72661, 2381548866_u64),
158 (72941, 2390408421_u64),
159 (123122, 4036781069_u64),
160 (12154, 402067100_u64),
161 (121574, 3986197593_u64),
162 (11391, 376696838_u64),
163 (65845, 2161801517_u64),
164 (86301, 2828270975_u64),
165 (61284, 2013616892_u64),
166 (70505, 2313368870_u64),
167 (30438, 1001394664_u64),
168 (60150, 1977329668_u64),
169 (114800, 3764946628_u64),
170 (109403, 3585352477_u64),
171 (21893, 721388468_u64),
172 (123569, 4051780471_u64),
173 (95617, 3134921876_u64),
174 (48561, 1597596985_u64),
175 (53334, 1753691710_u64),
176 (92746, 3040874493_u64),
177 (7110, 234966492_u64),
178 (9612, 318326551_u64),
179 (106958, 3506124249_u64),
180 (46889, 1542219146_u64),
181 (87790, 2877361609_u64),
182 (68878, 2260369112_u64),
183 (47917, 1576681737_u64),
184 (121128, 3971553290_u64),
185 (108602, 3559176081_u64),
186 (28217, 929692460_u64),
187 (69498, 2280554161_u64),
188 (63870, 2098322675_u64),
189 (57542, 1891698992_u64),
190 (122148, 4004515805_u64),
191 (46254, 1521227949_u64),
192 (42850, 1408996941_u64),
193 (92661, 3037901517_u64),
194 (57720, 1897369989_u64),
195 ] {
196 assert_eq!(key.encrypt_inner(inp), expected);
197 }
198 }
199}