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