myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit 37a3f372119f61a2b8bc36464fc2cf31ea4b714c
parent c5673ea7f0d8708821bb860c6d0fa01bc0b18e37
Author: triesap <tyson@radroots.org>
Date:   Sun, 12 Apr 2026 04:57:54 +0000

custody: use shared file-backed vault helpers

Diffstat:
MCargo.toml | 2+-
Msrc/custody.rs | 11++++-------
Msrc/identity_storage.rs | 138++++---------------------------------------------------------------------------
3 files changed, 12 insertions(+), 139 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -25,7 +25,7 @@ radroots_nostr_accounts = { path = "../lib/crates/nostr_accounts", default-featu radroots_nostr = { path = "../lib/crates/nostr", features = ["client", "events"] } radroots_nostr_connect = { path = "../lib/crates/nostr_connect" } radroots_nostr_signer = { path = "../lib/crates/nostr_signer", features = ["native"] } -radroots_protected_store = { path = "../lib/crates/protected_store" } +radroots_protected_store = { path = "../lib/crates/protected_store", features = ["std"] } radroots_runtime_paths = { path = "../lib/crates/runtime_paths" } radroots_secret_vault = { path = "../lib/crates/secret_vault", features = ["std", "os-keyring"] } radroots_sql_core = { path = "../lib/crates/sql_core", features = ["native"] } diff --git a/src/custody.rs b/src/custody.rs @@ -11,8 +11,7 @@ use radroots_nostr::prelude::{ RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrPublicKey, }; use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountRecord, RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, - RadrootsNostrSelectedAccountStatus, + RadrootsNostrAccountRecord, RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, }; use radroots_secret_vault::{RadrootsSecretVault, RadrootsSecretVaultOsKeyring}; use serde::{Deserialize, Serialize}; @@ -1443,11 +1442,9 @@ impl MycIdentityProvider { source, })?; } - let manager = RadrootsNostrAccountsManager::new( - Arc::new(RadrootsNostrFileAccountStore::new( - account_store_path.as_path(), - )), - Arc::new(RadrootsSecretVaultOsKeyring::new(service_name.clone())), + let manager = RadrootsNostrAccountsManager::new_file_backed_with_vault( + account_store_path.as_path(), + RadrootsSecretVaultOsKeyring::new(service_name.clone()), ) .map_err(|source| MycError::CustodyManager { role: role.to_owned(), diff --git a/src/identity_storage.rs b/src/identity_storage.rs @@ -1,145 +1,19 @@ -use std::ffi::OsString; use std::fs; use std::path::{Path, PathBuf}; -use chacha20poly1305::aead::{Aead, KeyInit, Payload}; -use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; -use getrandom::getrandom; use radroots_identity::{RadrootsIdentity, RadrootsIdentityFile, RadrootsIdentityPublic}; use radroots_protected_store::{ - RADROOTS_PROTECTED_STORE_KEY_LENGTH, RADROOTS_PROTECTED_STORE_NONCE_LENGTH, - RadrootsProtectedStoreEnvelope, + RadrootsProtectedFileKeySource, RadrootsProtectedStoreEnvelope, sidecar_path, }; -use radroots_secret_vault::{RadrootsSecretKeyWrapping, RadrootsSecretVaultAccessError}; -use zeroize::Zeroize; +use radroots_secret_vault::RadrootsSecretVaultAccessError; use crate::error::MycError; const ENCRYPTED_IDENTITY_KEY_SLOT: &str = "myc_identity"; const ENCRYPTED_IDENTITY_KEY_SUFFIX: &str = ".key"; -const WRAPPED_KEY_VERSION: u8 = 1; - -#[derive(Debug, Clone)] -struct MycEncryptedIdentityKeySource { - key_path: PathBuf, -} - -impl MycEncryptedIdentityKeySource { - fn new(path: &Path) -> Self { - Self { - key_path: encrypted_identity_wrapping_key_path(path), - } - } - - fn load_or_create_wrapping_key( - &self, - ) -> Result<[u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], RadrootsSecretVaultAccessError> { - if self.key_path.exists() { - return self.load_wrapping_key(); - } - - if let Some(parent) = self.key_path.parent() - && !parent.as_os_str().is_empty() - { - fs::create_dir_all(parent).map_err(io_backend_error)?; - } - let mut key = [0_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]; - getrandom(&mut key) - .map_err(|_| RadrootsSecretVaultAccessError::Backend("entropy unavailable".into()))?; - fs::write(&self.key_path, key.as_slice()).map_err(io_backend_error)?; - set_secret_permissions(&self.key_path)?; - Ok(key) - } - - fn load_wrapping_key( - &self, - ) -> Result<[u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], RadrootsSecretVaultAccessError> { - let raw = fs::read(&self.key_path).map_err(io_backend_error)?; - if raw.len() != RADROOTS_PROTECTED_STORE_KEY_LENGTH { - return Err(RadrootsSecretVaultAccessError::Backend(format!( - "encrypted identity wrapping key {} has invalid length {}", - self.key_path.display(), - raw.len() - ))); - } - - let mut key = [0_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]; - key.copy_from_slice(&raw); - Ok(key) - } -} - -impl RadrootsSecretKeyWrapping for MycEncryptedIdentityKeySource { - type Error = RadrootsSecretVaultAccessError; - - fn wrap_data_key(&self, key_slot: &str, plaintext_key: &[u8]) -> Result<Vec<u8>, Self::Error> { - let mut master_key = self.load_or_create_wrapping_key()?; - let mut nonce = [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH]; - getrandom(&mut nonce) - .map_err(|_| RadrootsSecretVaultAccessError::Backend("entropy unavailable".into()))?; - let cipher = XChaCha20Poly1305::new(Key::from_slice(&master_key)); - let ciphertext = cipher - .encrypt( - XNonce::from_slice(&nonce), - Payload { - msg: plaintext_key, - aad: key_slot.as_bytes(), - }, - ) - .map_err(|_| { - RadrootsSecretVaultAccessError::Backend( - "failed to wrap encrypted identity data key".into(), - ) - })?; - master_key.zeroize(); - - let mut encoded = Vec::with_capacity(1 + nonce.len() + ciphertext.len()); - encoded.push(WRAPPED_KEY_VERSION); - encoded.extend_from_slice(&nonce); - encoded.extend_from_slice(ciphertext.as_slice()); - Ok(encoded) - } - - fn unwrap_data_key(&self, key_slot: &str, wrapped_key: &[u8]) -> Result<Vec<u8>, Self::Error> { - if wrapped_key.len() <= 1 + RADROOTS_PROTECTED_STORE_NONCE_LENGTH { - return Err(RadrootsSecretVaultAccessError::Backend( - "wrapped encrypted identity data key is truncated".into(), - )); - } - if wrapped_key[0] != WRAPPED_KEY_VERSION { - return Err(RadrootsSecretVaultAccessError::Backend(format!( - "unsupported encrypted identity wrapped data key version {}", - wrapped_key[0] - ))); - } - - let mut master_key = self.load_wrapping_key()?; - let nonce_offset = 1; - let ciphertext_offset = nonce_offset + RADROOTS_PROTECTED_STORE_NONCE_LENGTH; - let cipher = XChaCha20Poly1305::new(Key::from_slice(&master_key)); - let plaintext = cipher - .decrypt( - XNonce::from_slice(&wrapped_key[nonce_offset..ciphertext_offset]), - Payload { - msg: &wrapped_key[ciphertext_offset..], - aad: key_slot.as_bytes(), - }, - ) - .map_err(|_| { - RadrootsSecretVaultAccessError::Backend( - "failed to unwrap encrypted identity data key".into(), - ) - })?; - master_key.zeroize(); - Ok(plaintext) - } -} pub fn encrypted_identity_wrapping_key_path(path: impl AsRef<Path>) -> PathBuf { - let path = path.as_ref(); - let mut value = OsString::from(path.as_os_str()); - value.push(ENCRYPTED_IDENTITY_KEY_SUFFIX); - PathBuf::from(value) + sidecar_path(path, ENCRYPTED_IDENTITY_KEY_SUFFIX) } pub fn store_encrypted_identity( @@ -157,7 +31,8 @@ pub fn store_encrypted_identity( } let payload = serde_json::to_vec(&identity.to_file())?; - let key_source = MycEncryptedIdentityKeySource::new(path); + let key_source = + RadrootsProtectedFileKeySource::from_sidecar_suffix(path, ENCRYPTED_IDENTITY_KEY_SUFFIX); let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key( &key_source, ENCRYPTED_IDENTITY_KEY_SLOT, @@ -233,7 +108,8 @@ pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentit path: path.to_path_buf(), source, })?; - let key_source = MycEncryptedIdentityKeySource::new(path); + let key_source = + RadrootsProtectedFileKeySource::from_sidecar_suffix(path, ENCRYPTED_IDENTITY_KEY_SUFFIX); let envelope = RadrootsProtectedStoreEnvelope::decode_json(&encoded).map_err(|error| { MycError::InvalidOperation(format!( "failed to decode encrypted identity {}: {error}",