use base64::prelude::{BASE64_STANDARD_NO_PAD, Engine as _};
use std::future::Future;
#[cfg(feature = "tracing")]
use tracing::error;
use web_sys::{Storage, wasm_bindgen::JsValue};
pub const KEY_STORAGE_KEY: &str = "identity";
pub const KEY_STORAGE_DELEGATION: &str = "delegation";
pub(crate) const KEY_VECTOR: &str = "iv";
const LOCAL_STORAGE_PREFIX: &str = "ic-";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StoredKey {
String(String),
}
impl StoredKey {
pub fn decode(&self) -> Result<[u8; 32], DecodeError> {
match self {
StoredKey::String(s) => {
let bytes = BASE64_STANDARD_NO_PAD
.decode(s)
.map_err(DecodeError::Base64)?;
let bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| DecodeError::Ed25519("Invalid slice length".to_string()))?;
Ok(bytes)
}
}
}
pub fn encode(key: &[u8; 32]) -> String {
BASE64_STANDARD_NO_PAD.encode(key)
}
}
impl From<String> for StoredKey {
fn from(value: String) -> Self {
StoredKey::String(value)
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum DecodeError {
#[error("Ed25519 error: {0}")]
Ed25519(String),
#[error("Base64 error: {0}")]
Base64(base64::DecodeError),
}
impl From<DecodeError> for JsValue {
fn from(err: DecodeError) -> Self {
JsValue::from_str(&err.to_string())
}
}
pub trait AuthClientStorage {
fn get<T: AsRef<str>>(&mut self, key: T) -> impl Future<Output = Option<StoredKey>>;
fn set<S: AsRef<str>, T: AsRef<str>>(
&mut self,
key: S,
value: T,
) -> impl Future<Output = Result<(), ()>>;
fn remove<T: AsRef<str>>(&mut self, key: T) -> impl Future<Output = Result<(), ()>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct LocalStorage;
impl LocalStorage {
pub fn new() -> Self {
LocalStorage
}
fn get_local_storage(&self) -> Option<Storage> {
match web_sys::window()?.local_storage() {
Ok(storage) => storage,
Err(_e) => {
#[cfg(feature = "tracing")]
error!("Could not find local storage: {_e:?}");
None
}
}
}
}
impl AuthClientStorage for LocalStorage {
async fn get<T: AsRef<str>>(&mut self, key: T) -> Option<StoredKey> {
let local_storage = self.get_local_storage()?;
let key = format!("{}{}", LOCAL_STORAGE_PREFIX, key.as_ref());
let value = match local_storage.get_item(&key) {
Ok(value) => value,
Err(_e) => {
#[cfg(feature = "tracing")]
error!("Could not get item from local storage: {_e:?}");
return None;
}
};
value.map(StoredKey::String)
}
async fn set<S: AsRef<str>, T: AsRef<str>>(&mut self, key: S, value: T) -> Result<(), ()> {
let local_storage = match self.get_local_storage() {
Some(local_storage) => local_storage,
None => return Err(()),
};
let key = format!("{}{}", LOCAL_STORAGE_PREFIX, key.as_ref());
match local_storage.set_item(&key, value.as_ref()) {
Ok(_) => Ok(()),
Err(_) => {
#[cfg(feature = "tracing")]
error!("Could not set item in local storage");
Err(())
}
}
}
async fn remove<T: AsRef<str>>(&mut self, key: T) -> Result<(), ()> {
let local_storage = match self.get_local_storage() {
Some(local_storage) => local_storage,
None => return Err(()),
};
let key = format!("{}{}", LOCAL_STORAGE_PREFIX, key.as_ref());
match local_storage.remove_item(&key) {
Ok(_) => Ok(()),
Err(_) => {
#[cfg(feature = "tracing")]
error!("Could not remove item from local storage");
Err(())
}
}
}
}
#[derive(Debug, Clone)]
pub enum AuthClientStorageType {
LocalStorage(LocalStorage),
}
impl Default for AuthClientStorageType {
fn default() -> Self {
AuthClientStorageType::LocalStorage(LocalStorage::new())
}
}
impl AuthClientStorage for AuthClientStorageType {
async fn get<T: AsRef<str>>(&mut self, key: T) -> Option<StoredKey> {
match self {
AuthClientStorageType::LocalStorage(storage) => storage.get(key).await,
}
}
async fn set<S: AsRef<str>, T: AsRef<str>>(&mut self, key: S, value: T) -> Result<(), ()> {
match self {
AuthClientStorageType::LocalStorage(storage) => storage.set(key, value).await,
}
}
async fn remove<T: AsRef<str>>(&mut self, key: T) -> Result<(), ()> {
match self {
AuthClientStorageType::LocalStorage(storage) => storage.remove(key).await,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ic_ed25519::PrivateKey;
use wasm_bindgen_test::*;
#[test]
fn test_stored_key_encode_decode() {
let private_key = PrivateKey::generate();
let raw_key = private_key.serialize_raw();
let encoded = StoredKey::encode(&raw_key);
let key = StoredKey::String(encoded);
let decoded = key.decode().unwrap();
assert_eq!(raw_key, decoded);
}
#[allow(dead_code)]
#[wasm_bindgen_test]
async fn test_local_storage() {
let mut storage = LocalStorage;
storage.set("test", "value").await.unwrap();
let value = storage.get("test").await.unwrap();
assert_eq!(value, StoredKey::String("value".to_string()));
storage.remove("test").await.unwrap();
let value = storage.get("test").await;
assert_eq!(value, None);
}
#[allow(dead_code)]
#[wasm_bindgen_test]
async fn test_auth_client_storage_type() {
let mut storage = AuthClientStorageType::LocalStorage(LocalStorage);
storage.set("test", "value").await.unwrap();
let value = storage.get("test").await.unwrap();
assert_eq!(value, StoredKey::String("value".to_string()));
storage.remove("test").await.unwrap();
let value = storage.get("test").await;
assert_eq!(value, None);
}
}