commit 92d30b7cbc8dd9d20c3efa1d345df5bad499a32e
parent 426fba9fdf84d95105d6c055e9c55cbf34358aa0
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 01:55:10 +0000
app-core: add web crypto service
- implement web crypto service config and store registry logic
- add key rotation, envelope encrypt/decrypt, and legacy fallback
- wire registry export/import and provider integration
- add unit tests for store config merging
Diffstat:
2 files changed, 631 insertions(+), 0 deletions(-)
diff --git a/crates/core/src/crypto/mod.rs b/crates/core/src/crypto/mod.rs
@@ -6,6 +6,7 @@ pub mod keys;
pub mod kdf;
pub mod registry;
pub mod provider;
+pub mod service;
pub use error::{RadrootsClientCryptoError, RadrootsClientCryptoErrorMessage};
pub use types::{
@@ -22,6 +23,10 @@ pub use types::{
RadrootsClientWebCryptoService,
};
pub use provider::RadrootsClientDeviceKeyMaterialProvider;
+pub use service::{
+ RadrootsClientWebCryptoServiceConfig,
+ RadrootsClientWebCryptoServiceImpl,
+};
pub use envelope::{crypto_envelope_decode, crypto_envelope_encode};
pub use keys::crypto_key_id_create;
pub use kdf::{crypto_kdf_iterations_default, crypto_kdf_salt_create};
diff --git a/crates/core/src/crypto/service.rs b/crates/core/src/crypto/service.rs
@@ -0,0 +1,626 @@
+use std::cell::RefCell;
+use std::collections::HashMap;
+
+use async_trait::async_trait;
+
+use crate::crypto::{crypto_registry_export, crypto_registry_import};
+
+#[cfg(target_arch = "wasm32")]
+use crate::crypto::{
+ crypto_envelope_decode,
+ crypto_envelope_encode,
+ crypto_kdf_derive_kek,
+ crypto_kdf_iterations_default,
+ crypto_kdf_salt_create,
+ crypto_key_export_raw,
+ crypto_key_generate,
+ crypto_key_id_create,
+ crypto_key_import_raw,
+ crypto_key_unwrap,
+ crypto_key_wrap,
+ crypto_registry_get_key_entry,
+ crypto_registry_get_store_index,
+ crypto_registry_set_key_entry,
+ crypto_registry_set_store_index,
+};
+#[cfg(target_arch = "wasm32")]
+use crate::crypto::random::fill_random;
+#[cfg(target_arch = "wasm32")]
+use crate::idb::{idb_get, idb_store_ensure, idb_store_exists, idb_value_as_bytes};
+
+use super::{
+ RadrootsClientCryptoDecryptOutcome,
+ RadrootsClientCryptoError,
+ RadrootsClientCryptoRegistryExport,
+ RadrootsClientCryptoStoreConfig,
+ RadrootsClientKeyMaterialProvider,
+ RadrootsClientWebCryptoService,
+};
+use super::provider::RadrootsClientDeviceKeyMaterialProvider;
+
+#[cfg(target_arch = "wasm32")]
+use super::{
+ RadrootsClientCryptoAlgorithm,
+ RadrootsClientCryptoEnvelope,
+ RadrootsClientCryptoKeyEntry,
+ RadrootsClientCryptoKeyStatus,
+ RadrootsClientCryptoStoreIndex,
+ RadrootsClientLegacyKeyConfig,
+};
+
+const DEFAULT_IV_LENGTH: u32 = 12;
+#[cfg(target_arch = "wasm32")]
+const DEFAULT_KDF_SALT_BYTES: usize = 16;
+
+pub struct RadrootsClientWebCryptoServiceConfig {
+ pub key_material_provider: Option<Box<dyn RadrootsClientKeyMaterialProvider>>,
+}
+
+pub struct RadrootsClientWebCryptoServiceImpl {
+ store_configs: RefCell<HashMap<String, RadrootsClientCryptoStoreConfig>>,
+ #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
+ key_material_provider: Box<dyn RadrootsClientKeyMaterialProvider>,
+}
+
+impl RadrootsClientWebCryptoServiceImpl {
+ pub fn new(config: Option<RadrootsClientWebCryptoServiceConfig>) -> Self {
+ let provider = config
+ .and_then(|config| config.key_material_provider)
+ .unwrap_or_else(|| Box::new(RadrootsClientDeviceKeyMaterialProvider));
+ Self {
+ store_configs: RefCell::new(HashMap::new()),
+ key_material_provider: provider,
+ }
+ }
+
+ #[cfg(any(test, target_arch = "wasm32"))]
+ fn resolve_store_config(&self, store_id: &str) -> RadrootsClientCryptoStoreConfig {
+ if let Some(existing) = self.store_configs.borrow().get(store_id) {
+ return existing.clone();
+ }
+ let config = RadrootsClientCryptoStoreConfig {
+ store_id: store_id.to_string(),
+ legacy_key: None,
+ iv_length: Some(DEFAULT_IV_LENGTH),
+ };
+ self.store_configs
+ .borrow_mut()
+ .insert(store_id.to_string(), config.clone());
+ config
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ async fn resolve_active_key(
+ &self,
+ store_id: &str,
+ ) -> Result<ResolvedKey, RadrootsClientCryptoError> {
+ let index = crypto_registry_get_store_index(store_id).await?;
+ let config = self.resolve_store_config(store_id);
+ let Some(index) = index else {
+ return self.create_store_key(store_id, &config).await;
+ };
+ let entry = crypto_registry_get_key_entry(&index.active_key_id).await?;
+ let Some(entry) = entry else {
+ return self.create_store_key(store_id, &config).await;
+ };
+ let key = self.unwrap_key_entry(&entry).await?;
+ Ok(ResolvedKey { key, entry, index })
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ async fn resolve_key_by_id(
+ &self,
+ store_id: &str,
+ key_id: &str,
+ ) -> Result<ResolvedKey, RadrootsClientCryptoError> {
+ let entry = crypto_registry_get_key_entry(key_id).await?;
+ let Some(entry) = entry else {
+ return Err(RadrootsClientCryptoError::KeyNotFound);
+ };
+ let index = match crypto_registry_get_store_index(store_id).await? {
+ Some(index) => index,
+ None => {
+ let next_index = RadrootsClientCryptoStoreIndex {
+ store_id: store_id.to_string(),
+ active_key_id: entry.key_id.clone(),
+ key_ids: vec![entry.key_id.clone()],
+ created_at: entry.created_at,
+ };
+ crypto_registry_set_store_index(next_index.clone()).await?;
+ next_index
+ }
+ };
+ let key = self.unwrap_key_entry(&entry).await?;
+ Ok(ResolvedKey {
+ key,
+ entry,
+ index,
+ })
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ async fn create_store_key(
+ &self,
+ store_id: &str,
+ config: &RadrootsClientCryptoStoreConfig,
+ ) -> Result<ResolvedKey, RadrootsClientCryptoError> {
+ let created = self.create_key_entry(store_id, config).await?;
+ let index = RadrootsClientCryptoStoreIndex {
+ store_id: store_id.to_string(),
+ active_key_id: created.entry.key_id.clone(),
+ key_ids: vec![created.entry.key_id.clone()],
+ created_at: created.entry.created_at,
+ };
+ crypto_registry_set_store_index(index.clone()).await?;
+ Ok(ResolvedKey {
+ key: created.key,
+ entry: created.entry,
+ index,
+ })
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ async fn create_key_entry(
+ &self,
+ store_id: &str,
+ config: &RadrootsClientCryptoStoreConfig,
+ ) -> Result<CreatedKey, RadrootsClientCryptoError> {
+ let key_id = crypto_key_id_create()?;
+ let created_at = js_sys::Date::now() as u64;
+ let kdf_salt = crypto_kdf_salt_create(DEFAULT_KDF_SALT_BYTES)?;
+ let kdf_iterations = crypto_kdf_iterations_default();
+ let mut material = self.key_material_provider.get_key_material().await?;
+ let provider_id = self.key_material_provider.get_provider_id().await?;
+ let kek = crypto_kdf_derive_kek(&material, &kdf_salt, kdf_iterations).await?;
+ material.fill(0);
+ let key = crypto_key_generate().await?;
+ let mut raw_key = crypto_key_export_raw(&key).await?;
+ let (wrapped_key, wrap_iv) = crypto_key_wrap(&kek, &mut raw_key).await?;
+ let iv_length = config.iv_length.unwrap_or(DEFAULT_IV_LENGTH);
+ let entry = RadrootsClientCryptoKeyEntry {
+ key_id,
+ store_id: store_id.to_string(),
+ created_at,
+ status: RadrootsClientCryptoKeyStatus::Active,
+ wrapped_key,
+ wrap_iv,
+ kdf_salt,
+ kdf_iterations,
+ iv_length,
+ algorithm: RadrootsClientCryptoAlgorithm::AesGcm,
+ provider_id,
+ };
+ crypto_registry_set_key_entry(entry.clone()).await?;
+ Ok(CreatedKey { key, entry })
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ async fn unwrap_key_entry(
+ &self,
+ entry: &RadrootsClientCryptoKeyEntry,
+ ) -> Result<web_sys::CryptoKey, RadrootsClientCryptoError> {
+ let mut material = self.key_material_provider.get_key_material().await?;
+ let kek = crypto_kdf_derive_kek(&material, &entry.kdf_salt, entry.kdf_iterations).await?;
+ material.fill(0);
+ crypto_key_unwrap(&kek, &entry.wrapped_key, &entry.wrap_iv).await
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ async fn decrypt_envelope(
+ &self,
+ store_id: &str,
+ envelope: RadrootsClientCryptoEnvelope,
+ ) -> Result<RadrootsClientCryptoDecryptOutcome, RadrootsClientCryptoError> {
+ let resolved = self.resolve_key_by_id(store_id, &envelope.key_id).await?;
+ let plaintext =
+ decrypt_bytes(&resolved.key, &envelope.iv, &envelope.ciphertext).await?;
+ let needs_reencrypt = resolved.index.active_key_id != envelope.key_id;
+ if !needs_reencrypt {
+ return Ok(RadrootsClientCryptoDecryptOutcome {
+ plaintext,
+ needs_reencrypt,
+ reencrypted: None,
+ });
+ }
+ let reencrypted = self.encrypt(store_id, &plaintext).await?;
+ Ok(RadrootsClientCryptoDecryptOutcome {
+ plaintext,
+ needs_reencrypt,
+ reencrypted: Some(reencrypted),
+ })
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ async fn decrypt_legacy(
+ &self,
+ store_id: &str,
+ blob: &[u8],
+ legacy_key: Option<RadrootsClientLegacyKeyConfig>,
+ iv_length: u32,
+ ) -> Result<RadrootsClientCryptoDecryptOutcome, RadrootsClientCryptoError> {
+ let Some(legacy_key) = legacy_key else {
+ return Err(RadrootsClientCryptoError::LegacyKeyMissing);
+ };
+ let legacy_crypto_key = self.load_legacy_key(&legacy_key).await?;
+ let Some(legacy_crypto_key) = legacy_crypto_key else {
+ return Err(RadrootsClientCryptoError::LegacyKeyMissing);
+ };
+ let iv_len = iv_length as usize;
+ if blob.len() <= iv_len {
+ return Err(RadrootsClientCryptoError::InvalidEnvelope);
+ }
+ let iv = &blob[..iv_len];
+ let ciphertext = &blob[iv_len..];
+ let plaintext = decrypt_bytes_with_algorithm(
+ &legacy_crypto_key,
+ &legacy_key.algorithm,
+ iv,
+ ciphertext,
+ )
+ .await?;
+ let reencrypted = self.encrypt(store_id, &plaintext).await?;
+ Ok(RadrootsClientCryptoDecryptOutcome {
+ plaintext,
+ needs_reencrypt: true,
+ reencrypted: Some(reencrypted),
+ })
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ async fn load_legacy_key(
+ &self,
+ legacy: &RadrootsClientLegacyKeyConfig,
+ ) -> Result<Option<web_sys::CryptoKey>, RadrootsClientCryptoError> {
+ let exists = idb_store_exists(legacy.idb_config.database, legacy.idb_config.store)
+ .await
+ .map_err(map_idb_error)?;
+ if !exists {
+ return Ok(None);
+ }
+ idb_store_ensure(legacy.idb_config.database, legacy.idb_config.store)
+ .await
+ .map_err(map_idb_error)?;
+ let stored = idb_get(
+ legacy.idb_config.database,
+ legacy.idb_config.store,
+ &legacy.key_name,
+ )
+ .await
+ .map_err(map_idb_error)?;
+ let Some(stored) = stored else {
+ return Ok(None);
+ };
+ if let Ok(key) = stored.clone().dyn_into::<web_sys::CryptoKey>() {
+ return Ok(Some(key));
+ }
+ let Some(bytes) = idb_value_as_bytes(&stored) else {
+ return Ok(None);
+ };
+ crypto_key_import_raw(&bytes).await.map(Some)
+ }
+}
+
+impl Default for RadrootsClientWebCryptoServiceImpl {
+ fn default() -> Self {
+ Self::new(None)
+ }
+}
+
+#[async_trait(?Send)]
+impl RadrootsClientWebCryptoService for RadrootsClientWebCryptoServiceImpl {
+ fn register_store_config(&mut self, config: RadrootsClientCryptoStoreConfig) {
+ let store_id = config.store_id.clone();
+ let mut configs = self.store_configs.borrow_mut();
+ if let Some(existing) = configs.get(&store_id).cloned() {
+ configs.insert(
+ store_id,
+ RadrootsClientCryptoStoreConfig {
+ store_id: config.store_id,
+ iv_length: config.iv_length.or(existing.iv_length),
+ legacy_key: config.legacy_key.or_else(|| existing.legacy_key.clone()),
+ },
+ );
+ return;
+ }
+ configs.insert(
+ store_id,
+ RadrootsClientCryptoStoreConfig {
+ store_id: config.store_id,
+ iv_length: Some(config.iv_length.unwrap_or(DEFAULT_IV_LENGTH)),
+ legacy_key: config.legacy_key,
+ },
+ );
+ }
+
+ async fn encrypt(
+ &self,
+ store_id: &str,
+ plaintext: &[u8],
+ ) -> Result<Vec<u8>, RadrootsClientCryptoError> {
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ let _ = (store_id, plaintext);
+ return Err(RadrootsClientCryptoError::CryptoUndefined);
+ }
+ #[cfg(target_arch = "wasm32")]
+ {
+ let resolved = self.resolve_active_key(store_id).await?;
+ let iv_length = if resolved.entry.iv_length == 0 {
+ DEFAULT_IV_LENGTH
+ } else {
+ resolved.entry.iv_length
+ };
+ let mut iv = vec![0u8; iv_length as usize];
+ fill_random(&mut iv)?;
+ let ciphertext = encrypt_bytes(&resolved.key, &iv, plaintext).await?;
+ let envelope = RadrootsClientCryptoEnvelope {
+ version: 1,
+ key_id: resolved.entry.key_id.clone(),
+ iv,
+ created_at: js_sys::Date::now() as u64,
+ ciphertext,
+ };
+ crypto_envelope_encode(&envelope)
+ }
+ }
+
+ async fn decrypt(
+ &self,
+ store_id: &str,
+ blob: &[u8],
+ ) -> Result<Vec<u8>, RadrootsClientCryptoError> {
+ let outcome = self.decrypt_record(store_id, blob).await?;
+ Ok(outcome.plaintext)
+ }
+
+ async fn decrypt_record(
+ &self,
+ store_id: &str,
+ blob: &[u8],
+ ) -> Result<RadrootsClientCryptoDecryptOutcome, RadrootsClientCryptoError> {
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ let _ = (store_id, blob);
+ return Err(RadrootsClientCryptoError::CryptoUndefined);
+ }
+ #[cfg(target_arch = "wasm32")]
+ {
+ let config = self.resolve_store_config(store_id);
+ let envelope = crypto_envelope_decode(blob)?;
+ if let Some(envelope) = envelope {
+ return self.decrypt_envelope(store_id, envelope).await;
+ }
+ let iv_length = config.iv_length.unwrap_or(DEFAULT_IV_LENGTH);
+ return self
+ .decrypt_legacy(store_id, blob, config.legacy_key, iv_length)
+ .await;
+ }
+ }
+
+ async fn rotate_store_key(
+ &self,
+ store_id: &str,
+ ) -> Result<String, RadrootsClientCryptoError> {
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ let _ = store_id;
+ return Err(RadrootsClientCryptoError::CryptoUndefined);
+ }
+ #[cfg(target_arch = "wasm32")]
+ {
+ let config = self.resolve_store_config(store_id);
+ let index = match crypto_registry_get_store_index(store_id).await? {
+ Some(index) => index,
+ None => {
+ let created = self.create_store_key(store_id, &config).await?;
+ return Ok(created.entry.key_id);
+ }
+ };
+ if let Some(entry) = crypto_registry_get_key_entry(&index.active_key_id).await? {
+ let rotated = RadrootsClientCryptoKeyEntry {
+ status: RadrootsClientCryptoKeyStatus::Rotated,
+ ..entry
+ };
+ crypto_registry_set_key_entry(rotated).await?;
+ }
+ let created = self.create_key_entry(store_id, &config).await?;
+ let next_index = RadrootsClientCryptoStoreIndex {
+ store_id: index.store_id,
+ active_key_id: created.entry.key_id.clone(),
+ key_ids: merge_key_ids(&index.key_ids, &created.entry.key_id),
+ created_at: index.created_at,
+ };
+ crypto_registry_set_store_index(next_index).await?;
+ Ok(created.entry.key_id)
+ }
+ }
+
+ async fn export_registry(
+ &self,
+ ) -> Result<RadrootsClientCryptoRegistryExport, RadrootsClientCryptoError> {
+ crypto_registry_export().await
+ }
+
+ async fn import_registry(
+ &self,
+ registry: RadrootsClientCryptoRegistryExport,
+ ) -> Result<(), RadrootsClientCryptoError> {
+ crypto_registry_import(registry).await
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+struct ResolvedKey {
+ key: web_sys::CryptoKey,
+ entry: RadrootsClientCryptoKeyEntry,
+ index: RadrootsClientCryptoStoreIndex,
+}
+
+#[cfg(target_arch = "wasm32")]
+struct CreatedKey {
+ key: web_sys::CryptoKey,
+ entry: RadrootsClientCryptoKeyEntry,
+}
+
+#[cfg(target_arch = "wasm32")]
+fn merge_key_ids(current: &[String], next_key_id: &str) -> Vec<String> {
+ if current.iter().any(|key_id| key_id == next_key_id) {
+ return current.to_vec();
+ }
+ let mut merged = current.to_vec();
+ merged.push(next_key_id.to_string());
+ merged
+}
+
+#[cfg(target_arch = "wasm32")]
+fn map_idb_error(err: crate::idb::RadrootsClientIdbStoreError) -> RadrootsClientCryptoError {
+ match err {
+ crate::idb::RadrootsClientIdbStoreError::IdbUndefined => {
+ RadrootsClientCryptoError::IdbUndefined
+ }
+ _ => RadrootsClientCryptoError::RegistryFailure,
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+fn subtle_crypto() -> Result<web_sys::SubtleCrypto, RadrootsClientCryptoError> {
+ let window = web_sys::window().ok_or(RadrootsClientCryptoError::CryptoUndefined)?;
+ let crypto = window
+ .crypto()
+ .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
+ Ok(crypto.subtle())
+}
+
+#[cfg(target_arch = "wasm32")]
+fn aes_gcm_params(iv: &[u8]) -> Result<js_sys::Object, RadrootsClientCryptoError> {
+ let algo = js_sys::Object::new();
+ js_sys::Reflect::set(&algo, &"name".into(), &"AES-GCM".into())
+ .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
+ let iv_array = js_sys::Uint8Array::from(iv);
+ js_sys::Reflect::set(&algo, &"iv".into(), &iv_array.into())
+ .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
+ Ok(algo)
+}
+
+#[cfg(target_arch = "wasm32")]
+fn algorithm_params(
+ name: &str,
+ iv: &[u8],
+) -> Result<js_sys::Object, RadrootsClientCryptoError> {
+ let algo = js_sys::Object::new();
+ js_sys::Reflect::set(&algo, &"name".into(), &name.into())
+ .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
+ let iv_array = js_sys::Uint8Array::from(iv);
+ js_sys::Reflect::set(&algo, &"iv".into(), &iv_array.into())
+ .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
+ Ok(algo)
+}
+
+#[cfg(target_arch = "wasm32")]
+async fn encrypt_bytes(
+ key: &web_sys::CryptoKey,
+ iv: &[u8],
+ plaintext: &[u8],
+) -> Result<Vec<u8>, RadrootsClientCryptoError> {
+ let subtle = subtle_crypto()?;
+ let algo = aes_gcm_params(iv)?;
+ let promise = subtle
+ .encrypt_with_object_and_u8_array(&algo, key, plaintext)
+ .map_err(|_| RadrootsClientCryptoError::EncryptFailure)?;
+ let value = wasm_bindgen_futures::JsFuture::from(promise)
+ .await
+ .map_err(|_| RadrootsClientCryptoError::EncryptFailure)?;
+ let array = js_sys::Uint8Array::new(&value);
+ let mut out = vec![0u8; array.length() as usize];
+ array.copy_to(&mut out);
+ Ok(out)
+}
+
+#[cfg(target_arch = "wasm32")]
+async fn decrypt_bytes(
+ key: &web_sys::CryptoKey,
+ iv: &[u8],
+ ciphertext: &[u8],
+) -> Result<Vec<u8>, RadrootsClientCryptoError> {
+ let subtle = subtle_crypto()?;
+ let algo = aes_gcm_params(iv)?;
+ let promise = subtle
+ .decrypt_with_object_and_u8_array(&algo, key, ciphertext)
+ .map_err(|_| RadrootsClientCryptoError::DecryptFailure)?;
+ let value = wasm_bindgen_futures::JsFuture::from(promise)
+ .await
+ .map_err(|_| RadrootsClientCryptoError::DecryptFailure)?;
+ let array = js_sys::Uint8Array::new(&value);
+ let mut out = vec![0u8; array.length() as usize];
+ array.copy_to(&mut out);
+ Ok(out)
+}
+
+#[cfg(target_arch = "wasm32")]
+async fn decrypt_bytes_with_algorithm(
+ key: &web_sys::CryptoKey,
+ algorithm: &str,
+ iv: &[u8],
+ ciphertext: &[u8],
+) -> Result<Vec<u8>, RadrootsClientCryptoError> {
+ let subtle = subtle_crypto()?;
+ let algo = algorithm_params(algorithm, iv)?;
+ let promise = subtle
+ .decrypt_with_object_and_u8_array(&algo, key, ciphertext)
+ .map_err(|_| RadrootsClientCryptoError::DecryptFailure)?;
+ let value = wasm_bindgen_futures::JsFuture::from(promise)
+ .await
+ .map_err(|_| RadrootsClientCryptoError::DecryptFailure)?;
+ let array = js_sys::Uint8Array::new(&value);
+ let mut out = vec![0u8; array.length() as usize];
+ array.copy_to(&mut out);
+ Ok(out)
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::crypto::{
+ RadrootsClientCryptoStoreConfig,
+ RadrootsClientLegacyKeyConfig,
+ RadrootsClientWebCryptoService,
+ };
+ use crate::idb::RadrootsClientIdbConfig;
+
+ use super::{RadrootsClientWebCryptoServiceImpl, DEFAULT_IV_LENGTH};
+
+ #[test]
+ fn register_store_config_defaults_iv_length() {
+ let mut service = RadrootsClientWebCryptoServiceImpl::default();
+ service.register_store_config(RadrootsClientCryptoStoreConfig {
+ store_id: "store".to_string(),
+ legacy_key: None,
+ iv_length: None,
+ });
+ let config = service.resolve_store_config("store");
+ assert_eq!(config.iv_length, Some(DEFAULT_IV_LENGTH));
+ }
+
+ #[test]
+ fn register_store_config_merges_updates() {
+ let mut service = RadrootsClientWebCryptoServiceImpl::default();
+ service.register_store_config(RadrootsClientCryptoStoreConfig {
+ store_id: "store".to_string(),
+ legacy_key: None,
+ iv_length: Some(16),
+ });
+ let legacy = RadrootsClientLegacyKeyConfig {
+ idb_config: RadrootsClientIdbConfig::new("db", "store"),
+ key_name: "key".to_string(),
+ iv_length: 12,
+ algorithm: "AES-GCM".to_string(),
+ };
+ service.register_store_config(RadrootsClientCryptoStoreConfig {
+ store_id: "store".to_string(),
+ legacy_key: Some(legacy.clone()),
+ iv_length: None,
+ });
+ let config = service.resolve_store_config("store");
+ assert_eq!(config.iv_length, Some(16));
+ assert_eq!(config.legacy_key, Some(legacy));
+ }
+}