ic_auth_client/
storage.rs

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