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    /// Derives an identity from a [JWT] `issuer` and a `subject`.
120    ///
121    /// [JWT]: https://en.wikipedia.org/wiki/JSON_Web_Token
122    pub fn from_claims(issuer: &str, subject: &str) -> Self {
123        let input = format!("{issuer}|{subject}");
124        let first_hash = blake3::hash(input.as_bytes());
125        let id_hash = &first_hash.as_bytes()[..26];
126        let mut checksum_input = [0u8; 28];
127        // TODO: double check this gets the right number...
128        checksum_input[2..].copy_from_slice(id_hash);
129        checksum_input[0] = 0xc2;
130        checksum_input[1] = 0x00;
131        let checksum_hash = &blake3::hash(&checksum_input);
132
133        let mut final_bytes = [0u8; 32];
134        final_bytes[0] = 0xc2;
135        final_bytes[1] = 0x00;
136        final_bytes[2..6].copy_from_slice(&checksum_hash.as_bytes()[..4]);
137        final_bytes[6..].copy_from_slice(id_hash);
138
139        // We want the leading two bytes of the Identity to be `c200` when formatted.
140        // This means that these should be the MOST significant bytes.
141        // This corresponds to a BIG-ENDIAN byte order of our buffer above.
142        Identity::from_be_byte_array(final_bytes)
143    }
144
145    /// Returns this `Identity` as a byte array.
146    pub fn to_byte_array(&self) -> [u8; 32] {
147        self.__identity__.to_le_bytes()
148    }
149
150    /// Convert this `Identity` to a BIG-ENDIAN byte array.
151    pub fn to_be_byte_array(&self) -> [u8; 32] {
152        self.__identity__.to_be_bytes()
153    }
154
155    /// Convert this `Identity` to a hexadecimal string.
156    pub fn to_hex(&self) -> HexString<32> {
157        spacetimedb_sats::hex::encode(&self.to_be_byte_array())
158    }
159
160    /// Extract the first 8 bytes of this `Identity` as if it was stored in BIG-ENDIAN
161    /// format. (That is, the most significant bytes.)
162    pub fn abbreviate(&self) -> [u8; 8] {
163        self.to_be_byte_array()[..8].try_into().unwrap()
164    }
165
166    /// Extract the first 16 characters of this `Identity`'s hexadecimal representation.
167    pub fn to_abbreviated_hex(&self) -> HexString<8> {
168        spacetimedb_sats::hex::encode(&self.abbreviate())
169    }
170
171    pub fn from_hex(hex: impl AsRef<[u8]>) -> Result<Self, hex::FromHexError> {
172        hex::FromHex::from_hex(hex)
173    }
174}
175
176impl fmt::Display for Identity {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        f.pad(&self.to_hex())
179    }
180}
181
182impl fmt::Debug for Identity {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        f.debug_tuple("Identity").field(&self.to_hex()).finish()
185    }
186}
187
188impl hex::FromHex for Identity {
189    type Error = hex::FromHexError;
190
191    fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
192        from_hex_pad(hex).map(Identity::from_be_byte_array)
193    }
194}
195
196impl FromStr for Identity {
197    type Err = <Self as hex::FromHex>::Error;
198
199    fn from_str(s: &str) -> Result<Self, Self::Err> {
200        Self::from_hex(s)
201    }
202}
203
204impl From<Identity> for AlgebraicValue {
205    fn from(value: Identity) -> Self {
206        AlgebraicValue::product([value.to_u256().into()])
207    }
208}
209
210#[cfg(feature = "serde")]
211impl serde::Serialize for Identity {
212    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
213        spacetimedb_sats::ser::serde::serialize_to(&self.to_be_byte_array(), serializer)
214    }
215}
216
217#[cfg(feature = "serde")]
218impl<'de> serde::Deserialize<'de> for Identity {
219    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
220        let arr = spacetimedb_sats::de::serde::deserialize_from(deserializer)?;
221        Ok(Identity::from_be_byte_array(arr))
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use proptest::prelude::*;
229    use proptest::string::string_regex;
230    use spacetimedb_sats::{de::serde::DeserializeWrapper, ser::serde::SerializeWrapper, GroundSpacetimeType as _};
231
232    #[test]
233    fn identity_is_special() {
234        assert!(Identity::get_type().is_special());
235    }
236
237    #[test]
238    fn identity_json_serialization_big_endian() {
239        let id = Identity::from_be_byte_array([
240            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,
241            28, 29, 30, 31,
242        ]);
243
244        let hex = id.to_hex();
245        assert!(
246            hex.as_str().starts_with("ff01"),
247            "expected {hex:?} to start with \"ff01\""
248        );
249
250        let json1 = serde_json::to_string(&id).unwrap();
251        let json2 = serde_json::to_string(SerializeWrapper::from_ref(&id)).unwrap();
252
253        assert!(
254            json1.contains(hex.as_str()),
255            "expected {json1} to contain {hex} but it didn't"
256        );
257        assert!(
258            json2.contains(hex.as_str()),
259            "expected {json2} to contain {hex} but it didn't"
260        );
261    }
262
263    /// Make sure the checksum is valid.
264    fn validate_checksum(id: &[u8; 32]) -> bool {
265        let checksum_input = &id[6..];
266        let mut checksum_input_with_prefix = [0u8; 28];
267        checksum_input_with_prefix[2..].copy_from_slice(checksum_input);
268        checksum_input_with_prefix[0] = 0xc2;
269        checksum_input_with_prefix[1] = 0x00;
270        let checksum_hash = &blake3::hash(&checksum_input_with_prefix);
271        checksum_hash.as_bytes()[0..4] == id[2..6]
272    }
273
274    proptest! {
275        #[test]
276        fn identity_conversions(w0: u128, w1: u128) {
277            let v = Identity::from_u256(u256::from_words(w0, w1));
278
279            prop_assert_eq!(Identity::from_byte_array(v.to_byte_array()), v);
280            prop_assert_eq!(Identity::from_be_byte_array(v.to_be_byte_array()), v);
281            prop_assert_eq!(Identity::from_hex(v.to_hex()).unwrap(), v);
282
283            let de1: Identity = serde_json::from_str(&serde_json::to_string(&v).unwrap()).unwrap();
284            prop_assert_eq!(de1, v);
285            let DeserializeWrapper(de2): DeserializeWrapper<Identity> = serde_json::from_str(&serde_json::to_string(SerializeWrapper::from_ref(&v)).unwrap()).unwrap();
286            prop_assert_eq!(de2, v);
287        }
288
289        #[test]
290        fn from_claims_formats_correctly(s1 in string_regex(r".{3,5}").unwrap(), s2 in string_regex(r".{3,5}").unwrap()) {
291            let id = Identity::from_claims(&s1, &s2);
292            prop_assert!(id.to_hex().starts_with("c200"));
293            prop_assert!(validate_checksum(&id.to_be_byte_array()));
294        }
295    }
296}