app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 3+++
Mcrates/core/Cargo.toml | 3+++
Mcrates/core/src/crypto/mod.rs | 14++++++++++++++
Acrates/core/src/crypto/types.rs | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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); + } +}