ic_auth_client/
storage.rs1use 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
7pub const KEY_STORAGE_KEY: &str = "identity";
9pub const KEY_STORAGE_DELEGATION: &str = "delegation";
11pub(crate) const KEY_VECTOR: &str = "iv";
12
13const LOCAL_STORAGE_PREFIX: &str = "ic-";
14
15#[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
61pub 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#[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#[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}