commit bc6f91db370f9facf64edff67ff0a5f893fe96ee
parent 406dbf24d0fd0721314d4c03e6e0f27490077f41
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 00:33:20 +0000
app-core: add crypto types and traits
- define crypto envelopes, key entries, and registry types
- add status and algorithm helpers with parse methods
- introduce async crypto service and key material traits
- add async-trait dependency and unit tests for helpers
Diffstat:
4 files changed, 197 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1148,6 +1148,9 @@ dependencies = [
[[package]]
name = "radroots-app-core"
version = "0.1.0"
+dependencies = [
+ "async-trait",
+]
[[package]]
name = "reactive_graph"
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
@@ -8,3 +8,6 @@ rust-version.workspace = true
[lib]
crate-type = ["rlib"]
+
+[dependencies]
+async-trait = "0.1.83"
diff --git a/crates/core/src/crypto/mod.rs b/crates/core/src/crypto/mod.rs
@@ -1,3 +1,17 @@
pub mod error;
+pub mod types;
pub use error::{RadrootsClientCryptoError, RadrootsClientCryptoErrorMessage};
+pub use types::{
+ RadrootsClientCryptoAlgorithm,
+ RadrootsClientCryptoDecryptOutcome,
+ RadrootsClientCryptoEnvelope,
+ RadrootsClientCryptoKeyEntry,
+ RadrootsClientCryptoKeyStatus,
+ RadrootsClientCryptoRegistryExport,
+ RadrootsClientCryptoStoreConfig,
+ RadrootsClientCryptoStoreIndex,
+ RadrootsClientKeyMaterialProvider,
+ RadrootsClientLegacyKeyConfig,
+ RadrootsClientWebCryptoService,
+};
diff --git a/crates/core/src/crypto/types.rs b/crates/core/src/crypto/types.rs
@@ -0,0 +1,177 @@
+use async_trait::async_trait;
+
+use crate::idb::RadrootsClientIdbConfig;
+
+use super::RadrootsClientCryptoError;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsClientCryptoKeyStatus {
+ Active,
+ Rotated,
+}
+
+impl RadrootsClientCryptoKeyStatus {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ RadrootsClientCryptoKeyStatus::Active => "active",
+ RadrootsClientCryptoKeyStatus::Rotated => "rotated",
+ }
+ }
+
+ pub fn parse(value: &str) -> Option<Self> {
+ match value {
+ "active" => Some(RadrootsClientCryptoKeyStatus::Active),
+ "rotated" => Some(RadrootsClientCryptoKeyStatus::Rotated),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsClientCryptoAlgorithm {
+ AesGcm,
+}
+
+impl RadrootsClientCryptoAlgorithm {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ RadrootsClientCryptoAlgorithm::AesGcm => "AES-GCM",
+ }
+ }
+
+ pub fn parse(value: &str) -> Option<Self> {
+ match value {
+ "AES-GCM" => Some(RadrootsClientCryptoAlgorithm::AesGcm),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsClientCryptoEnvelope {
+ pub version: u8,
+ pub key_id: String,
+ pub iv: Vec<u8>,
+ pub created_at: u64,
+ pub ciphertext: Vec<u8>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsClientCryptoKeyEntry {
+ pub key_id: String,
+ pub store_id: String,
+ pub created_at: u64,
+ pub status: RadrootsClientCryptoKeyStatus,
+ pub wrapped_key: Vec<u8>,
+ pub wrap_iv: Vec<u8>,
+ pub kdf_salt: Vec<u8>,
+ pub kdf_iterations: u32,
+ pub iv_length: u32,
+ pub algorithm: RadrootsClientCryptoAlgorithm,
+ pub provider_id: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsClientCryptoStoreIndex {
+ pub store_id: String,
+ pub active_key_id: String,
+ pub key_ids: Vec<String>,
+ pub created_at: u64,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsClientCryptoRegistryExport {
+ pub stores: Vec<RadrootsClientCryptoStoreIndex>,
+ pub keys: Vec<RadrootsClientCryptoKeyEntry>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsClientCryptoDecryptOutcome {
+ pub plaintext: Vec<u8>,
+ pub needs_reencrypt: bool,
+ pub reencrypted: Option<Vec<u8>>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsClientLegacyKeyConfig {
+ pub idb_config: RadrootsClientIdbConfig,
+ pub key_name: String,
+ pub iv_length: u32,
+ pub algorithm: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsClientCryptoStoreConfig {
+ pub store_id: String,
+ pub legacy_key: Option<RadrootsClientLegacyKeyConfig>,
+ pub iv_length: Option<u32>,
+}
+
+#[async_trait(?Send)]
+pub trait RadrootsClientKeyMaterialProvider {
+ async fn get_key_material(&self) -> Result<Vec<u8>, RadrootsClientCryptoError>;
+ async fn get_provider_id(&self) -> Result<String, RadrootsClientCryptoError>;
+}
+
+#[async_trait(?Send)]
+pub trait RadrootsClientWebCryptoService {
+ fn register_store_config(&mut self, config: RadrootsClientCryptoStoreConfig);
+
+ async fn encrypt(
+ &self,
+ store_id: &str,
+ plaintext: &[u8],
+ ) -> Result<Vec<u8>, RadrootsClientCryptoError>;
+
+ async fn decrypt(
+ &self,
+ store_id: &str,
+ blob: &[u8],
+ ) -> Result<Vec<u8>, RadrootsClientCryptoError>;
+
+ async fn decrypt_record(
+ &self,
+ store_id: &str,
+ blob: &[u8],
+ ) -> Result<RadrootsClientCryptoDecryptOutcome, RadrootsClientCryptoError>;
+
+ async fn rotate_store_key(&self, store_id: &str) -> Result<String, RadrootsClientCryptoError>;
+
+ async fn export_registry(
+ &self,
+ ) -> Result<RadrootsClientCryptoRegistryExport, RadrootsClientCryptoError>;
+
+ async fn import_registry(
+ &self,
+ registry: RadrootsClientCryptoRegistryExport,
+ ) -> Result<(), RadrootsClientCryptoError>;
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{RadrootsClientCryptoAlgorithm, RadrootsClientCryptoKeyStatus};
+
+ #[test]
+ fn key_status_roundtrip() {
+ let active = RadrootsClientCryptoKeyStatus::Active;
+ let rotated = RadrootsClientCryptoKeyStatus::Rotated;
+
+ assert_eq!(active.as_str(), "active");
+ assert_eq!(rotated.as_str(), "rotated");
+ assert_eq!(RadrootsClientCryptoKeyStatus::parse("active"), Some(active));
+ assert_eq!(RadrootsClientCryptoKeyStatus::parse("rotated"), Some(rotated));
+ assert_eq!(RadrootsClientCryptoKeyStatus::parse("unknown"), None);
+ }
+
+ #[test]
+ fn algorithm_roundtrip() {
+ let algo = RadrootsClientCryptoAlgorithm::AesGcm;
+
+ assert_eq!(algo.as_str(), "AES-GCM");
+ assert_eq!(
+ RadrootsClientCryptoAlgorithm::parse("AES-GCM"),
+ Some(algo)
+ );
+ assert_eq!(RadrootsClientCryptoAlgorithm::parse("AES-CBC"), None);
+ }
+}