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

            
8
use rand::RngExt;
9
use tor_bytes::Reader;
10
use tor_llcrypto::util::ct::CtByteArray;
11
use tor_rpcbase::{LookupError, ObjectId};
12
use zeroize::Zeroizing;
13

            
14
use 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)]
23
pub(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.)
34
const 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.)
39
const 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)]
48
pub(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`].
54
type Mac = CtByteArray<MAC_LEN>;
55

            
56
impl MacKey {
57
    /// Construct a new random `MacKey`.
58
6
    pub(crate) fn new<Rng: rand::Rng + rand::CryptoRng>(rng: &mut Rng) -> Self {
59
6
        Self {
60
6
            key: Zeroizing::new(rng.random()),
61
6
        }
62
6
    }
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
10
    fn mac(&self, inp: &[u8], out: &mut [u8]) {
68
        use tiny_keccak::{Hasher as _, Kmac};
69
10
        let mut mac = Kmac::v128(&self.key[..], b"artirpc globalid");
70
10
        mac.update(inp);
71
10
        mac.finalize(out);
72
10
    }
73
}
74

            
75
impl 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
2
    pub(crate) fn encode(&self, key: &MacKey) -> ObjectId {
98
        use base64ct::{Base64Unpadded as B64, Encoding};
99
2
        let bytes = self.encode_as_bytes(key, &mut rand::rng());
100
2
        let string = format!("{}{}", GlobalId::TAG_CHAR, B64::encode_string(&bytes[..]));
101
2
        ObjectId::from(string)
102
2
    }
103

            
104
    /// As `encode`, but do not base64-encode the result.
105
6
    fn encode_as_bytes<R: rand::Rng>(&self, key: &MacKey, rng: &mut R) -> Vec<u8> {
106
6
        let mut bytes = Vec::with_capacity(Self::ENCODED_LEN);
107
6
        bytes.resize(MAC_LEN, 0);
108
6
        bytes.extend_from_slice(self.connection.as_ref());
109
6
        bytes.extend_from_slice(&self.local_id.to_bytes(rng));
110
6
        {
111
6
            // TODO RPC: Maybe we should stick the MAC at the end to make everything simpler.
112
6
            let (mac, text) = bytes.split_at_mut(MAC_LEN);
113
6
            key.mac(text, mac);
114
6
        }
115
6
        bytes
116
6
    }
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
    #[allow(clippy::string_slice)] // TODO
122
6
    pub(crate) fn try_decode(key: &MacKey, s: &ObjectId) -> Result<Option<Self>, LookupError> {
123
        use base64ct::{Base64Unpadded as B64, Encoding};
124
6
        if !s.as_ref().starts_with(GlobalId::TAG_CHAR) {
125
2
            return Ok(None);
126
4
        }
127
4
        let mut bytes = [0_u8; Self::ENCODED_LEN];
128
4
        let byte_slice = B64::decode(&s.as_ref()[1..], &mut bytes[..])
129
5
            .map_err(|_| LookupError::NoObject(s.clone()))?;
130
2
        Self::try_decode_from_bytes(key, byte_slice)
131
2
            .ok_or_else(|| LookupError::NoObject(s.clone()))
132
2
            .map(Some)
133
6
    }
134

            
135
    /// As `try_decode`, but expect a byte slice rather than a base64-encoded string.
136
4
    fn try_decode_from_bytes(key: &MacKey, bytes: &[u8]) -> Option<Self> {
137
4
        if bytes.len() != Self::ENCODED_LEN {
138
            return None;
139
4
        }
140

            
141
        // TODO RPC: Just use Reader here?
142

            
143
4
        let mut found_mac = [0; MAC_LEN];
144
4
        key.mac(&bytes[MAC_LEN..], &mut found_mac[..]);
145
4
        let found_mac = Mac::from(found_mac);
146

            
147
4
        let mut r: Reader = Reader::from_slice(bytes);
148
4
        let declared_mac: Mac = r.extract().ok()?;
149
4
        if found_mac != declared_mac {
150
2
            return None;
151
2
        }
152
2
        let connection = r.extract::<[u8; ConnectionId::LEN]>().ok()?.into();
153
2
        let rest = r.into_rest();
154
2
        let local_id = GenIdx::from_bytes(rest)?;
155

            
156
2
        Some(Self {
157
2
            connection,
158
2
            local_id,
159
2
        })
160
4
    }
161
}
162

            
163
#[cfg(test)]
164
mod test {
165
    // @@ begin test lint list maintained by maint/add_warning @@
166
    #![allow(clippy::bool_assert_comparison)]
167
    #![allow(clippy::clone_on_copy)]
168
    #![allow(clippy::dbg_macro)]
169
    #![allow(clippy::mixed_attributes_style)]
170
    #![allow(clippy::print_stderr)]
171
    #![allow(clippy::print_stdout)]
172
    #![allow(clippy::single_char_pattern)]
173
    #![allow(clippy::unwrap_used)]
174
    #![allow(clippy::unchecked_time_subtraction)]
175
    #![allow(clippy::useless_vec)]
176
    #![allow(clippy::needless_pass_by_value)]
177
    #![allow(clippy::string_slice)] // See arti#2571
178
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
179

            
180
    use super::*;
181

            
182
    const GLOBAL_ID_B64_ENCODED_LEN: usize = (GlobalId::ENCODED_LEN * 8).div_ceil(6) + 1;
183

            
184
    #[test]
185
    fn roundtrip() {
186
        use slotmap_careful::KeyData;
187
        let mut rng = tor_basic_utils::test_rng::testing_rng();
188

            
189
        let conn1 = ConnectionId::from(*b"example1-------!");
190
        let genidx_s1 = GenIdx::from(KeyData::from_ffi(0x43_0000_0043));
191

            
192
        let gid1 = GlobalId {
193
            connection: conn1,
194
            local_id: genidx_s1,
195
        };
196
        let mac_key = MacKey::new(&mut rng);
197
        let enc1 = gid1.encode(&mac_key);
198
        let gid1_decoded = GlobalId::try_decode(&mac_key, &enc1).unwrap().unwrap();
199
        assert_eq!(gid1, gid1_decoded);
200
        assert!(enc1.as_ref().starts_with(GlobalId::TAG_CHAR));
201

            
202
        assert_eq!(enc1.as_ref().len(), GLOBAL_ID_B64_ENCODED_LEN);
203
    }
204

            
205
    #[test]
206
    fn not_a_global_id() {
207
        let mut rng = tor_basic_utils::test_rng::testing_rng();
208
        let mac_key = MacKey::new(&mut rng);
209
        let decoded = GlobalId::try_decode(&mac_key, &ObjectId::from("helloworld"));
210
        assert!(matches!(decoded, Ok(None)));
211

            
212
        let decoded = GlobalId::try_decode(&mac_key, &ObjectId::from("$helloworld"));
213
        assert!(matches!(decoded, Err(LookupError::NoObject(_))));
214
    }
215

            
216
    #[test]
217
    fn mac_works() {
218
        use slotmap_careful::KeyData;
219
        let mut rng = tor_basic_utils::test_rng::testing_rng();
220

            
221
        let conn1 = ConnectionId::from(*b"example1-------!");
222
        let conn2 = ConnectionId::from(*b"example2-------!");
223
        let genidx_s1 = GenIdx::from(KeyData::from_ffi(0x43_0000_0043));
224
        let genidx_s2 = GenIdx::from(KeyData::from_ffi(0x171_0000_0171));
225

            
226
        let gid1 = GlobalId {
227
            connection: conn1,
228
            local_id: genidx_s1,
229
        };
230
        let gid2 = GlobalId {
231
            connection: conn2,
232
            local_id: genidx_s2,
233
        };
234
        let mac_key = MacKey::new(&mut rng);
235
        let enc1 = gid1.encode_as_bytes(&mac_key, &mut rng);
236
        let enc2 = gid2.encode_as_bytes(&mac_key, &mut rng);
237

            
238
        // Make a 'combined' encoded gid with the mac from one and the info from
239
        // the other.
240
        let mut combined = Vec::from(&enc1[0..MAC_LEN]);
241
        combined.extend_from_slice(&enc2[MAC_LEN..]);
242
        let outcome = GlobalId::try_decode_from_bytes(&mac_key, &combined[..]);
243
        // Can't decode, because MAC was wrong.
244
        assert!(outcome.is_none());
245
    }
246
}