commit bfc25af39607127d6fcde62a8d49025753608854
parent 625be723530c4efe979fa9f40cc79d789334b558
Author: triesap <tyson@radroots.org>
Date: Wed, 5 Aug 2026 22:03:42 +0000
refactor: adopt context-bound envelopes
Diffstat:
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"
);