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