ic_auth_client/
storage.rs

1//! Storage module for managing key storage.
2//!
3//! This module provides utilities for storing and retrieving keys securely.
4//! It includes support for both asynchronous and synchronous storage backends.
5
6use base64::prelude::{BASE64_STANDARD_NO_PAD, Engine as _};
7use thiserror::Error;
8
9#[cfg(feature = "wasm-js")]
10pub mod async_storage;
11#[cfg(feature = "native")]
12pub mod sync_storage;
13
14/// A key for storing the identity key pair.
15pub const KEY_STORAGE_KEY: &str = "identity";
16/// A key for storing the delegation chain.
17pub const KEY_STORAGE_DELEGATION: &str = "delegation";
18pub(crate) const KEY_VECTOR: &str = "iv";
19
20/// Enum for storing different types of keys.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum StoredKey {
23    /// A base64-encoded string representation of a 32-byte key.
24    String(String),
25    /// Raw 32-byte key data.
26    Raw([u8; 32]),
27}
28
29impl StoredKey {
30    /// Decodes the stored key into a 32-byte array.
31    ///
32    /// For `String` variants, decodes from base64. For `Raw` variants, returns the bytes directly.
33    ///
34    /// # Errors
35    ///
36    /// Returns `DecodeError::Base64` if base64 decoding fails.
37    /// Returns `DecodeError::Ed25519` if the decoded data is not exactly 32 bytes.
38    pub fn decode(&self) -> Result<[u8; 32], DecodeError> {
39        match self {
40            StoredKey::String(s) => {
41                let bytes = BASE64_STANDARD_NO_PAD
42                    .decode(s)
43                    .map_err(DecodeError::Base64)?;
44                let bytes: [u8; 32] = bytes
45                    .try_into()
46                    .map_err(|_| DecodeError::Ed25519("Invalid slice length".to_string()))?;
47                Ok(bytes)
48            }
49            StoredKey::Raw(bytes) => Ok(*bytes),
50        }
51    }
52
53    /// Encodes the stored key as a string.
54    ///
55    /// For `String` variants, returns the string directly. For `Raw` variants, encodes as base64.
56    pub fn encode(&self) -> String {
57        match self {
58            StoredKey::String(s) => s.clone(),
59            StoredKey::Raw(bytes) => BASE64_STANDARD_NO_PAD.encode(bytes),
60        }
61    }
62}
63
64impl From<[u8; 32]> for StoredKey {
65    fn from(value: [u8; 32]) -> Self {
66        StoredKey::Raw(value)
67    }
68}
69
70impl TryFrom<Vec<u8>> for StoredKey {
71    type Error = DecodeError;
72
73    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
74        let bytes: [u8; 32] = value
75            .try_into()
76            .map_err(|_| DecodeError::Ed25519("Invalid slice length".to_string()))?;
77        Ok(StoredKey::Raw(bytes))
78    }
79}
80
81impl From<String> for StoredKey {
82    fn from(value: String) -> Self {
83        StoredKey::String(value)
84    }
85}
86
87/// Error type for key decoding operations.
88///
89/// This enum represents the various errors that can occur when decoding
90/// stored keys, including Ed25519-specific errors and base64 decoding errors.
91#[derive(Debug, Clone, thiserror::Error)]
92pub enum DecodeError {
93    /// An error related to Ed25519 key operations.
94    ///
95    /// This variant is used for Ed25519-specific errors, such as invalid
96    /// key lengths or malformed key data.
97    #[error("Ed25519 error: {0}")]
98    Ed25519(String),
99    /// An error that occurred during base64 decoding.
100    ///
101    /// This variant wraps base64 decoding errors that can occur when
102    /// converting string-encoded keys back to binary format.
103    #[error("Base64 error: {0}")]
104    Base64(base64::DecodeError),
105}
106
107/// Error type for storage operations.
108#[derive(Error, Debug)]
109pub enum StorageError {
110    /// An error from the keyring.
111    #[error("Keyring error: {0}")]
112    Keyring(String),
113    /// An error from the web-sys storage.
114    #[error("Web Sys error: {0}")]
115    WebSys(String),
116    /// An error from filesystem storage.
117    #[error("File storage error: {0}")]
118    File(String),
119    /// An error that occurred during decoding.
120    #[error("Decode error: {0}")]
121    Decode(#[from] DecodeError),
122}
123
124#[cfg(feature = "keyring")]
125impl From<keyring::Error> for StorageError {
126    fn from(err: keyring::Error) -> Self {
127        StorageError::Keyring(err.to_string())
128    }
129}
130
131#[cfg(feature = "wasm-js")]
132impl From<web_sys::wasm_bindgen::JsValue> for StorageError {
133    fn from(err: web_sys::wasm_bindgen::JsValue) -> Self {
134        StorageError::WebSys(
135            err.as_string()
136                .unwrap_or_else(|| "unknown websys error".to_string()),
137        )
138    }
139}
140
141impl From<std::io::Error> for StorageError {
142    fn from(err: std::io::Error) -> Self {
143        StorageError::File(err.to_string())
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use ed25519_dalek::SigningKey;
151
152    #[test]
153    fn test_stored_key_encode_decode() {
154        let mut rng = rand::thread_rng();
155        let signing_key = SigningKey::generate(&mut rng);
156        let raw_key = signing_key.to_bytes();
157
158        let encoded = StoredKey::Raw(raw_key).encode();
159        let key = StoredKey::String(encoded);
160        let decoded = key.decode().unwrap();
161        assert_eq!(raw_key, decoded);
162    }
163}