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