Skip to main content

arti_rpcserver/
globalid.rs

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.
7
8use 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/// A [RpcMgr](crate::RpcMgr)-scoped identifier for an RPC object.
17///
18/// A `GlobalId` identifies an RPC object uniquely among all the objects visible
19/// to any active session on an RpcMgr.
20///
21/// Its encoding is unforgeable.
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub(crate) struct GlobalId {
24    /// The RPC connection within whose object map `local_id` is visible.
25    pub(crate) connection: ConnectionId,
26    /// The identifier of the object within `connection`'s object map.
27    pub(crate) local_id: GenIdx,
28}
29
30/// The number of bytes in our [`MacKey`].
31///
32/// (Our choice of algorithm allows any key length we want; 128 bits should be
33/// secure enough.)
34const MAC_KEY_LEN: usize = 16;
35/// The number of bytes in a [`Mac`].
36///
37/// (Our choice of algorithm allows any MAC length we want; 128 bits should be
38/// enough to make the results unforgeable.)
39const MAC_LEN: usize = 16;
40
41/// An key that we use to compute message authentication codes (MACs) for our
42/// [`GlobalId`]s
43///
44/// We do not guarantee any particular MAC algorithm; we should be able to
45/// change MAC algorithms without breaking any user code. Right now, we choose a
46/// Kangaroo12-based construction in order to be reasonably fast.
47#[derive(Clone)]
48pub(crate) struct MacKey {
49    /// The key itself.
50    key: Zeroizing<[u8; MAC_KEY_LEN]>,
51}
52
53/// A message authentication code produced by [`MacKey::mac`].
54type Mac = CtByteArray<MAC_LEN>;
55
56impl MacKey {
57    /// Construct a new random `MacKey`.
58    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    /// Compute the AMC of a given input `inp`, and store the result into `out`.
65    ///
66    /// The current construction allows `out` to be any length.
67    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    /// The number of bytes used to encode a `GlobalId` in binary form.
77    const ENCODED_LEN: usize = MAC_LEN + ConnectionId::LEN + GenIdx::BYTE_LEN;
78
79    /// A prefix we use when encoding global IDs in base64.
80    ///
81    /// Since this isn't a valid base 64 character, we can't confuse it with
82    /// a base64 string.
83    const TAG_CHAR: char = '$';
84
85    /// Create a new GlobalId from its parts.
86    pub(crate) fn new(connection: ConnectionId, local_id: GenIdx) -> GlobalId {
87        GlobalId {
88            connection,
89            local_id,
90        }
91    }
92
93    /// Encode this ID in an unforgeable string that we can later use to
94    /// uniquely identify an RPC object.
95    ///
96    /// As with local IDs, this encoding is nondeterministic.
97    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    /// As `encode`, but do not base64-encode the result.
105    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            // TODO RPC: Maybe we should stick the MAC at the end to make everything simpler.
112            let (mac, text) = bytes.split_at_mut(MAC_LEN);
113            key.mac(text, mac);
114        }
115        bytes
116    }
117
118    /// Try to decode and validate `s` as a [`GlobalId`].
119    ///
120    /// Returns `Ok(None)` if `s` is not tagged as an identifier for a `GlobalId`.
121    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    /// As `try_decode`, but expect a byte slice rather than a base64-encoded string.
135    fn try_decode_from_bytes(key: &MacKey, bytes: &[u8]) -> Option<Self> {
136        if bytes.len() != Self::ENCODED_LEN {
137            return None;
138        }
139
140        // TODO RPC: Just use Reader here?
141
142        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    // @@ begin test lint list maintained by maint/add_warning @@
165    #![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)] // See arti#2571
177    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
178
179    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        // Make a 'combined' encoded gid with the mac from one and the info from
238        // the other.
239        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        // Can't decode, because MAC was wrong.
243        assert!(outcome.is_none());
244    }
245}