1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
|
//! Manager-global identifiers, for things that need to be identified outside
//! the scope of a single RPC connection.
//!
//! We expect to use this code to identify `TorClient`s and similar objects that
//! can be passed as the target of a SOCKS request. Since the SOCKS request is
//! not part of the RPC session, we need a way for it to refer to these objects.
use tor_bytes::Reader;
use tor_llcrypto::util::ct::CtByteArray;
use tor_rpcbase::{LookupError, ObjectId};
use zeroize::Zeroizing;
use crate::{connection::ConnectionId, objmap::GenIdx};
/// A [RpcMgr](crate::RpcMgr)-scoped identifier for an RPC object.
///
/// A `GlobalId` identifies an RPC object uniquely among all the objects visible
/// to any active session on an RpcMgr.
///
/// Its encoding is unforgeable.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct GlobalId {
/// The RPC connection within whose object map `local_id` is visible.
pub(crate) connection: ConnectionId,
/// The identifier of the object within `connection`'s object map.
pub(crate) local_id: GenIdx,
}
/// The number of bytes in our [`MacKey`].
///
/// (Our choice of algorithm allows any key length we want; 128 bits should be
/// secure enough.)
const MAC_KEY_LEN: usize = 16;
/// The number of bytes in a [`Mac`].
///
/// (Our choice of algorithm allows any MAC length we want; 128 bits should be
/// enough to make the results unforgeable.)
const MAC_LEN: usize = 16;
/// An key that we use to compute message authentication codes (MACs) for our
/// [`GlobalId`]s
///
/// We do not guarantee any particular MAC algorithm; we should be able to
/// change MAC algorithms without breaking any user code. Right now, we choose a
/// Kangaroo12-based construction in order to be reasonably fast.
#[derive(Clone)]
pub(crate) struct MacKey {
/// The key itself.
key: Zeroizing<[u8; MAC_KEY_LEN]>,
}
/// A message authentication code produced by [`MacKey::mac`].
type Mac = CtByteArray<MAC_LEN>;
impl MacKey {
/// Construct a new random `MacKey`.
pub(crate) fn new<Rng: rand::Rng + rand::CryptoRng>(rng: &mut Rng) -> Self {
Self {
key: Zeroizing::new(rng.random()),
}
}
/// Compute the AMC of a given input `inp`, and store the result into `out`.
///
/// The current construction allows `out` to be any length.
fn mac(&self, inp: &[u8], out: &mut [u8]) {
use tiny_keccak::{Hasher as _, Kmac};
let mut mac = Kmac::v128(&self.key[..], b"artirpc globalid");
mac.update(inp);
mac.finalize(out);
}
}
impl GlobalId {
/// The number of bytes used to encode a `GlobalId` in binary form.
const ENCODED_LEN: usize = MAC_LEN + ConnectionId::LEN + GenIdx::BYTE_LEN;
/// A prefix we use when encoding global IDs in base64.
///
/// Since this isn't a valid base 64 character, we can't confuse it with
/// a base64 string.
const TAG_CHAR: char = '$';
/// Create a new GlobalId from its parts.
pub(crate) fn new(connection: ConnectionId, local_id: GenIdx) -> GlobalId {
GlobalId {
connection,
local_id,
}
}
/// Encode this ID in an unforgeable string that we can later use to
/// uniquely identify an RPC object.
///
/// As with local IDs, this encoding is nondeterministic.
pub(crate) fn encode(&self, key: &MacKey) -> ObjectId {
use base64ct::{Base64Unpadded as B64, Encoding};
let bytes = self.encode_as_bytes(key, &mut rand::rng());
let string = format!("{}{}", GlobalId::TAG_CHAR, B64::encode_string(&bytes[..]));
ObjectId::from(string)
}
/// As `encode`, but do not base64-encode the result.
fn encode_as_bytes<R: rand::RngCore>(&self, key: &MacKey, rng: &mut R) -> Vec<u8> {
let mut bytes = Vec::with_capacity(Self::ENCODED_LEN);
bytes.resize(MAC_LEN, 0);
bytes.extend_from_slice(self.connection.as_ref());
bytes.extend_from_slice(&self.local_id.to_bytes(rng));
{
// TODO RPC: Maybe we should stick the MAC at the end to make everything simpler.
let (mac, text) = bytes.split_at_mut(MAC_LEN);
key.mac(text, mac);
}
bytes
}
/// Try to decode and validate `s` as a [`GlobalId`].
///
/// Returns `Ok(None)` if `s` is not tagged as an identifier for a `GlobalId`.
pub(crate) fn try_decode(key: &MacKey, s: &ObjectId) -> Result<Option<Self>, LookupError> {
use base64ct::{Base64Unpadded as B64, Encoding};
if !s.as_ref().starts_with(GlobalId::TAG_CHAR) {
return Ok(None);
}
let mut bytes = [0_u8; Self::ENCODED_LEN];
let byte_slice = B64::decode(&s.as_ref()[1..], &mut bytes[..])
.map_err(|_| LookupError::NoObject(s.clone()))?;
Self::try_decode_from_bytes(key, byte_slice)
.ok_or_else(|| LookupError::NoObject(s.clone()))
.map(Some)
}
/// As `try_decode`, but expect a byte slice rather than a base64-encoded string.
fn try_decode_from_bytes(key: &MacKey, bytes: &[u8]) -> Option<Self> {
if bytes.len() != Self::ENCODED_LEN {
return None;
}
// TODO RPC: Just use Reader here?
let mut found_mac = [0; MAC_LEN];
key.mac(&bytes[MAC_LEN..], &mut found_mac[..]);
let found_mac = Mac::from(found_mac);
let mut r: Reader = Reader::from_slice(bytes);
let declared_mac: Mac = r.extract().ok()?;
if found_mac != declared_mac {
return None;
}
let connection = r.extract::<[u8; ConnectionId::LEN]>().ok()?.into();
let rest = r.into_rest();
let local_id = GenIdx::from_bytes(rest)?;
Some(Self {
connection,
local_id,
})
}
}
#[cfg(test)]
mod test {
// @@ begin test lint list maintained by maint/add_warning @@
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use super::*;
const GLOBAL_ID_B64_ENCODED_LEN: usize = (GlobalId::ENCODED_LEN * 8).div_ceil(6) + 1;
#[test]
fn roundtrip() {
use crate::objmap::{StrongIdx, WeakIdx};
use slotmap_careful::KeyData;
let mut rng = tor_basic_utils::test_rng::testing_rng();
let conn1 = ConnectionId::from(*b"example1-------!");
let conn2 = ConnectionId::from(*b"example2!!!!!!!!");
let genidx_s1 = GenIdx::Strong(StrongIdx::from(KeyData::from_ffi(0x43_0000_0043)));
let genidx_w2 = GenIdx::Weak(WeakIdx::from(KeyData::from_ffi(0x171_0000_0171)));
let gid1 = GlobalId {
connection: conn1,
local_id: genidx_s1,
};
let gid2 = GlobalId {
connection: conn2,
local_id: genidx_w2,
};
let mac_key = MacKey::new(&mut rng);
let enc1 = gid1.encode(&mac_key);
let gid1_decoded = GlobalId::try_decode(&mac_key, &enc1).unwrap().unwrap();
assert_eq!(gid1, gid1_decoded);
assert!(enc1.as_ref().starts_with(GlobalId::TAG_CHAR));
let enc2 = gid2.encode(&mac_key);
let gid2_decoded = GlobalId::try_decode(&mac_key, &enc2).unwrap().unwrap();
assert_eq!(gid2, gid2_decoded);
assert_ne!(gid1_decoded, gid2_decoded);
assert!(enc1.as_ref().starts_with(GlobalId::TAG_CHAR));
assert_eq!(enc1.as_ref().len(), GLOBAL_ID_B64_ENCODED_LEN);
assert_eq!(enc2.as_ref().len(), GLOBAL_ID_B64_ENCODED_LEN);
}
#[test]
fn not_a_global_id() {
let mut rng = tor_basic_utils::test_rng::testing_rng();
let mac_key = MacKey::new(&mut rng);
let decoded = GlobalId::try_decode(&mac_key, &ObjectId::from("helloworld"));
assert!(matches!(decoded, Ok(None)));
let decoded = GlobalId::try_decode(&mac_key, &ObjectId::from("$helloworld"));
assert!(matches!(decoded, Err(LookupError::NoObject(_))));
}
#[test]
fn mac_works() {
use crate::objmap::{StrongIdx, WeakIdx};
use slotmap_careful::KeyData;
let mut rng = tor_basic_utils::test_rng::testing_rng();
let conn1 = ConnectionId::from(*b"example1-------!");
let conn2 = ConnectionId::from(*b"example2!!!!!!!!");
let genidx_s1 = GenIdx::Strong(StrongIdx::from(KeyData::from_ffi(0x43_0000_0043)));
let genidx_w1 = GenIdx::Weak(WeakIdx::from(KeyData::from_ffi(0x171_0000_0171)));
let gid1 = GlobalId {
connection: conn1,
local_id: genidx_s1,
};
let gid2 = GlobalId {
connection: conn2,
local_id: genidx_w1,
};
let mac_key = MacKey::new(&mut rng);
let enc1 = gid1.encode_as_bytes(&mac_key, &mut rng);
let enc2 = gid2.encode_as_bytes(&mac_key, &mut rng);
// Make a 'combined' encoded gid with the mac from one and the info from
// the other.
let mut combined = Vec::from(&enc1[0..MAC_LEN]);
combined.extend_from_slice(&enc2[MAC_LEN..]);
let outcome = GlobalId::try_decode_from_bytes(&mac_key, &combined[..]);
// Can't decode, because MAC was wrong.
assert!(outcome.is_none());
}
}
|