xmtp_db/encrypted_store/
user_preferences.rs

1use super::{
2    ConnectionExt,
3    schema::user_preferences::{self, dsl},
4};
5use crate::{StorageError, Store};
6use diesel::{insert_into, prelude::*};
7use xmtp_common::time::now_ns;
8
9#[derive(
10    Identifiable, Insertable, Queryable, AsChangeset, Debug, Clone, PartialEq, Eq, Default,
11)]
12#[diesel(table_name = user_preferences)]
13#[diesel(primary_key(id))]
14pub struct StoredUserPreferences {
15    pub id: i32,
16    /// HMAC key root
17    pub hmac_key: Option<Vec<u8>>,
18    pub hmac_key_cycled_at_ns: Option<i64>,
19    /// Whether DM group updates have been migrated.
20    pub dm_group_updates_migrated: bool,
21}
22
23impl<C> Store<C> for StoredUserPreferences
24where
25    C: ConnectionExt,
26{
27    type Output = ();
28    fn store(&self, conn: &C) -> Result<Self::Output, StorageError> {
29        conn.raw_query_write(|conn| {
30            diesel::update(dsl::user_preferences)
31                .set(self)
32                .execute(conn)
33        })?;
34
35        Ok(())
36    }
37}
38
39#[derive(Debug)]
40pub struct HmacKey {
41    // TODO: Use xmtp_cryptography::Secret for Zeroize support
42    pub key: [u8; 42],
43    // # of 30 day periods since unix epoch
44    pub epoch: i64,
45}
46
47impl HmacKey {
48    pub fn random_key() -> Vec<u8> {
49        xmtp_common::rand_vec::<42>()
50    }
51}
52
53impl StoredUserPreferences {
54    pub fn load(conn: impl ConnectionExt) -> Result<Self, StorageError> {
55        let pref = conn.raw_query_read(|conn| dsl::user_preferences.first(conn).optional())?;
56        Ok(pref.unwrap_or_default())
57    }
58
59    fn store(&self, conn: &impl crate::DbQuery) -> Result<(), StorageError> {
60        conn.raw_query_write(|conn| {
61            insert_into(dsl::user_preferences)
62                .values(self)
63                .on_conflict(user_preferences::id)
64                .do_update()
65                .set(self)
66                .execute(conn)
67        })?;
68
69        Ok(())
70    }
71
72    pub fn store_hmac_key(
73        conn: &impl crate::DbQuery,
74        key: &[u8],
75        cycled_at: Option<i64>,
76    ) -> Result<(), StorageError> {
77        if key.len() != 42 {
78            return Err(StorageError::InvalidHmacLength);
79        }
80
81        let mut preferences = Self::load(conn)?;
82
83        if let (Some(old), Some(new)) = (preferences.hmac_key_cycled_at_ns, cycled_at)
84            && old > new
85        {
86            return Ok(());
87        }
88
89        preferences.hmac_key = Some(key.to_vec());
90        preferences.hmac_key_cycled_at_ns = Some(cycled_at.unwrap_or_else(now_ns));
91        preferences.store(conn)?;
92
93        Ok(())
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    #[cfg(target_arch = "wasm32")]
100    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);
101    use super::*;
102
103    #[xmtp_common::test]
104    fn test_insert_and_update_preferences() {
105        crate::test_utils::with_connection(|conn| {
106            let pref = StoredUserPreferences::load(conn).unwrap();
107            // by default, there is no key
108            assert!(pref.hmac_key.is_none());
109
110            // loads and stores a default
111            let pref = StoredUserPreferences::load(conn).unwrap();
112            // by default, there is no key
113            assert!(pref.hmac_key.is_none());
114
115            // set an hmac key
116            let hmac_key = HmacKey::random_key();
117            StoredUserPreferences::store_hmac_key(conn, &hmac_key, None).unwrap();
118            let pref = StoredUserPreferences::load(conn).unwrap();
119            // Make sure it saved
120            assert_eq!(hmac_key, pref.hmac_key.unwrap());
121
122            // check that there is only one preference stored
123            let query = dsl::user_preferences.order(dsl::id.desc());
124            let result = conn
125                .raw_query_read(|conn| query.load::<StoredUserPreferences>(conn))
126                .unwrap();
127            assert_eq!(result.len(), 1);
128        })
129    }
130}