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 tor_bytes::Reader;
9use tor_llcrypto::util::ct::CtByteArray;
10use tor_rpcbase::{LookupError, ObjectId};
11use zeroize::Zeroizing;
12
13use crate::{connection::ConnectionId, objmap::GenIdx};
14
15/// 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.
24    pub(crate) connection: ConnectionId,
25    /// The identifier of the object within `connection`'s object map.
26    pub(crate) local_id: GenIdx,
27}
28
29/// 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;
39
40/// 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.
49    key: Zeroizing<[u8; MAC_KEY_LEN]>,
50}
51
52/// A message authentication code produced by [`MacKey::mac`].
53type Mac = CtByteArray<MAC_LEN>;
54
55impl MacKey {
56    /// Construct a new random `MacKey`.
57    pub(crate) fn new<Rng: rand::Rng + rand::CryptoRng>(rng: &mut Rng) -> Self {
58        Self {
59            key: Zeroizing::new(rng.random()),
60        }
61    }
62
63    /// 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.
66    fn mac(&self, inp: &[u8], out: &mut [u8]) {
67        use tiny_keccak::{Hasher as _, Kmac};
68        let mut mac = Kmac::v128(&self.key[..], b"artirpc globalid");
69        mac.update(inp);
70        mac.finalize(out);
71    }
72}
73
74impl GlobalId {
75    /// The number of bytes used to encode a `GlobalId` in binary form.
76    const ENCODED_LEN: usize = MAC_LEN + ConnectionId::LEN + GenIdx::BYTE_LEN;
77    /// The number of bytes used to encode a `GlobalId` in base-64 form.
78    pub(crate) const B64_ENCODED_LEN: usize = (Self::ENCODED_LEN * 8).div_ceil(6);
79
80    /// Create a new GlobalId from its parts.
81    pub(crate) fn new(connection: ConnectionId, local_id: GenIdx) -> GlobalId {
82        GlobalId {
83            connection,
84            local_id,
85        }
86    }
87
88    /// Encode this ID in an unforgeable string that we can later use to
89    /// uniquely identify an RPC object.
90    ///
91    /// As with local IDs, this encoding is nondeterministic.
92    pub(crate) fn encode(&self, key: &MacKey) -> ObjectId {
93        use base64ct::{Base64Unpadded as B64, Encoding};
94        let bytes = self.encode_as_bytes(key, &mut rand::rng());
95        B64::encode_string(&bytes[..]).into()
96    }
97
98    /// As `encode`, but do not base64-encode the result.
99    fn encode_as_bytes<R: rand::RngCore>(&self, key: &MacKey, rng: &mut R) -> Vec<u8> {
100        let mut bytes = Vec::with_capacity(Self::ENCODED_LEN);
101        bytes.resize(MAC_LEN, 0);
102        bytes.extend_from_slice(self.connection.as_ref());
103        bytes.extend_from_slice(&self.local_id.to_bytes(rng));
104        {
105            // TODO RPC: Maybe we should stick the MAC at the end to make everything simpler.
106            let (mac, text) = bytes.split_at_mut(MAC_LEN);
107            key.mac(text, mac);
108        }
109        bytes
110    }
111
112    /// Try to decode and validate `s` as a [`GlobalId`].
113    pub(crate) fn try_decode(key: &MacKey, s: &ObjectId) -> Result<Self, LookupError> {
114        use base64ct::{Base64Unpadded as B64, Encoding};
115        let mut bytes = [0_u8; Self::ENCODED_LEN];
116        let byte_slice = B64::decode(s.as_ref(), &mut bytes[..])
117            .map_err(|_| LookupError::NoObject(s.clone()))?;
118        Self::try_decode_from_bytes(key, byte_slice).ok_or_else(|| LookupError::NoObject(s.clone()))
119    }
120
121    /// As `try_decode`, but expect a byte slice rather than a base64-encoded string.
122    fn try_decode_from_bytes(key: &MacKey, bytes: &[u8]) -> Option<Self> {
123        if bytes.len() != Self::ENCODED_LEN {
124            return None;
125        }
126
127        // TODO RPC: Just use Reader here?
128
129        let mut found_mac = [0; MAC_LEN];
130        key.mac(&bytes[MAC_LEN..], &mut found_mac[..]);
131        let found_mac = Mac::from(found_mac);
132
133        let mut r: Reader = Reader::from_slice(bytes);
134        let declared_mac: Mac = r.extract().ok()?;
135        if found_mac != declared_mac {
136            return None;
137        }
138        let connection = r.extract::<[u8; ConnectionId::LEN]>().ok()?.into();
139        let rest = r.into_rest();
140        let local_id = GenIdx::from_bytes(rest)?;
141
142        Some(Self {
143            connection,
144            local_id,
145        })
146    }
147}
148
149#[cfg(test)]
150mod test {
151    // @@ begin test lint list maintained by maint/add_warning @@
152    #![allow(clippy::bool_assert_comparison)]
153    #![allow(clippy::clone_on_copy)]
154    #![allow(clippy::dbg_macro)]
155    #![allow(clippy::mixed_attributes_style)]
156    #![allow(clippy::print_stderr)]
157    #![allow(clippy::print_stdout)]
158    #![allow(clippy::single_char_pattern)]
159    #![allow(clippy::unwrap_used)]
160    #![allow(clippy::unchecked_duration_subtraction)]
161    #![allow(clippy::useless_vec)]
162    #![allow(clippy::needless_pass_by_value)]
163    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
164
165    use super::*;
166
167    #[test]
168    fn roundtrip() {
169        use crate::objmap::{StrongIdx, WeakIdx};
170        use slotmap_careful::KeyData;
171        let mut rng = tor_basic_utils::test_rng::testing_rng();
172
173        let conn1 = ConnectionId::from(*b"example1-------!");
174        let conn2 = ConnectionId::from(*b"example2!!!!!!!!");
175        let genidx_s1 = GenIdx::Strong(StrongIdx::from(KeyData::from_ffi(0x43_0000_0043)));
176        let genidx_w2 = GenIdx::Weak(WeakIdx::from(KeyData::from_ffi(0x171_0000_0171)));
177
178        let gid1 = GlobalId {
179            connection: conn1,
180            local_id: genidx_s1,
181        };
182        let gid2 = GlobalId {
183            connection: conn2,
184            local_id: genidx_w2,
185        };
186
187        let mac_key = MacKey::new(&mut rng);
188        let enc1 = gid1.encode(&mac_key);
189        let gid1_decoded = GlobalId::try_decode(&mac_key, &enc1).unwrap();
190        assert_eq!(gid1, gid1_decoded);
191
192        let enc2 = gid2.encode(&mac_key);
193        let gid2_decoded = GlobalId::try_decode(&mac_key, &enc2).unwrap();
194        assert_eq!(gid2, gid2_decoded);
195        assert_ne!(gid1_decoded, gid2_decoded);
196
197        assert_eq!(enc1.as_ref().len(), GlobalId::B64_ENCODED_LEN);
198        assert_eq!(enc2.as_ref().len(), GlobalId::B64_ENCODED_LEN);
199    }
200
201    #[test]
202    fn mac_works() {
203        use crate::objmap::{StrongIdx, WeakIdx};
204        use slotmap_careful::KeyData;
205        let mut rng = tor_basic_utils::test_rng::testing_rng();
206
207        let conn1 = ConnectionId::from(*b"example1-------!");
208        let conn2 = ConnectionId::from(*b"example2!!!!!!!!");
209        let genidx_s1 = GenIdx::Strong(StrongIdx::from(KeyData::from_ffi(0x43_0000_0043)));
210        let genidx_w1 = GenIdx::Weak(WeakIdx::from(KeyData::from_ffi(0x171_0000_0171)));
211
212        let gid1 = GlobalId {
213            connection: conn1,
214            local_id: genidx_s1,
215        };
216        let gid2 = GlobalId {
217            connection: conn2,
218            local_id: genidx_w1,
219        };
220        let mac_key = MacKey::new(&mut rng);
221        let enc1 = gid1.encode_as_bytes(&mac_key, &mut rng);
222        let enc2 = gid2.encode_as_bytes(&mac_key, &mut rng);
223
224        // Make a 'combined' encoded gid with the mac from one and the info from
225        // the other.
226        let mut combined = Vec::from(&enc1[0..MAC_LEN]);
227        combined.extend_from_slice(&enc2[MAC_LEN..]);
228        let outcome = GlobalId::try_decode_from_bytes(&mac_key, &combined[..]);
229        // Can't decode, because MAC was wrong.
230        assert!(outcome.is_none());
231    }
232}