ic_auth_client/
storage.rs1use base64::prelude::{Engine as _, BASE64_STANDARD_NO_PAD};
2use ed25519_consensus::{SigningKey, Error as Ed25519Error};
3use std::future::Future;
4use web_sys::{wasm_bindgen::JsValue, Storage};
5
6pub const KEY_STORAGE_KEY: &str = "identity";
8pub const KEY_STORAGE_DELEGATION: &str = "delegation";
10pub(crate) const KEY_VECTOR: &str = "iv";
11
12const LOCAL_STORAGE_PREFIX: &str = "ic-";
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum StoredKey {
17 String(String),
18}
19
20impl StoredKey {
21 pub fn decode(&self) -> Result<SigningKey, DecodeError> {
22 match self {
23 StoredKey::String(s) => {
24 let bytes = BASE64_STANDARD_NO_PAD.decode(s).map_err(DecodeError::Base64)?;
25 let bytes: [u8; 32] = bytes.try_into().map_err(|_| DecodeError::Ed25519(Ed25519Error::InvalidSliceLength))?;
26 Ok(SigningKey::from(bytes))
27 },
28 }
29 }
30
31 pub fn encode(key: &SigningKey) -> String {
32 BASE64_STANDARD_NO_PAD.encode(key.as_bytes())
33 }
34}
35
36impl From<String> for StoredKey {
37 fn from(value: String) -> Self {
38 StoredKey::String(value)
39 }
40}
41
42#[derive(Debug, Clone, thiserror::Error)]
43pub enum DecodeError {
44 #[error("Ed25519 error: {0}")]
45 Ed25519(Ed25519Error),
46 #[error("Base64 error: {0}")]
47 Base64(base64::DecodeError),
48}
49
50impl From<DecodeError> for JsValue {
51 fn from(err: DecodeError) -> Self {
52 JsValue::from_str(&err.to_string())
53 }
54}
55
56pub trait AuthClientStorage {
58 fn get<T: AsRef<str>>(&mut self, key: T) -> impl Future<Output = Option<StoredKey>>;
59
60 fn set<S: AsRef<str>, T: AsRef<str>>(&mut self, key: S, value: T) -> impl Future<Output = ()>;
61
62 fn remove<T: AsRef<str>>(&mut self, key: T) -> impl Future<Output = ()>;
63}
64
65#[derive(Debug, Default, Clone, Copy)]
67pub struct LocalStorage;
68
69impl LocalStorage {
70 pub fn new() -> Self {
71 LocalStorage
72 }
73
74 fn get_local_storage(&self) -> Result<Storage, JsValue> {
75 if let Some(window) = web_sys::window() {
76 let local_storage = window.local_storage()?;
77 local_storage.ok_or("Could not find local storage.".into())
78 } else {
79 Err("No window found".into())
80 }
81 }
82}
83
84impl AuthClientStorage for LocalStorage {
85 async fn get<T: AsRef<str>>(&mut self, key: T) -> Option<StoredKey> {
86 let local_storage = self.get_local_storage().unwrap();
87 let key = format!("{}{}", LOCAL_STORAGE_PREFIX, key.as_ref());
88 let value = local_storage.get_item(&key).unwrap();
89 value.map(StoredKey::String)
90 }
91
92 async fn set<S: AsRef<str>, T: AsRef<str>>(&mut self, key: S, value: T) {
93 let local_storage = self.get_local_storage().unwrap();
94 let key = format!("{}{}", LOCAL_STORAGE_PREFIX, key.as_ref());
95 local_storage.set_item(&key, value.as_ref()).unwrap();
96 }
97
98 async fn remove<T: AsRef<str>>(&mut self, key: T) {
99 let local_storage = self.get_local_storage().unwrap();
100 let key = format!("{}{}", LOCAL_STORAGE_PREFIX, key.as_ref());
101 local_storage.remove_item(&key).unwrap();
102 }
103}
104
105#[derive(Debug, Clone)]
107pub enum AuthClientStorageType {
108 LocalStorage(LocalStorage),
109}
110
111impl Default for AuthClientStorageType {
112 fn default() -> Self {
113 AuthClientStorageType::LocalStorage(LocalStorage::new())
114 }
115}
116
117impl AuthClientStorage for AuthClientStorageType {
118 async fn get<T: AsRef<str>>(&mut self, key: T) -> Option<StoredKey> {
119 match self {
120 AuthClientStorageType::LocalStorage(storage) => {
121 storage.get(key).await
122 }
123 }
124 }
125
126 async fn set<S: AsRef<str>, T: AsRef<str>>(&mut self, key: S, value: T) {
127 match self {
128 AuthClientStorageType::LocalStorage(storage) => {
129 storage.set(key, value).await
130 }
131 }
132 }
133
134 async fn remove<T: AsRef<str>>(&mut self, key: T) {
135 match self {
136 AuthClientStorageType::LocalStorage(storage) => {
137 storage.remove(key).await
138 }
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use wasm_bindgen_test::*;
147
148 #[test]
149 fn test_stored_key_encode_decode() {
150 let rng = rand::thread_rng();
151 let signing_key = SigningKey::new(rng);
152
153 let encoded = StoredKey::encode(&signing_key);
154 let key = StoredKey::String(encoded);
155 let decoded = key.decode().unwrap();
156 assert_eq!(signing_key.as_bytes(), decoded.as_bytes());
157 }
158
159 #[allow(dead_code)]
160 #[wasm_bindgen_test]
161 async fn test_local_storage() {
162 let mut storage = LocalStorage;
163 storage.set("test", "value").await;
164 let value = storage.get("test").await.unwrap();
165 assert_eq!(value, StoredKey::String("value".to_string()));
166 storage.remove("test").await;
167 let value = storage.get("test").await;
168 assert_eq!(value, None);
169 }
170
171 #[allow(dead_code)]
172 #[wasm_bindgen_test]
173 async fn test_auth_client_storage_type() {
174 let mut storage = AuthClientStorageType::LocalStorage(LocalStorage);
175 storage.set("test", "value").await;
176 let value = storage.get("test").await.unwrap();
177 assert_eq!(value, StoredKey::String("value".to_string()));
178 storage.remove("test").await;
179 let value = storage.get("test").await;
180 assert_eq!(value, None);
181 }
182}