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:
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}",