use rand::RngExt;
use tor_bytes::Reader;
use tor_llcrypto::util::ct::CtByteArray;
use tor_rpcbase::{LookupError, ObjectId};
use zeroize::Zeroizing;
use crate::{connection::ConnectionId, objmap::GenIdx};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct GlobalId {
pub(crate) connection: ConnectionId,
pub(crate) local_id: GenIdx,
}
const MAC_KEY_LEN: usize = 16;
const MAC_LEN: usize = 16;
#[derive(Clone)]
pub(crate) struct MacKey {
key: Zeroizing<[u8; MAC_KEY_LEN]>,
}
type Mac = CtByteArray<MAC_LEN>;
impl MacKey {
pub(crate) fn new<Rng: rand::Rng + rand::CryptoRng>(rng: &mut Rng) -> Self {
Self {
key: Zeroizing::new(rng.random()),
}
}
fn mac(&self, inp: &[u8], out: &mut [u8]) {
use tiny_keccak::{Hasher as _, Kmac};
let mut mac = Kmac::v128(&self.key[..], b"artirpc globalid");
mac.update(inp);
mac.finalize(out);
}
}
impl GlobalId {
const ENCODED_LEN: usize = MAC_LEN + ConnectionId::LEN + GenIdx::BYTE_LEN;
const TAG_CHAR: char = '$';
pub(crate) fn new(connection: ConnectionId, local_id: GenIdx) -> GlobalId {
GlobalId {
connection,
local_id,
}
}
pub(crate) fn encode(&self, key: &MacKey) -> ObjectId {
use base64ct::{Base64Unpadded as B64, Encoding};
let bytes = self.encode_as_bytes(key, &mut rand::rng());
let string = format!("{}{}", GlobalId::TAG_CHAR, B64::encode_string(&bytes[..]));
ObjectId::from(string)
}
fn encode_as_bytes<R: rand::Rng>(&self, key: &MacKey, rng: &mut R) -> Vec<u8> {
let mut bytes = Vec::with_capacity(Self::ENCODED_LEN);
bytes.resize(MAC_LEN, 0);
bytes.extend_from_slice(self.connection.as_ref());
bytes.extend_from_slice(&self.local_id.to_bytes(rng));
{
let (mac, text) = bytes.split_at_mut(MAC_LEN);
key.mac(text, mac);
}
bytes
}
pub(crate) fn try_decode(key: &MacKey, s: &ObjectId) -> Result<Option<Self>, LookupError> {
use base64ct::{Base64Unpadded as B64, Encoding};
if !s.as_ref().starts_with(GlobalId::TAG_CHAR) {
return Ok(None);
}
let mut bytes = [0_u8; Self::ENCODED_LEN];
let byte_slice = B64::decode(&s.as_ref()[1..], &mut bytes[..])
.map_err(|_| LookupError::NoObject(s.clone()))?;
Self::try_decode_from_bytes(key, byte_slice)
.ok_or_else(|| LookupError::NoObject(s.clone()))
.map(Some)
}
fn try_decode_from_bytes(key: &MacKey, bytes: &[u8]) -> Option<Self> {
if bytes.len() != Self::ENCODED_LEN {
return None;
}
let mut found_mac = [0; MAC_LEN];
key.mac(&bytes[MAC_LEN..], &mut found_mac[..]);
let found_mac = Mac::from(found_mac);
let mut r: Reader = Reader::from_slice(bytes);
let declared_mac: Mac = r.extract().ok()?;
if found_mac != declared_mac {
return None;
}
let connection = r.extract::<[u8; ConnectionId::LEN]>().ok()?.into();
let rest = r.into_rest();
let local_id = GenIdx::from_bytes(rest)?;
Some(Self {
connection,
local_id,
})
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
const GLOBAL_ID_B64_ENCODED_LEN: usize = (GlobalId::ENCODED_LEN * 8).div_ceil(6) + 1;
#[test]
fn roundtrip() {
use slotmap_careful::KeyData;
let mut rng = tor_basic_utils::test_rng::testing_rng();
let conn1 = ConnectionId::from(*b"example1-------!");
let genidx_s1 = GenIdx::from(KeyData::from_ffi(0x43_0000_0043));
let gid1 = GlobalId {
connection: conn1,
local_id: genidx_s1,
};
let mac_key = MacKey::new(&mut rng);
let enc1 = gid1.encode(&mac_key);
let gid1_decoded = GlobalId::try_decode(&mac_key, &enc1).unwrap().unwrap();
assert_eq!(gid1, gid1_decoded);
assert!(enc1.as_ref().starts_with(GlobalId::TAG_CHAR));
assert_eq!(enc1.as_ref().len(), GLOBAL_ID_B64_ENCODED_LEN);
}
#[test]
fn not_a_global_id() {
let mut rng = tor_basic_utils::test_rng::testing_rng();
let mac_key = MacKey::new(&mut rng);
let decoded = GlobalId::try_decode(&mac_key, &ObjectId::from("helloworld"));
assert!(matches!(decoded, Ok(None)));
let decoded = GlobalId::try_decode(&mac_key, &ObjectId::from("$helloworld"));
assert!(matches!(decoded, Err(LookupError::NoObject(_))));
}
#[test]
fn mac_works() {
use slotmap_careful::KeyData;
let mut rng = tor_basic_utils::test_rng::testing_rng();
let conn1 = ConnectionId::from(*b"example1-------!");
let conn2 = ConnectionId::from(*b"example2-------!");
let genidx_s1 = GenIdx::from(KeyData::from_ffi(0x43_0000_0043));
let genidx_s2 = GenIdx::from(KeyData::from_ffi(0x171_0000_0171));
let gid1 = GlobalId {
connection: conn1,
local_id: genidx_s1,
};
let gid2 = GlobalId {
connection: conn2,
local_id: genidx_s2,
};
let mac_key = MacKey::new(&mut rng);
let enc1 = gid1.encode_as_bytes(&mac_key, &mut rng);
let enc2 = gid2.encode_as_bytes(&mac_key, &mut rng);
let mut combined = Vec::from(&enc1[0..MAC_LEN]);
combined.extend_from_slice(&enc2[MAC_LEN..]);
let outcome = GlobalId::try_decode_from_bytes(&mac_key, &combined[..]);
assert!(outcome.is_none());
}
}