ic_auth_client/
storage.rs

1use 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
6/// A key for storing the identity key pair.
7pub const KEY_STORAGE_KEY: &str = "identity";
8/// A key for storing the delegation chain.
9pub const KEY_STORAGE_DELEGATION: &str = "delegation";
10pub(crate) const KEY_VECTOR: &str = "iv";
11
12const LOCAL_STORAGE_PREFIX: &str = "ic-";
13
14/// Enum for storing different types of keys.
15#[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
56/// Trait for persisting user authentication data.
57pub 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/// Implementation of [`AuthClientStorage`].
66#[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/// Enum for selecting the type of storage to use for [`AuthClient`](super::AuthClient).
106#[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}