Skip to content

Commit 2afffef

Browse files
authored
Consume nostr contacts (#659)
* Enable NIP-17 private messages * Company contact sharing * Review fixes
1 parent d615cec commit 2afffef

File tree

21 files changed

+443
-18
lines changed

21 files changed

+443
-18
lines changed

crates/bcr-ebill-api/src/service/company_service.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ pub trait CompanyServiceApi: ServiceTraitBounds {
111111
file_name: &str,
112112
private_key: &SecretKey,
113113
) -> Result<Vec<u8>>;
114+
115+
/// Shares derived keys for given company contact information.
116+
async fn share_contact_details(&self, share_to: &NodeId, company_id: NodeId) -> Result<()>;
114117
}
115118

116119
/// The company service is responsible for managing the companies
@@ -229,11 +232,9 @@ impl CompanyService {
229232
}
230233
}
231234

232-
/// Derives an identity child key, encrypts the contact data from company with it and returns the bcr metadata
235+
/// Derives a company contact encryption key, encrypts the contact data with it and returns the BCR metadata.
233236
fn get_bcr_data(company: &Company, keys: &CompanyKeys, relays: Vec<String>) -> Result<BcrMetadata> {
234-
// we derive via BcrKeys bcs it creates a Identity chain keypair
235-
let bcr_keys: BcrKeys = keys.clone().try_into()?;
236-
let derived_keys = bcr_keys.derive_keypair()?;
237+
let derived_keys = keys.derive_keypair()?;
237238
let contact = Contact {
238239
t: ContactType::Company,
239240
node_id: company.id.clone(),
@@ -908,6 +909,16 @@ impl CompanyServiceApi for CompanyService {
908909
return Err(super::Error::NotFound);
909910
}
910911
}
912+
913+
async fn share_contact_details(&self, share_to: &NodeId, company_id: NodeId) -> Result<()> {
914+
let company_keys = self.store.get_key_pair(&company_id).await?;
915+
let derived_keys = company_keys.derive_keypair()?;
916+
let keys = BcrKeys::from_private_key(&derived_keys.secret_key())?;
917+
self.notification_service
918+
.share_contact_details_keys(share_to, &company_id, &keys)
919+
.await?;
920+
Ok(())
921+
}
911922
}
912923

913924
#[cfg(test)]

crates/bcr-ebill-api/src/service/contact_service.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,8 @@ impl ContactService {
210210
.by_node_id(&contact.node_id)
211211
.await?
212212
{
213-
Some(nostr_contact) => nostr_contact.merge_contact(contact),
214-
None => NostrContact::from_contact(contact)?,
213+
Some(nostr_contact) => nostr_contact.merge_contact(contact, None),
214+
None => NostrContact::from_contact(contact, None)?,
215215
};
216216
self.nostr_contact_store.upsert(&nostr_contact).await?;
217217
Ok(())
@@ -1267,6 +1267,7 @@ pub mod tests {
12671267
relays: vec![],
12681268
trust_level: TrustLevel::Participant,
12691269
handshake_status: HandshakeStatus::None,
1270+
contact_private_key: None,
12701271
}))
12711272
});
12721273
let result = get_service(

crates/bcr-ebill-api/src/service/identity_service.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ pub trait IdentityServiceApi: ServiceTraitBounds {
105105

106106
/// Returns the local key pair
107107
async fn get_keys(&self) -> Result<BcrKeys>;
108+
109+
/// Shares derived keys for private identity contact information. Recipient is the given node id.
110+
async fn share_contact_details(&self, share_to: &NodeId) -> Result<()>;
108111
}
109112

110113
/// The identity service is responsible for managing the local identity
@@ -730,6 +733,16 @@ impl IdentityServiceApi for IdentityService {
730733
async fn get_keys(&self) -> Result<BcrKeys> {
731734
Ok(self.store.get_key_pair().await?)
732735
}
736+
737+
async fn share_contact_details(&self, share_to: &NodeId) -> Result<()> {
738+
let identity = self.get_full_identity().await?;
739+
let derived_keys = identity.key_pair.derive_keypair()?;
740+
let keys = BcrKeys::from_private_key(&derived_keys.secret_key())?;
741+
self.notification_service
742+
.share_contact_details_keys(share_to, &identity.identity.node_id, &keys)
743+
.await?;
744+
Ok(())
745+
}
733746
}
734747

735748
#[cfg(test)]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use bcr_ebill_core::NodeId;
2+
use secp256k1::SecretKey;
3+
use serde::{Deserialize, Serialize};
4+
5+
/// Event payload when keys for contact details are shared. This is used for both personal identity
6+
/// and company. The shared keys are derived from the private key of the identity or company and
7+
/// all the recipents to decrypt the private Nostr profile data that is stored on relays.
8+
#[derive(Serialize, Deserialize, Debug, Clone)]
9+
pub struct ContactShareEvent {
10+
/// The node id of the contact that is shared
11+
pub node_id: NodeId,
12+
/// The private key of the contact that is shared
13+
pub private_key: SecretKey,
14+
}

crates/bcr-ebill-api/src/service/notification_service/event/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod bill_events;
22
mod blockchain_event;
33
mod company_events;
4+
mod contact;
45
mod identity_events;
56

67
use crate::service::notification_service::{Error, Result};
@@ -12,6 +13,7 @@ pub use blockchain_event::{
1213
BillBlockEvent, ChainInvite, ChainKeys, CompanyBlockEvent, IdentityBlockEvent,
1314
};
1415
pub use company_events::CompanyChainEvent;
16+
pub use contact::ContactShareEvent;
1517
pub use identity_events::IdentityChainEvent;
1618

1719
/// The global event type that is used for all events.
@@ -29,6 +31,8 @@ pub enum EventType {
2931
CompanyChain,
3032
/// Private company invites with keys
3133
CompanyChainInvite,
34+
/// Share private company or identity contact details
35+
ContactShare,
3236
}
3337

3438
impl EventType {
@@ -39,6 +43,8 @@ impl EventType {
3943
EventType::BillChainInvite,
4044
EventType::IdentityChain,
4145
EventType::CompanyChain,
46+
EventType::CompanyChainInvite,
47+
EventType::ContactShare,
4248
]
4349
}
4450
}
@@ -85,6 +91,10 @@ impl<T: Serialize> Event<T> {
8591
pub fn new_company_invite(data: T) -> Self {
8692
Self::new(EventType::CompanyChainInvite, data)
8793
}
94+
95+
pub fn new_contact_share(data: T) -> Self {
96+
Self::new(EventType::ContactShare, data)
97+
}
8898
}
8999

90100
/// The event version that is used for all events if no specific version

crates/bcr-ebill-api/src/service/notification_service/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ impl NostrContactData {
9797
relays: relays.into_iter().filter_map(|r| r.parse().ok()).collect(),
9898
}
9999
}
100+
101+
pub fn get_bcr_metadata(&self) -> Option<BcrMetadata> {
102+
self.metadata
103+
.custom
104+
.get("bcr")
105+
.cloned()
106+
.and_then(|c| serde_json::from_value(c).ok())
107+
}
100108
}
101109

102110
/// Our custom data on nostr Metadata messages

crates/bcr-ebill-api/src/service/notification_service/service.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,12 @@ pub trait NotificationServiceApi: ServiceTraitBounds {
206206

207207
/// Resync bill chain
208208
async fn resync_bill_chain(&self, bill_id: &BillId) -> Result<()>;
209+
210+
/// Shares derived keys for private contact information via DM.
211+
async fn share_contact_details_keys(
212+
&self,
213+
recipient: &NodeId,
214+
contact_id: &NodeId,
215+
keys: &BcrKeys,
216+
) -> Result<()>;
209217
}

crates/bcr-ebill-core/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,15 @@ impl OptionalPostalAddress {
264264
}
265265
}
266266

267+
pub fn from_postal_address(address: &PostalAddress) -> Self {
268+
Self {
269+
country: Some(address.country.clone()),
270+
city: Some(address.city.clone()),
271+
zip: address.zip.clone(),
272+
address: Some(address.address.clone()),
273+
}
274+
}
275+
267276
pub fn is_fully_set(&self) -> bool {
268277
self.country.is_some() && self.city.is_some() && self.address.is_some()
269278
}

crates/bcr-ebill-core/src/nostr_contact.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::collections::BTreeSet;
22

3+
use secp256k1::SecretKey;
34
use serde::{Deserialize, Serialize};
45

56
use crate::{ValidationError, contact::Contact};
@@ -21,25 +22,31 @@ pub struct NostrContact {
2122
pub trust_level: TrustLevel,
2223
/// The handshake status with this contact.
2324
pub handshake_status: HandshakeStatus,
25+
/// The keys to decrypt private nostr contact details.
26+
pub contact_private_key: Option<SecretKey>,
2427
}
2528

2629
impl NostrContact {
2730
/// Creates a new Nostr contact from a contact. This is used when we have a contact and want to
2831
/// create the Nostr contact from it. Handshake is set to complete and we trust the contact.
29-
pub fn from_contact(contact: &Contact) -> Result<Self, ValidationError> {
32+
pub fn from_contact(
33+
contact: &Contact,
34+
private_key: Option<SecretKey>,
35+
) -> Result<Self, ValidationError> {
3036
let npub = contact.node_id.npub();
3137
Ok(Self {
3238
npub,
3339
name: Some(contact.name.clone()),
3440
relays: contact.nostr_relays.clone(),
3541
trust_level: TrustLevel::Trusted,
3642
handshake_status: HandshakeStatus::Added,
43+
contact_private_key: private_key,
3744
})
3845
}
3946

4047
/// Merges contact data into a nostr contact. This assumes at that point the handskake is
4148
/// complete and we trust the contact.
42-
pub fn merge_contact(&self, contact: &Contact) -> Self {
49+
pub fn merge_contact(&self, contact: &Contact, private_key: Option<SecretKey>) -> Self {
4350
let mut relays: BTreeSet<String> = BTreeSet::from_iter(self.relays.clone());
4451
relays.extend(contact.nostr_relays.clone());
4552
Self {
@@ -48,6 +55,7 @@ impl NostrContact {
4855
relays: relays.into_iter().collect(),
4956
trust_level: TrustLevel::Trusted,
5057
handshake_status: HandshakeStatus::Added,
58+
contact_private_key: private_key.or(self.contact_private_key),
5159
}
5260
}
5361
}

crates/bcr-ebill-persistence/src/db/nostr_contact_store.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
};
99
use async_trait::async_trait;
1010
use bcr_ebill_core::{
11-
NodeId, ServiceTraitBounds,
11+
NodeId, SecretKey, ServiceTraitBounds,
1212
nostr_contact::{HandshakeStatus, NostrContact, NostrPublicKey, TrustLevel},
1313
};
1414
use serde::{Deserialize, Serialize};
@@ -123,6 +123,8 @@ pub struct NostrContactDb {
123123
pub trust_level: TrustLevel,
124124
/// The handshake status with this contact.
125125
pub handshake_status: HandshakeStatus,
126+
/// The keys to decrypt private contact details.
127+
pub contact_private_key: Option<SecretKey>,
126128
}
127129

128130
impl From<NostrContact> for NostrContactDb {
@@ -136,6 +138,7 @@ impl From<NostrContact> for NostrContactDb {
136138
relays: contact.relays,
137139
trust_level: contact.trust_level,
138140
handshake_status: contact.handshake_status,
141+
contact_private_key: contact.contact_private_key,
139142
}
140143
}
141144
}
@@ -149,6 +152,7 @@ impl TryFrom<NostrContactDb> for NostrContact {
149152
relays: db.relays,
150153
trust_level: db.trust_level,
151154
handshake_status: db.handshake_status,
155+
contact_private_key: db.contact_private_key,
152156
})
153157
}
154158
}
@@ -345,6 +349,7 @@ mod tests {
345349
relays: vec!["test_relay".to_string()],
346350
trust_level: TrustLevel::None,
347351
handshake_status: HandshakeStatus::None,
352+
contact_private_key: None,
348353
}
349354
}
350355
}

0 commit comments

Comments
 (0)