update concord backend

This commit is contained in:
2026-09-19 07:46:09 +07:00
parent d3b8fa08de
commit aa3bd71351
5 changed files with 127 additions and 80 deletions
+30 -14
View File
@@ -447,16 +447,19 @@ pub fn parse_link(input: &str) -> Result<ParsedInviteLink, InviteError> {
})
}
pub fn build_direct_invite(
inviter: &Keys,
pub async fn build_direct_invite<S>(
inviter: &S,
recipient: &PublicKey,
invite: &CommunityInvite,
) -> Result<Event, InviteError> {
) -> Result<Event, InviteError>
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44,
{
invite.validate()?;
let json = serde_json::to_string(invite).map_err(json_error)?;
let rumor = EventBuilder::new(Kind::Custom(KIND_DIRECT_INVITE), json)
.finalize_unsigned(inviter.public_key());
let author = inviter.get_public_key_async().await.map_err(crypto_error)?;
let rumor = EventBuilder::new(Kind::Custom(KIND_DIRECT_INVITE), json).finalize_unsigned(author);
let mut tags = vec![Tag::custom("k", [KIND_DIRECT_INVITE.to_string()])];
@@ -469,15 +472,22 @@ pub fn build_direct_invite(
GiftWrapBuilder::new(*recipient, rumor)
.extra_tags(tags)
.finalize(inviter)
.finalize_async(inviter)
.await
.map_err(crypto_error)
}
pub fn unwrap_direct_invite(
/// The NIP-59 unwrap is `Sized`-bounded in the SDK, so this stays `Sized` too.
pub async fn unwrap_direct_invite<S>(
wrap: &Event,
recipient: &Keys,
) -> Result<(PublicKey, CommunityInvite), InviteError> {
let unwrapped = UnwrappedGift::from_gift_wrap(recipient, wrap).map_err(crypto_error)?;
recipient: &S,
) -> Result<(PublicKey, CommunityInvite), InviteError>
where
S: AsyncNip44,
{
let unwrapped = UnwrappedGift::from_gift_wrap_async(recipient, wrap)
.await
.map_err(crypto_error)?;
if unwrapped.rumor.kind.as_u16() != KIND_DIRECT_INVITE {
return Err(InviteError::BadEvent("rumor is not a direct invite"));
@@ -927,7 +937,12 @@ mod tests {
let recipient = Keys::generate();
let invite = bundle();
let wrap = build_direct_invite(&inviter, &recipient.public_key(), &invite).expect("builds");
let wrap = smol::block_on(build_direct_invite(
&inviter,
&recipient.public_key(),
&invite,
))
.expect("builds");
assert_eq!(wrap.kind, Kind::GiftWrap);
assert_ne!(
wrap.pubkey,
@@ -939,13 +954,14 @@ mod tests {
"the k tag is what makes an invite indexable"
);
let (sender, opened) = unwrap_direct_invite(&wrap, &recipient).expect("unwraps");
let (sender, opened) =
smol::block_on(unwrap_direct_invite(&wrap, &recipient)).expect("unwraps");
assert_eq!(sender, inviter.public_key());
assert_eq!(opened.community_id, invite.community_id);
// Somebody else's wrap is not ours to open...
let stranger = Keys::generate();
assert!(unwrap_direct_invite(&wrap, &stranger).is_err());
assert!(smol::block_on(unwrap_direct_invite(&wrap, &stranger)).is_err());
// ...and a wrap that opens to some other kind is not an invite.
let rumor = EventBuilder::new(Kind::Custom(crate::cord03::KIND_MESSAGE), "hello")
@@ -954,7 +970,7 @@ mod tests {
.finalize(&recipient)
.expect("wraps");
assert!(matches!(
unwrap_direct_invite(&wrap, &recipient),
smol::block_on(unwrap_direct_invite(&wrap, &recipient)),
Err(InviteError::BadEvent(_))
));
}
+41 -36
View File
@@ -2,11 +2,9 @@ use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use anyhow::Result;
use data_encoding::HEXLOWER;
use nostr::nips::nip44::v2::ConversationKey;
use data_encoding::{BASE64, HEXLOWER};
use nostr_sdk::prelude::{
AsyncGetPublicKey, AsyncSignEvent, Event, Keys, PublicKey, SecretKey, Tag, Timestamp,
UnsignedEvent,
AsyncGetPublicKey, AsyncNip44, AsyncSignEvent, Event, PublicKey, Tag, Timestamp, UnsignedEvent,
};
use serde::{Deserialize, Serialize};
@@ -301,34 +299,48 @@ pub fn blob_locator(
))
}
pub fn build_blob(
rotator: &Keys,
pub async fn build_blob<S>(
rotator: &S,
recipient: &PublicKey,
scope: RekeyScope,
epoch: Epoch,
new_key: &[u8; 32],
control_pk: Option<&[u8; 32]>,
control_root: Option<&[u8; 32]>,
) -> Result<RekeyBlob, RekeyError> {
) -> Result<RekeyBlob, RekeyError>
where
S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
{
let plaintext = encode_blob_plaintext(scope, epoch, new_key, control_pk, control_root)?;
let rotator_pk = rotator.get_public_key_async().await.map_err(crypto_error)?;
let wrapped = rotator
.nip44_encrypt_async(recipient, &BASE64.encode(&plaintext))
.await
.map_err(crypto_error)?;
Ok(RekeyBlob {
locator: blob_locator(&rotator.public_key(), recipient, scope, epoch),
wrapped: seal_to(rotator.secret_key(), recipient, &plaintext)?,
locator: blob_locator(&rotator_pk, recipient, scope, epoch),
wrapped,
})
}
pub fn open_blob(
recipient: &Keys,
pub async fn open_blob<S>(
recipient: &S,
rotator: &PublicKey,
scope: RekeyScope,
epoch: Epoch,
blob: &RekeyBlob,
community_id: &CommunityId,
) -> Result<KeyDelivery, RekeyError> {
let conversation =
ConversationKey::derive(recipient.secret_key(), rotator).map_err(crypto_error)?;
let plaintext = cord01::open_bytes(&conversation, &blob.wrapped)?;
) -> Result<KeyDelivery, RekeyError>
where
S: AsyncNip44 + ?Sized,
{
let text = recipient
.nip44_decrypt_async(rotator, &blob.wrapped)
.await
.map_err(crypto_error)?;
let plaintext = BASE64.decode(text.as_bytes()).map_err(crypto_error)?;
parse_blob_plaintext(&plaintext, scope, epoch, community_id)
}
@@ -344,15 +356,6 @@ pub fn find_my_blobs<'a>(
blobs.iter().filter(move |blob| blob.locator == wanted)
}
fn seal_to(
secret: &SecretKey,
recipient: &PublicKey,
plaintext: &[u8],
) -> Result<String, RekeyError> {
let conversation = ConversationKey::derive(secret, recipient).map_err(crypto_error)?;
Ok(cord01::seal_bytes(&conversation, plaintext)?)
}
#[derive(Debug, Clone)]
pub struct RekeyChunk {
pub rotator: PublicKey,
@@ -868,6 +871,8 @@ fn crypto_error(error: impl fmt::Display) -> RekeyError {
mod tests {
use std::collections::BTreeSet;
use nostr_sdk::prelude::Keys;
use super::*;
use crate::cord01::KIND_WRAP;
use crate::cord02::{
@@ -922,16 +927,16 @@ mod tests {
let scope = RekeyScope::Channel(channel());
let open = |keys: &Keys, scope: RekeyScope, epoch: Epoch, blob: &RekeyBlob| {
open_blob(
smol::block_on(open_blob(
keys,
&rotator.public_key(),
scope,
epoch,
blob,
&community_id,
)
))
};
let blob = build_blob(
let blob = smol::block_on(build_blob(
&rotator,
&recipient.public_key(),
scope,
@@ -939,7 +944,7 @@ mod tests {
&key,
None,
None,
)
))
.expect("builds");
assert_eq!(
@@ -972,7 +977,7 @@ mod tests {
.to_bytes();
let base = |pk: Option<&[u8; 32]>, root: Option<&[u8; 32]>| {
build_blob(
smol::block_on(build_blob(
&rotator,
&recipient.public_key(),
RekeyScope::Base,
@@ -980,7 +985,7 @@ mod tests {
&key,
pk,
root,
)
))
.expect("builds")
};
@@ -1053,7 +1058,7 @@ mod tests {
let community_id = community();
let blob_for = |recipient: &Keys, key: [u8; 32]| {
build_blob(
smol::block_on(build_blob(
&rotator,
&recipient.public_key(),
scope,
@@ -1061,7 +1066,7 @@ mod tests {
&key,
None,
None,
)
))
.expect("builds")
};
let mine = blob_for(&me, [0xAA; 32]);
@@ -1139,14 +1144,14 @@ mod tests {
.next()
.expect("located");
assert_eq!(
open_blob(
smol::block_on(open_blob(
&me,
&rotator.public_key(),
scope,
epoch,
located,
&community_id
)
))
.expect("opens")
.new_key,
[0xAA; 32]
@@ -1423,7 +1428,7 @@ mod tests {
.map(|_| {
let member = Keys::generate();
build_blob(
smol::block_on(build_blob(
&rotator,
&member.public_key(),
scope,
@@ -1431,7 +1436,7 @@ mod tests {
&[0xCD; 32],
None,
None,
)
))
.expect("builds")
})
.collect();
-3
View File
@@ -1,6 +1,3 @@
//! One module per CORD document. CORD-07 (audio/video) is unimplemented, and
//! CORD-08's timer rides the Chat and Control planes it edits rather than owning a file.
pub mod cord01;
pub mod cord02;
pub mod cord03;