ic_auth_client/
storage.rs1use base64::prelude::{BASE64_STANDARD_NO_PAD, Engine as _};
2use std::future::Future;
3use web_sys::{Storage, wasm_bindgen::JsValue};
4
5pub const KEY_STORAGE_KEY: &str = "identity";
7pub const KEY_STORAGE_DELEGATION: &str = "delegation";
9pub(crate) const KEY_VECTOR: &str = "iv";
10
11const LOCAL_STORAGE_PREFIX: &str = "ic-";
12
13#[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
59pub 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#[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#[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}