Skip to main content

ic_auth_client/
storage.rs

1use base64::prelude::{BASE64_STANDARD_NO_PAD, Engine as _};
2use std::future::Future;
3use web_sys::{Storage, wasm_bindgen::JsValue};
4
5/// A key for storing the identity key pair.
6pub const KEY_STORAGE_KEY: &str = "identity";
7/// A key for storing the delegation chain.
8pub const KEY_STORAGE_DELEGATION: &str = "delegation";
9pub(crate) const KEY_VECTOR: &str = "iv";
10
11const LOCAL_STORAGE_PREFIX: &str = "ic-";
12
13/// Enum for storing different types of keys.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum StoredKey {
16    String(String),
17}
18
19impl StoredKey {
20    pub fn decode(&self) -> Result<[u8; 32], DecodeError> {
21        match self {
22            StoredKey::String(s) => {
23                let bytes = BASE64_STANDARD_NO_PAD
24                    .decode(s)
25                    .map_err(DecodeError::Base64)?;
26                let bytes: [u8; 32] = bytes
27                    .try_into()
28                    .map_err(|_| DecodeError::Ed25519("Invalid slice length".to_string()))?;
29                Ok(bytes)
30            }
31        }
32    }
33
34    pub fn encode(key: &[u8; 32]) -> String {
35        BASE64_STANDARD_NO_PAD.encode(key)
36    }
37}
38
39impl From<String> for StoredKey {
40    fn from(value: String) -> Self {
41        StoredKey::String(value)
42    }
43}
44
45#[derive(Debug, Clone, thiserror::Error)]
46pub enum DecodeError {
47    #[error("Ed25519 error: {0}")]
48    Ed25519(String),
49    #[error("Base64 error: {0}")]
50    Base64(base64::DecodeError),
51}
52
53impl From<DecodeError> for JsValue {
54    fn from(err: DecodeError) -> Self {
55        JsValue::from_str(&err.to_string())
56    }
57}
58
59/// Trait for persisting user authentication data.
60pub trait AuthClientStorage {
61    fn get<T: AsRef<str>>(&mut self, key: T) -> impl Future<Output = Option<StoredKey>>;
62
63    fn set<S: AsRef<str>, T: AsRef<str>>(
64        &mut self,
65        key: S,
66        value: T,
67    ) -> impl Future<Output = Result<(), ()>>;
68
69    fn remove<T: AsRef<str>>(&mut self, key: T) -> impl Future<Output = Result<(), ()>>;
70}
71
72/// Implementation of [`AuthClientStorage`].
73#[derive(Debug, Default, Clone, Copy)]
74pub struct LocalStorage;
75
76impl LocalStorage {
77    pub fn new() -> Self {
78        LocalStorage
79    }
80
81    fn get_local_storage(&self) -> Option<Storage> {
82        match gloo_utils::window().local_storage() {
83            Ok(storage) => storage,
84            Err(_e) => {
85                #[cfg(feature = "tracing")]
86                error!("Could not find local storage: {_e:?}");
87                None
88            }
89        }
90    }
91}
92
93impl AuthClientStorage for LocalStorage {
94    async fn get<T: AsRef<str>>(&mut self, key: T) -> Option<StoredKey> {
95        let local_storage = self.get_local_storage()?;
96        let key = format!("{}{}", LOCAL_STORAGE_PREFIX, key.as_ref());
97        let value = match local_storage.get_item(&key) {
98            Ok(value) => value,
99            Err(_e) => {
100                #[cfg(feature = "tracing")]
101                error!("Could not get item from local storage: {_e:?}");
102                return None;
103            }
104        };
105        value.map(StoredKey::String)
106    }
107
108    async fn set<S: AsRef<str>, T: AsRef<str>>(&mut self, key: S, value: T) -> Result<(), ()> {
109        let local_storage = match self.get_local_storage() {
110            Some(local_storage) => local_storage,
111            None => return Err(()),
112        };
113        let key = format!("{}{}", LOCAL_STORAGE_PREFIX, key.as_ref());
114        match local_storage.set_item(&key, value.as_ref()) {
115            Ok(_) => Ok(()),
116            Err(_) => {
117                #[cfg(feature = "tracing")]
118                error!("Could not set item in local storage");
119                Err(())
120            }
121        }
122    }
123
124    async fn remove<T: AsRef<str>>(&mut self, key: T) -> Result<(), ()> {
125        let local_storage = match self.get_local_storage() {
126            Some(local_storage) => local_storage,
127            None => return Err(()),
128        };
129        let key = format!("{}{}", LOCAL_STORAGE_PREFIX, key.as_ref());
130        match local_storage.remove_item(&key) {
131            Ok(_) => Ok(()),
132            Err(_) => {
133                #[cfg(feature = "tracing")]
134                error!("Could not remove item from local storage");
135                Err(())
136            }
137        }
138    }
139}
140
141/// Enum for selecting the type of storage to use for [`AuthClient`](super::AuthClient).
142#[derive(Debug, Clone)]
143pub enum AuthClientStorageType {
144    LocalStorage(LocalStorage),
145}
146
147impl Default for AuthClientStorageType {
148    fn default() -> Self {
149        AuthClientStorageType::LocalStorage(LocalStorage::new())
150    }
151}
152
153impl AuthClientStorage for AuthClientStorageType {
154    async fn get<T: AsRef<str>>(&mut self, key: T) -> Option<StoredKey> {
155        match self {
156            AuthClientStorageType::LocalStorage(storage) => storage.get(key).await,
157        }
158    }
159
160    async fn set<S: AsRef<str>, T: AsRef<str>>(&mut self, key: S, value: T) -> Result<(), ()> {
161        match self {
162            AuthClientStorageType::LocalStorage(storage) => storage.set(key, value).await,
163        }
164    }
165
166    async fn remove<T: AsRef<str>>(&mut self, key: T) -> Result<(), ()> {
167        match self {
168            AuthClientStorageType::LocalStorage(storage) => storage.remove(key).await,
169        }
170    }
171}
172
173#[allow(dead_code)]
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use ed25519_dalek::SigningKey;
178    use wasm_bindgen_test::*;
179
180    #[test]
181    fn test_stored_key_encode_decode() {
182        let mut rng = rand::thread_rng();
183        let signing_key = SigningKey::generate(&mut rng);
184        let raw_key = signing_key.to_bytes();
185
186        let encoded = StoredKey::encode(&raw_key);
187        let key = StoredKey::String(encoded);
188        let decoded = key.decode().unwrap();
189        assert_eq!(raw_key, decoded);
190    }
191
192    #[wasm_bindgen_test]
193    async fn test_local_storage() {
194        let mut storage = LocalStorage;
195        storage.set("test", "value").await.unwrap();
196        let value = storage.get("test").await.unwrap();
197        assert_eq!(value, StoredKey::String("value".to_string()));
198        storage.remove("test").await.unwrap();
199        let value = storage.get("test").await;
200        assert_eq!(value, None);
201    }
202
203    #[wasm_bindgen_test]
204    async fn test_auth_client_storage_type() {
205        let mut storage = AuthClientStorageType::LocalStorage(LocalStorage);
206        storage.set("test", "value").await.unwrap();
207        let value = storage.get("test").await.unwrap();
208        assert_eq!(value, StoredKey::String("value".to_string()));
209        storage.remove("test").await.unwrap();
210        let value = storage.get("test").await;
211        assert_eq!(value, None);
212    }
213}