radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit bfc25af39607127d6fcde62a8d49025753608854
parent 625be723530c4efe979fa9f40cc79d789334b558
Author: triesap <tyson@radroots.org>
Date:   Wed,  5 Aug 2026 22:03:42 +0000

refactor: adopt context-bound envelopes

Diffstat:
MCargo.lock | 22++++++++++++----------
MCargo.toml | 16++++++++--------
Msrc/app/identity_storage.rs | 270++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mtests/source_boundary.rs | 2+-
4 files changed, 268 insertions(+), 42 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1604,7 +1604,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radroots_blossom" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "mediatype", "serde", @@ -1616,7 +1616,7 @@ dependencies = [ [[package]] name = "radroots_core" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "rust_decimal", "serde", @@ -1625,7 +1625,7 @@ dependencies = [ [[package]] name = "radroots_event" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "hex", "jiff-tzdb", @@ -1643,7 +1643,7 @@ dependencies = [ [[package]] name = "radroots_event_codec" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "hex", "radroots_blossom", @@ -1660,7 +1660,7 @@ dependencies = [ [[package]] name = "radroots_identity" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "k256", "serde", @@ -1670,7 +1670,7 @@ dependencies = [ [[package]] name = "radroots_nostr" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "nostr", "radroots_event", @@ -1684,7 +1684,7 @@ dependencies = [ [[package]] name = "radroots_protocol" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "serde", ] @@ -1692,16 +1692,18 @@ dependencies = [ [[package]] name = "radroots_secrets" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "chacha20poly1305", + "sha2", + "subtle", "zeroize", ] [[package]] name = "radroots_transport" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "radroots_event", "radroots_identity", @@ -1712,7 +1714,7 @@ dependencies = [ [[package]] name = "radroots_transport_nostr" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib.git?rev=5537bf0e1c36a94958a4c6eceb7722845b9a4efa#5537bf0e1c36a94958a4c6eceb7722845b9a4efa" +source = "git+https://github.com/radrootslabs/lib.git?rev=f7f456d906d91aadbb9fac4850afaa8c5723c3e9#f7f456d906d91aadbb9fac4850afaa8c5723c3e9" dependencies = [ "async-wsocket", "nostr-relay-pool", diff --git a/Cargo.toml b/Cargo.toml @@ -11,14 +11,14 @@ description = "Radroots local runtime daemon for storage, sync, and relay publis resolver = "3" [workspace.dependencies] -radroots_event = { git = "https://github.com/radrootslabs/lib.git", rev = "5537bf0e1c36a94958a4c6eceb7722845b9a4efa", version = "=0.1.0-alpha" } -radroots_event_codec = { git = "https://github.com/radrootslabs/lib.git", rev = "5537bf0e1c36a94958a4c6eceb7722845b9a4efa", version = "=0.1.0-alpha" } -radroots_identity = { git = "https://github.com/radrootslabs/lib.git", rev = "5537bf0e1c36a94958a4c6eceb7722845b9a4efa", version = "=0.1.0-alpha" } -radroots_nostr = { git = "https://github.com/radrootslabs/lib.git", rev = "5537bf0e1c36a94958a4c6eceb7722845b9a4efa", version = "=0.1.0-alpha" } -radroots_protocol = { git = "https://github.com/radrootslabs/lib.git", rev = "5537bf0e1c36a94958a4c6eceb7722845b9a4efa", version = "=0.1.0-alpha", default-features = false } -radroots_secrets = { git = "https://github.com/radrootslabs/lib.git", rev = "5537bf0e1c36a94958a4c6eceb7722845b9a4efa", version = "=0.1.0-alpha", default-features = false } -radroots_transport_nostr = { git = "https://github.com/radrootslabs/lib.git", rev = "5537bf0e1c36a94958a4c6eceb7722845b9a4efa", version = "=0.1.0-alpha", default-features = false } -radroots_transport = { git = "https://github.com/radrootslabs/lib.git", rev = "5537bf0e1c36a94958a4c6eceb7722845b9a4efa", version = "=0.1.0-alpha", default-features = false } +radroots_event = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha" } +radroots_event_codec = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha" } +radroots_identity = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha" } +radroots_nostr = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha" } +radroots_protocol = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha", default-features = false } +radroots_secrets = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha", default-features = false } +radroots_transport_nostr = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha", default-features = false } +radroots_transport = { git = "https://github.com/radrootslabs/lib.git", rev = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9", version = "=0.1.0-alpha", default-features = false } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/src/app/identity_storage.rs b/src/app/identity_storage.rs @@ -7,11 +7,17 @@ use anyhow::{Context, Result, bail}; use chacha20poly1305::aead::{Aead, KeyInit, Payload}; use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; use nostr::{Keys, SecretKey}; -use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest}; +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; +use radroots_secrets::envelope::{ + ENVELOPE_VERSION, LEGACY_ENVELOPE_VERSION, LegacyV1ResealAuthority, Nonce, SealMaterial, + SealRequest, +}; use radroots_secrets::error::Operation; use radroots_secrets::id::{BackendKind, KeyVersion}; use radroots_secrets::wrapping::{ - BoxFuture, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret, + BoxFuture, LegacyV1UnwrapRequest, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret, }; use radroots_secrets::{EncryptedEnvelope, KeyWrapping, SecretId, SecretRef}; use serde::{Deserialize, Serialize}; @@ -20,7 +26,9 @@ use zeroize::Zeroize; const RADROOTSD_IDENTITY_KEY_SLOT: &str = "radrootsd_identity"; const WRAPPING_KEY_BYTES: usize = 32; const WRAPPING_NONCE_BYTES: usize = 24; -const WRAPPED_KEY_VERSION: u8 = 1; +const LEGACY_WRAPPED_KEY_VERSION: u8 = 1; +const WRAPPED_KEY_VERSION: u8 = 2; +const WRAPPING_AAD_DOMAIN: &[u8] = b"radrootsd.wrapped_data_key.v2"; struct DaemonFileKeyWrapping { key_path: PathBuf, @@ -83,14 +91,16 @@ impl KeyWrapping for DaemonFileKeyWrapping { request: WrapRequest<'a>, ) -> BoxFuture<'a, Result<WrappedSecret, radroots_secrets::Error>> { Box::pin(async move { + validate_daemon_reference(request.reference(), Operation::Wrap)?; let mut key = self.load_or_create_key()?; let nonce: [u8; WRAPPING_NONCE_BYTES] = rand::random(); + let aad = wrapping_aad(request.reference(), request.context()); let ciphertext = request.plaintext().expose_secret(|plaintext| { XChaCha20Poly1305::new(Key::from_slice(&key)).encrypt( XNonce::from_slice(&nonce), Payload { msg: plaintext, - aad: request.reference().id().as_str().as_bytes(), + aad: aad.as_slice(), }, ) }); @@ -109,26 +119,81 @@ impl KeyWrapping for DaemonFileKeyWrapping { request: UnwrapRequest<'a>, ) -> BoxFuture<'a, Result<SecretMaterial, radroots_secrets::Error>> { Box::pin(async move { - let wrapped = request.wrapped().as_bytes(); - if wrapped.len() <= 1 + WRAPPING_NONCE_BYTES || wrapped[0] != WRAPPED_KEY_VERSION { - return Err(secret_backend_failure(Operation::Unwrap)); - } - let mut key = self.load_key()?; - let plaintext = XChaCha20Poly1305::new(Key::from_slice(&key)).decrypt( - XNonce::from_slice(&wrapped[1..1 + WRAPPING_NONCE_BYTES]), - Payload { - msg: &wrapped[1 + WRAPPING_NONCE_BYTES..], - aad: request.reference().id().as_str().as_bytes(), - }, - ); - key.zeroize(); - SecretMaterial::from_slice( - &plaintext.map_err(|_| secret_backend_failure(Operation::Unwrap))?, + validate_daemon_reference(request.reference(), Operation::Unwrap)?; + let aad = wrapping_aad(request.reference(), request.context()); + self.unwrap_with_aad(request.wrapped(), WRAPPED_KEY_VERSION, aad.as_slice()) + }) + } + + fn unwrap_legacy_v1<'a>( + &'a self, + request: LegacyV1UnwrapRequest<'a>, + ) -> BoxFuture<'a, Result<SecretMaterial, radroots_secrets::Error>> { + Box::pin(async move { + validate_daemon_reference(request.reference(), Operation::Unwrap)?; + self.unwrap_with_aad( + request.wrapped(), + LEGACY_WRAPPED_KEY_VERSION, + request.reference().id().as_str().as_bytes(), ) }) } } +impl DaemonFileKeyWrapping { + fn unwrap_with_aad( + &self, + wrapped: &WrappedSecret, + expected_version: u8, + aad: &[u8], + ) -> Result<SecretMaterial, radroots_secrets::Error> { + let wrapped = wrapped.as_bytes(); + if wrapped.len() <= 1 + WRAPPING_NONCE_BYTES || wrapped[0] != expected_version { + return Err(secret_backend_failure(Operation::Unwrap)); + } + let mut key = self.load_key()?; + let plaintext = XChaCha20Poly1305::new(Key::from_slice(&key)).decrypt( + XNonce::from_slice(&wrapped[1..1 + WRAPPING_NONCE_BYTES]), + Payload { + msg: &wrapped[1 + WRAPPING_NONCE_BYTES..], + aad, + }, + ); + key.zeroize(); + SecretMaterial::from_slice( + &plaintext.map_err(|_| secret_backend_failure(Operation::Unwrap))?, + ) + } +} + +fn wrapping_aad(reference: &SecretRef, context: &EnvelopeContext) -> Vec<u8> { + let id = reference.id().as_str().as_bytes(); + let mut aad = Vec::with_capacity(WRAPPING_AAD_DOMAIN.len() + 2 + id.len() + 4 + 32); + aad.extend_from_slice(WRAPPING_AAD_DOMAIN); + aad.extend_from_slice( + &u16::try_from(id.len()) + .expect("validated secret identifier length fits u16") + .to_be_bytes(), + ); + aad.extend_from_slice(id); + aad.extend_from_slice(&reference.key_version().get().to_be_bytes()); + aad.extend_from_slice(&context.authentication_digest()); + aad +} + +fn validate_daemon_reference( + reference: &SecretRef, + operation: Operation, +) -> Result<(), radroots_secrets::Error> { + if reference.backend() != BackendKind::External + || reference.key_version().get() != 1 + || reference.id().as_str() != RADROOTSD_IDENTITY_KEY_SLOT + { + return Err(secret_backend_failure(operation)); + } + Ok(()) +} + fn identity_secret_ref() -> Result<SecretRef, radroots_secrets::Error> { Ok(SecretRef::new( SecretId::parse(RADROOTSD_IDENTITY_KEY_SLOT)?, @@ -137,6 +202,14 @@ fn identity_secret_ref() -> Result<SecretRef, radroots_secrets::Error> { )) } +fn identity_envelope_context() -> Result<EnvelopeContext, radroots_secrets::Error> { + Ok(EnvelopeContext::new( + EnvelopePurpose::parse("radroots.service_identity")?, + EnvelopeSubject::parse("service", "radrootsd")?, + PayloadSchemaId::parse("radroots.daemon_identity.v1")?, + )) +} + fn secret_backend_failure(operation: Operation) -> radroots_secrets::Error { radroots_secrets::Error::BackendFailure { backend: BackendKind::External, @@ -265,21 +338,26 @@ pub fn store_encrypted_identity(path: impl AsRef<Path>, identity: &DaemonIdentit let data_key = SecretMaterial::from_slice(&rand::random::<[u8; 32]>())?; let nonce = Nonce::new(rand::random()); let wrapping = DaemonFileKeyWrapping::new(path); + let context = identity_envelope_context()?; let envelope = futures_executor::block_on(EncryptedEnvelope::seal( &wrapping, SealRequest::new( identity_secret_ref()?, + context, &plaintext, SealMaterial::new(data_key, nonce), ), ))?; - let encoded = envelope.encode()?; + persist_envelope(path, envelope.encode()?.as_slice()) +} + +fn persist_envelope(path: &Path, encoded: &[u8]) -> Result<()> { let mut temporary = tempfile::NamedTempFile::new_in( path.parent() .filter(|parent| !parent.as_os_str().is_empty()) .unwrap_or(Path::new(".")), )?; - temporary.write_all(encoded.as_slice())?; + temporary.write_all(encoded)?; temporary.as_file().sync_all()?; set_file_permissions(temporary.as_file())?; temporary.persist(path)?; @@ -293,12 +371,60 @@ pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<DaemonIdentity> let envelope = EncryptedEnvelope::decode(encoded.as_slice()) .context("decode encrypted daemon identity")?; let wrapping = DaemonFileKeyWrapping::new(path); - let payload = futures_executor::block_on(envelope.open(&wrapping)) + let context = identity_envelope_context()?; + if envelope.version() == LEGACY_ENVELOPE_VERSION { + return migrate_legacy_identity(path, envelope, &wrapping, context); + } + if envelope.version() != ENVELOPE_VERSION { + bail!("unsupported encrypted daemon identity version"); + } + open_identity(&envelope, &wrapping, &context) +} + +fn open_identity( + envelope: &EncryptedEnvelope, + wrapping: &DaemonFileKeyWrapping, + context: &EnvelopeContext, +) -> Result<DaemonIdentity> { + let payload = futures_executor::block_on(envelope.open(wrapping, context)) .context("open encrypted daemon identity")?; let file: DaemonIdentityFile = payload.expose_secret(|bytes| serde_json::from_slice(bytes))?; DaemonIdentity::from_file(file) } +fn migrate_legacy_identity( + path: &Path, + envelope: EncryptedEnvelope, + wrapping: &DaemonFileKeyWrapping, + context: EnvelopeContext, +) -> Result<DaemonIdentity> { + let expected_reference = identity_secret_ref()?; + let resealed = futures_executor::block_on(envelope.reseal_legacy_v1( + wrapping, + &LegacyV1ResealAuthority::new(), + &expected_reference, + identity_secret_ref()?, + context.clone(), + &valid_identity_payload, + SealMaterial::new( + SecretMaterial::from_slice(&rand::random::<[u8; 32]>())?, + Nonce::new(rand::random()), + ), + )) + .context("migrate legacy encrypted daemon identity")?; + let envelope = resealed.into_envelope(); + let identity = open_identity(&envelope, wrapping, &context)?; + persist_envelope(path, envelope.encode()?.as_slice())?; + Ok(identity) +} + +fn valid_identity_payload(bytes: &[u8]) -> bool { + serde_json::from_slice::<DaemonIdentityFile>(bytes) + .ok() + .and_then(|file| DaemonIdentity::from_file(file).ok()) + .is_some() +} + #[cfg(unix)] fn set_secret_permissions(path: &Path) -> std::io::Result<()> { use std::os::unix::fs::PermissionsExt; @@ -340,7 +466,17 @@ fn resolved_identity_path(path: Option<&Path>) -> PathBuf { #[cfg(test)] mod tests { - use super::{encrypted_identity_key_path, load_service_identity}; + use std::fs; + + use chacha20poly1305::aead::{Aead, KeyInit, Payload}; + use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; + use radroots_secrets::EncryptedEnvelope; + use radroots_secrets::envelope::{ENVELOPE_VERSION, LEGACY_ENVELOPE_VERSION}; + + use super::{ + DaemonIdentity, RADROOTSD_IDENTITY_KEY_SLOT, encrypted_identity_key_path, + identity_envelope_context, load_service_identity, set_secret_permissions, + }; #[test] fn load_service_identity_generates_encrypted_identity_artifacts() { @@ -354,6 +490,13 @@ mod tests { assert_eq!(generated.id(), loaded.id()); assert!(path.is_file()); assert!(encrypted_identity_key_path(&path).is_file()); + let envelope = EncryptedEnvelope::decode(&fs::read(&path).expect("read envelope")) + .expect("decode envelope"); + assert_eq!(envelope.version(), ENVELOPE_VERSION); + assert_eq!( + envelope.context(), + Some(&identity_envelope_context().expect("identity context")) + ); } #[test] @@ -367,4 +510,85 @@ mod tests { .expect_err("missing wrapping key should fail"); assert!(err.to_string().contains("identity")); } + + #[test] + fn load_service_identity_atomically_migrates_legacy_v1() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("radrootsd-identity.secret.json"); + let expected = DaemonIdentity::generate(); + store_legacy_identity(&path, &expected); + + let loaded = load_service_identity(Some(&path), false).expect("migrate legacy identity"); + assert_eq!(loaded.id(), expected.id()); + + let encoded = fs::read(&path).expect("read migrated identity"); + let migrated = EncryptedEnvelope::decode(&encoded).expect("decode migrated identity"); + assert_eq!(migrated.version(), ENVELOPE_VERSION); + assert_eq!( + migrated.context(), + Some(&identity_envelope_context().expect("identity context")) + ); + let reloaded = load_service_identity(Some(&path), false).expect("reload migrated identity"); + assert_eq!(reloaded.id(), expected.id()); + } + + fn store_legacy_identity(path: &std::path::Path, identity: &DaemonIdentity) { + const NONCE_BYTES: usize = 24; + const TAG_BYTES: usize = 16; + + let wrapping_key = [0x11; 32]; + let data_key = [0x22; 32]; + let wrapping_nonce = [0x33; NONCE_BYTES]; + let envelope_nonce = [0x44; NONCE_BYTES]; + let payload = serde_json::to_vec(&identity.to_file()).expect("encode identity payload"); + + let wrapped_ciphertext = XChaCha20Poly1305::new(Key::from_slice(&wrapping_key)) + .encrypt( + XNonce::from_slice(&wrapping_nonce), + Payload { + msg: &data_key, + aad: RADROOTSD_IDENTITY_KEY_SLOT.as_bytes(), + }, + ) + .expect("wrap legacy data key"); + let mut wrapped = Vec::with_capacity(1 + NONCE_BYTES + wrapped_ciphertext.len()); + wrapped.push(1); + wrapped.extend_from_slice(&wrapping_nonce); + wrapped.extend_from_slice(&wrapped_ciphertext); + + let id = RADROOTSD_IDENTITY_KEY_SLOT.as_bytes(); + let ciphertext_len = u32::try_from(payload.len() + TAG_BYTES).expect("ciphertext length"); + let mut encoded = Vec::new(); + encoded.extend_from_slice(b"RRS1"); + encoded.extend_from_slice(&LEGACY_ENVELOPE_VERSION.to_be_bytes()); + encoded.push(1); // XChaCha20-Poly1305 + encoded.push(1); // provider-wrapped key source + encoded.push(4); // external backend + encoded.extend_from_slice(&1_u32.to_be_bytes()); + encoded.extend_from_slice(&u16::try_from(id.len()).expect("id length").to_be_bytes()); + encoded.extend_from_slice(id); + encoded.extend_from_slice(&envelope_nonce); + encoded.extend_from_slice( + &u32::try_from(wrapped.len()) + .expect("wrapped length") + .to_be_bytes(), + ); + encoded.extend_from_slice(&wrapped); + encoded.extend_from_slice(&ciphertext_len.to_be_bytes()); + let ciphertext = XChaCha20Poly1305::new(Key::from_slice(&data_key)) + .encrypt( + XNonce::from_slice(&envelope_nonce), + Payload { + msg: &payload, + aad: &encoded, + }, + ) + .expect("encrypt legacy payload"); + encoded.extend_from_slice(&ciphertext); + + fs::write(path, encoded).expect("write legacy envelope"); + let key_path = encrypted_identity_key_path(path); + fs::write(&key_path, wrapping_key).expect("write wrapping key"); + set_secret_permissions(&key_path).expect("secure wrapping key"); + } } diff --git a/tests/source_boundary.rs b/tests/source_boundary.rs @@ -198,7 +198,7 @@ fn transport_publish_sources_reject_removed_protocol_identifiers() { } assert!( manifest_source.contains( - "radroots_transport_nostr = { git = \"https://github.com/radrootslabs/lib.git\", rev = \"5537bf0e1c36a94958a4c6eceb7722845b9a4efa\", version = \"=0.1.0-alpha\"" + "radroots_transport_nostr = { git = \"https://github.com/radrootslabs/lib.git\", rev = \"f7f456d906d91aadbb9fac4850afaa8c5723c3e9\", version = \"=0.1.0-alpha\"" ), "Cargo.toml must pin radroots_transport_nostr to the approved Lib revision and exact version" );