spacetimedb_lib/
identity.rs

1use crate::from_hex_pad;
2use blake3;
3use core::mem;
4use spacetimedb_bindings_macro::{Deserialize, Serialize};
5use spacetimedb_sats::hex::HexString;
6use spacetimedb_sats::{impl_st, u256, AlgebraicType, AlgebraicValue};
7use std::{fmt, str::FromStr};
8
9pub type RequestId = u32;
10
11#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
12pub struct AuthCtx {
13    pub owner: Identity,
14    pub caller: Identity,
15}
16
17impl AuthCtx {
18    pub fn new(owner: Identity, caller: Identity) -> Self {
19        Self { owner, caller }
20    }
21    /// For when the owner == caller
22    pub fn for_current(owner: Identity) -> Self {
23        Self { owner, caller: owner }
24    }
25    /// Does `owner == caller`
26    pub fn is_owner(&self) -> bool {
27        self.owner == self.caller
28    }
29    /// WARNING: Use this only for simple test were the `auth` don't matter
30    pub fn for_testing() -> Self {
31        AuthCtx {
32            owner: Identity::__dummy(),
33            caller: Identity::__dummy(),
34        }
35    }
36}
37
38/// An `Identity` for something interacting with the database.
39///
40/// <!-- TODO: docs for OpenID stuff. -->
41///
42/// An `Identity` is a 256-bit unsigned integer. These are encoded in various ways.
43/// - In JSON, an `Identity` is represented as a hexadecimal number wrapped in a string, `"0x[64 hex characters]"`.
44/// - In BSATN, an `Identity` is represented as a LITTLE-ENDIAN number 32 bytes long.
45/// - In memory, an `Identity` is stored as a 256-bit number with the endianness of the host system.
46///
47/// If you are manually converting a hexadecimal string to a byte array like so:
48/// ```ignore
49/// "0xb0b1b2..."
50/// ->
51/// [0xb0, 0xb1, 0xb2, ...]
52/// ```
53/// Make sure you call `Identity::from_be_byte_array` and NOT `Identity::from_byte_array`.
54/// The standard way of writing hexadecimal numbers follows a big-endian convention, if you
55/// index the characters in written text in increasing order from left to right.
56#[derive(Default, Eq, PartialEq, PartialOrd, Ord, Clone, Copy, Hash, Serialize, Deserialize)]
57pub struct Identity {
58    __identity__: u256,
59}
60
61impl_st!([] Identity, AlgebraicType::identity());
62
63#[cfg(feature = "memory-usage")]
64impl spacetimedb_memory_usage::MemoryUsage for Identity {}
65
66#[cfg(feature = "metrics_impls")]
67impl spacetimedb_metrics::typed_prometheus::AsPrometheusLabel for Identity {
68    fn as_prometheus_str(&self) -> impl AsRef<str> + '_ {
69        self.to_hex()
70    }
71}
72
73impl Identity {
74    /// The 0x0 `Identity`
75    pub const ZERO: Self = Self::from_u256(u256::ZERO);
76
77    /// The 0x1 `Identity`
78    pub const ONE: Self = Self::from_u256(u256::ONE);
79
80    /// Create an `Identity` from a LITTLE-ENDIAN byte array.
81    ///
82    /// If you are parsing an `Identity` from a string, you probably want `from_be_byte_array` instead.
83    pub const fn from_byte_array(bytes: [u8; 32]) -> Self {
84        // SAFETY: The transmute is an implementation of `u256::from_le_bytes`,
85        // but works in a const context.
86        Self::from_u256(u256::from_le(unsafe { mem::transmute::<[u8; 32], u256>(bytes) }))
87    }
88
89    /// Create an `Identity` from a BIG-ENDIAN byte array.
90    ///
91    /// This method is the correct choice if you have converted the bytes of a hexadecimal-formatted `Identity`
92    /// to a byte array in the following way:
93    /// ```ignore
94    /// "0xb0b1b2..."
95    /// ->
96    /// [0xb0, 0xb1, 0xb2, ...]
97    /// ```
98    pub const fn from_be_byte_array(bytes: [u8; 32]) -> Self {
99        // SAFETY: The transmute is an implementation of `u256::from_be_bytes`,
100        // but works in a const context.
101        Self::from_u256(u256::from_be(unsafe { mem::transmute::<[u8; 32], u256>(bytes) }))
102    }
103
104    /// Converts `__identity__: u256` to `Identity`.
105    pub const fn from_u256(__identity__: u256) -> Self {
106        Self { __identity__ }
107    }
108
109    /// Converts this identity to an `u256`.
110    pub const fn to_u256(&self) -> u256 {
111        self.__identity__
112    }
113
114    #[doc(hidden)]
115    pub fn __dummy() -> Self {
116        Self::ZERO
117    }
118
119    pub fn from_claims(issuer: &str, subject: &str) -> Self {
120        let input = format!("{issuer}|{subject}");
121        let first_hash = blake3::hash(input.as_bytes());
122        let id_hash = &first_hash.as_bytes()[..26];
123        let mut checksum_input = [0u8; 28];
124        // TODO: double check this gets the right number...
125        checksum_input[2..].copy_from_slice(id_hash);
126        checksum_input[0] = 0xc2;
127        checksum_input[1] = 0x00;
128        let checksum_hash = &blake3::hash(&checksum_input);
129
130        let mut final_bytes = [0u8; 32];
131        final_bytes[0] = 0xc2;
132        final_bytes[1] = 0x00;
133        final_bytes[2..6].copy_from_slice(&checksum_hash.as_bytes()[..4]);
134        final_bytes[6..].copy_from_slice(id_hash);
135
136        // We want the leading two bytes of the Identity to be `c200` when formatted.
137        // This means that these should be the MOST significant bytes.
138        // This corresponds to a BIG-ENDIAN byte order of our buffer above.
139        Identity::from_be_byte_array(final_bytes)
140    }
141
142    /// Returns this `Identity` as a byte array.
143    pub fn to_byte_array(&self) -> [u8; 32] {
144        self.__identity__.to_le_bytes()
145    }
146
147    /// Convert this `Identity` to a BIG-ENDIAN byte array.
148    pub fn to_be_byte_array(&self) -> [u8; 32] {
149        self.__identity__.to_be_bytes()
150    }
151
152    /// Convert this `Identity` to a hexadecimal string.
153    pub fn to_hex(&self) -> HexString<32> {
154        spacetimedb_sats::hex::encode(&self.to_be_byte_array())
155    }
156
157    /// Extract the first 8 bytes of this `Identity` as if it was stored in BIG-ENDIAN
158    /// format. (That is, the most significant bytes.)
159    pub fn abbreviate(&self) -> [u8; 8] {
160        self.to_be_byte_array()[..8].try_into().unwrap()
161    }
162
163    /// Extract the first 16 characters of this `Identity`'s hexadecimal representation.
164    pub fn to_abbreviated_hex(&self) -> HexString<8> {
165        spacetimedb_sats::hex::encode(&self.abbreviate())
166    }
167
168    pub fn from_hex(hex: impl AsRef<[u8]>) -> Result<Self, hex::FromHexError> {
169        hex::FromHex::from_hex(hex)
170    }
171}
172
173impl fmt::Display for Identity {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        f.pad(&self.to_hex())
176    }
177}
178
179impl fmt::Debug for Identity {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        f.debug_tuple("Identity").field(&self.to_hex()).finish()
182    }
183}
184
185impl hex::FromHex for Identity {
186    type Error = hex::FromHexError;
187
188    fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
189        from_hex_pad(hex).map(Identity::from_be_byte_array)
190    }
191}
192
193impl FromStr for Identity {
194    type Err = <Self as hex::FromHex>::Error;
195
196    fn from_str(s: &str) -> Result<Self, Self::Err> {
197        Self::from_hex(s)
198    }
199}
200
201impl From<Identity> for AlgebraicValue {
202    fn from(value: Identity) -> Self {
203        AlgebraicValue::product([value.to_u256().into()])
204    }
205}
206
207#[cfg(feature = "serde")]
208impl serde::Serialize for Identity {
209    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
210        spacetimedb_sats::ser::serde::serialize_to(&self.to_be_byte_array(), serializer)
211    }
212}
213
214#[cfg(feature = "serde")]
215impl<'de> serde::Deserialize<'de> for Identity {
216    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
217        let arr = spacetimedb_sats::de::serde::deserialize_from(deserializer)?;
218        Ok(Identity::from_be_byte_array(arr))
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use proptest::prelude::*;
226    use proptest::string::string_regex;
227    use spacetimedb_sats::{de::serde::DeserializeWrapper, ser::serde::SerializeWrapper, GroundSpacetimeType as _};
228
229    #[test]
230    fn identity_is_special() {
231        assert!(Identity::get_type().is_special());
232    }
233
234    #[test]
235    fn identity_json_serialization_big_endian() {
236        let id = Identity::from_be_byte_array([
237            0xff, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
238            28, 29, 30, 31,
239        ]);
240
241        let hex = id.to_hex();
242        assert!(
243            hex.as_str().starts_with("ff01"),
244            "expected {hex:?} to start with \"ff01\""
245        );
246
247        let json1 = serde_json::to_string(&id).unwrap();
248        let json2 = serde_json::to_string(SerializeWrapper::from_ref(&id)).unwrap();
249
250        assert!(
251            json1.contains(hex.as_str()),
252            "expected {json1} to contain {hex} but it didn't"
253        );
254        assert!(
255            json2.contains(hex.as_str()),
256            "expected {json2} to contain {hex} but it didn't"
257        );
258    }
259
260    /// Make sure the checksum is valid.
261    fn validate_checksum(id: &[u8; 32]) -> bool {
262        let checksum_input = &id[6..];
263        let mut checksum_input_with_prefix = [0u8; 28];
264        checksum_input_with_prefix[2..].copy_from_slice(checksum_input);
265        checksum_input_with_prefix[0] = 0xc2;
266        checksum_input_with_prefix[1] = 0x00;
267        let checksum_hash = &blake3::hash(&checksum_input_with_prefix);
268        checksum_hash.as_bytes()[0..4] == id[2..6]
269    }
270
271    proptest! {
272        #[test]
273        fn identity_conversions(w0: u128, w1: u128) {
274            let v = Identity::from_u256(u256::from_words(w0, w1));
275
276            prop_assert_eq!(Identity::from_byte_array(v.to_byte_array()), v);
277            prop_assert_eq!(Identity::from_be_byte_array(v.to_be_byte_array()), v);
278            prop_assert_eq!(Identity::from_hex(v.to_hex()).unwrap(), v);
279
280            let de1: Identity = serde_json::from_str(&serde_json::to_string(&v).unwrap()).unwrap();
281            prop_assert_eq!(de1, v);
282            let DeserializeWrapper(de2): DeserializeWrapper<Identity> = serde_json::from_str(&serde_json::to_string(SerializeWrapper::from_ref(&v)).unwrap()).unwrap();
283            prop_assert_eq!(de2, v);
284        }
285
286        #[test]
287        fn from_claims_formats_correctly(s1 in string_regex(r".{3,5}").unwrap(), s2 in string_regex(r".{3,5}").unwrap()) {
288            let id = Identity::from_claims(&s1, &s2);
289            prop_assert!(id.to_hex().starts_with("c200"));
290            prop_assert!(validate_checksum(&id.to_be_byte_array()));
291        }
292    }
293}