arti_rpcserver/
globalid.rs1use rand::RngExt;
9use tor_bytes::Reader;
10use tor_llcrypto::util::ct::CtByteArray;
11use tor_rpcbase::{LookupError, ObjectId};
12use zeroize::Zeroizing;
13
14use crate::{connection::ConnectionId, objmap::GenIdx};
15
16#[derive(Clone, Debug, Eq, PartialEq)]
23pub(crate) struct GlobalId {
24 pub(crate) connection: ConnectionId,
26 pub(crate) local_id: GenIdx,
28}
29
30const MAC_KEY_LEN: usize = 16;
35const MAC_LEN: usize = 16;
40
41#[derive(Clone)]
48pub(crate) struct MacKey {
49 key: Zeroizing<[u8; MAC_KEY_LEN]>,
51}
52
53type Mac = CtByteArray<MAC_LEN>;
55
56impl MacKey {
57 pub(crate) fn new<Rng: rand::Rng + rand::CryptoRng>(rng: &mut Rng) -> Self {
59 Self {
60 key: Zeroizing::new(rng.random()),
61 }
62 }
63
64 fn mac(&self, inp: &[u8], out: &mut [u8]) {
68 use tiny_keccak::{Hasher as _, Kmac};
69 let mut mac = Kmac::v128(&self.key[..], b"artirpc globalid");
70 mac.update(inp);
71 mac.finalize(out);
72 }
73}
74
75impl GlobalId {
76 const ENCODED_LEN: usize = MAC_LEN + ConnectionId::LEN + GenIdx::BYTE_LEN;
78
79 const TAG_CHAR: char = '$';
84
85 pub(crate) fn new(connection: ConnectionId, local_id: GenIdx) -> GlobalId {
87 GlobalId {
88 connection,
89 local_id,
90 }
91 }
92
93 pub(crate) fn encode(&self, key: &MacKey) -> ObjectId {
98 use base64ct::{Base64Unpadded as B64, Encoding};
99 let bytes = self.encode_as_bytes(key, &mut rand::rng());
100 let string = format!("{}{}", GlobalId::TAG_CHAR, B64::encode_string(&bytes[..]));
101 ObjectId::from(string)
102 }
103
104 fn encode_as_bytes<R: rand::Rng>(&self, key: &MacKey, rng: &mut R) -> Vec<u8> {
106 let mut bytes = Vec::with_capacity(Self::ENCODED_LEN);
107 bytes.resize(MAC_LEN, 0);
108 bytes.extend_from_slice(self.connection.as_ref());
109 bytes.extend_from_slice(&self.local_id.to_bytes(rng));
110 {
111 let (mac, text) = bytes.split_at_mut(MAC_LEN);
113 key.mac(text, mac);
114 }
115 bytes
116 }
117
118 pub(crate) fn try_decode(key: &MacKey, s: &ObjectId) -> Result<Option<Self>, LookupError> {
122 use base64ct::{Base64Unpadded as B64, Encoding};
123 let Some(remainder) = s.as_ref().strip_prefix(GlobalId::TAG_CHAR) else {
124 return Ok(None);
125 };
126 let mut bytes = [0_u8; Self::ENCODED_LEN];
127 let byte_slice =
128 B64::decode(remainder, &mut bytes[..]).map_err(|_| LookupError::NoObject(s.clone()))?;
129 Self::try_decode_from_bytes(key, byte_slice)
130 .ok_or_else(|| LookupError::NoObject(s.clone()))
131 .map(Some)
132 }
133
134 fn try_decode_from_bytes(key: &MacKey, bytes: &[u8]) -> Option<Self> {
136 if bytes.len() != Self::ENCODED_LEN {
137 return None;
138 }
139
140 let mut found_mac = [0; MAC_LEN];
143 key.mac(&bytes[MAC_LEN..], &mut found_mac[..]);
144 let found_mac = Mac::from(found_mac);
145
146 let mut r: Reader = Reader::from_slice(bytes);
147 let declared_mac: Mac = r.extract().ok()?;
148 if found_mac != declared_mac {
149 return None;
150 }
151 let connection = r.extract::<[u8; ConnectionId::LEN]>().ok()?.into();
152 let rest = r.into_rest();
153 let local_id = GenIdx::from_bytes(rest)?;
154
155 Some(Self {
156 connection,
157 local_id,
158 })
159 }
160}
161
162#[cfg(test)]
163mod test {
164 #![allow(clippy::bool_assert_comparison)]
166 #![allow(clippy::clone_on_copy)]
167 #![allow(clippy::dbg_macro)]
168 #![allow(clippy::mixed_attributes_style)]
169 #![allow(clippy::print_stderr)]
170 #![allow(clippy::print_stdout)]
171 #![allow(clippy::single_char_pattern)]
172 #![allow(clippy::unwrap_used)]
173 #![allow(clippy::unchecked_time_subtraction)]
174 #![allow(clippy::useless_vec)]
175 #![allow(clippy::needless_pass_by_value)]
176 #![allow(clippy::string_slice)] use super::*;
180
181 const GLOBAL_ID_B64_ENCODED_LEN: usize = (GlobalId::ENCODED_LEN * 8).div_ceil(6) + 1;
182
183 #[test]
184 fn roundtrip() {
185 use slotmap_careful::KeyData;
186 let mut rng = tor_basic_utils::test_rng::testing_rng();
187
188 let conn1 = ConnectionId::from(*b"example1-------!");
189 let genidx_s1 = GenIdx::from(KeyData::from_ffi(0x43_0000_0043));
190
191 let gid1 = GlobalId {
192 connection: conn1,
193 local_id: genidx_s1,
194 };
195 let mac_key = MacKey::new(&mut rng);
196 let enc1 = gid1.encode(&mac_key);
197 let gid1_decoded = GlobalId::try_decode(&mac_key, &enc1).unwrap().unwrap();
198 assert_eq!(gid1, gid1_decoded);
199 assert!(enc1.as_ref().starts_with(GlobalId::TAG_CHAR));
200
201 assert_eq!(enc1.as_ref().len(), GLOBAL_ID_B64_ENCODED_LEN);
202 }
203
204 #[test]
205 fn not_a_global_id() {
206 let mut rng = tor_basic_utils::test_rng::testing_rng();
207 let mac_key = MacKey::new(&mut rng);
208 let decoded = GlobalId::try_decode(&mac_key, &ObjectId::from("helloworld"));
209 assert!(matches!(decoded, Ok(None)));
210
211 let decoded = GlobalId::try_decode(&mac_key, &ObjectId::from("$helloworld"));
212 assert!(matches!(decoded, Err(LookupError::NoObject(_))));
213 }
214
215 #[test]
216 fn mac_works() {
217 use slotmap_careful::KeyData;
218 let mut rng = tor_basic_utils::test_rng::testing_rng();
219
220 let conn1 = ConnectionId::from(*b"example1-------!");
221 let conn2 = ConnectionId::from(*b"example2-------!");
222 let genidx_s1 = GenIdx::from(KeyData::from_ffi(0x43_0000_0043));
223 let genidx_s2 = GenIdx::from(KeyData::from_ffi(0x171_0000_0171));
224
225 let gid1 = GlobalId {
226 connection: conn1,
227 local_id: genidx_s1,
228 };
229 let gid2 = GlobalId {
230 connection: conn2,
231 local_id: genidx_s2,
232 };
233 let mac_key = MacKey::new(&mut rng);
234 let enc1 = gid1.encode_as_bytes(&mac_key, &mut rng);
235 let enc2 = gid2.encode_as_bytes(&mac_key, &mut rng);
236
237 let mut combined = Vec::from(&enc1[0..MAC_LEN]);
240 combined.extend_from_slice(&enc2[MAC_LEN..]);
241 let outcome = GlobalId::try_decode_from_bytes(&mac_key, &combined[..]);
242 assert!(outcome.is_none());
244 }
245}