Skip to content
This repository has been archived by the owner on Feb 3, 2025. It is now read-only.

Commit

Permalink
Allow overriding derived nsec or extension
Browse files Browse the repository at this point in the history
  • Loading branch information
benthecarman committed Jan 19, 2024
1 parent bf1bb63 commit 9d877c8
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 37 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion mutiny-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ reqwest = { version = "0.11", default-features = false, features = ["json"] }
async-trait = "0.1.68"
url = { version = "2.3.1", features = ["serde"] }
nostr = { version = "0.27.0", default-features = false, features = ["nip05", "nip47", "nip57"] }
nostr-sdk = { version = "0.27.0", default-features = false }
nostr-sdk = { version = "0.27.0", default-features = false, features = ["nip05", "nip47", "nip57"] }
cbc = { version = "0.1", features = ["alloc"] }
aes = { version = "0.8" }
jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] }
Expand Down Expand Up @@ -84,6 +84,9 @@ js-sys = "0.3.65"
gloo-net = { version = "0.4.0" }
instant = { version = "0.1", features = ["wasm-bindgen"] }
getrandom = { version = "0.2", features = ["js"] }
# add nip07 feature for wasm32
nostr = { version = "0.27.0", default-features = false, features = ["nip05", "nip07", "nip47", "nip57"] }
nostr-sdk = { version = "0.27.0", default-features = false, features = ["nip05", "nip07", "nip47", "nip57"] }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt"] }
Expand Down
10 changes: 10 additions & 0 deletions mutiny-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ pub enum MutinyError {
/// Error getting nostr data
#[error("Failed to get nostr data.")]
NostrError,
/// Error with Nip07 Extension
#[error("Error with NIP-07 extension")]
Nip07Extension,
/// Incorrect password entered.
#[error("Incorrect password entered.")]
IncorrectPassword,
Expand Down Expand Up @@ -463,6 +466,13 @@ impl From<nostr::nips::nip04::Error> for MutinyError {
}
}

#[cfg(target_arch = "wasm32")]
impl From<nostr::nips::nip07::Error> for MutinyError {
fn from(_e: nostr::nips::nip07::Error) -> Self {
Self::Nip07Extension
}
}

impl From<nostr::nips::nip57::Error> for MutinyError {
fn from(_e: nostr::nips::nip57::Error) -> Self {
Self::NostrError
Expand Down
10 changes: 9 additions & 1 deletion mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ use std::{str::FromStr, sync::atomic::Ordering};
use uuid::Uuid;

use crate::labels::LabelItem;
use crate::nostr::NostrKeySource;
use crate::utils::parse_profile_metadata;
#[cfg(test)]
use mockall::{automock, predicate::*};
Expand Down Expand Up @@ -550,6 +551,7 @@ pub struct MutinyWalletConfig {

pub struct MutinyWalletBuilder<S: MutinyStorage> {
xprivkey: ExtendedPrivKey,
nostr_key_source: NostrKeySource,
storage: S,
config: Option<MutinyWalletConfig>,
session_id: Option<String>,
Expand All @@ -567,6 +569,7 @@ impl<S: MutinyStorage> MutinyWalletBuilder<S> {
MutinyWalletBuilder::<S> {
xprivkey,
storage,
nostr_key_source: NostrKeySource::Derived,
config: None,
session_id: None,
network: None,
Expand Down Expand Up @@ -607,6 +610,10 @@ impl<S: MutinyStorage> MutinyWalletBuilder<S> {
self.subscription_url = Some(subscription_url);
}

pub fn with_nostr_key_source(&mut self, key_source: NostrKeySource) {
self.nostr_key_source = key_source;
}

pub fn do_not_connect_peers(&mut self) {
self.do_not_connect_peers = true;
}
Expand Down Expand Up @@ -664,6 +671,7 @@ impl<S: MutinyStorage> MutinyWalletBuilder<S> {
// create nostr manager
let nostr = Arc::new(NostrManager::from_mnemonic(
self.xprivkey,
self.nostr_key_source,
self.storage.clone(),
logger.clone(),
stop.clone(),
Expand Down Expand Up @@ -836,7 +844,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
log_warn!(logger, "Failed to clear expired NWC invoices: {e}");
}

let client = Client::new(&nostr.primary_key);
let client = Client::new(nostr.primary_key.clone());

client
.add_relays(nostr.get_relays())
Expand Down
90 changes: 70 additions & 20 deletions mutiny-core/src/nostr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use nostr::key::{SecretKey, XOnlyPublicKey};
use nostr::nips::nip47::*;
use nostr::prelude::{decrypt, encrypt};
use nostr::{Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Tag, Timestamp};
use nostr_sdk::{Client, RelayPoolNotification};
use nostr_sdk::{Client, ClientSigner, RelayPoolNotification};
use std::collections::HashSet;
use std::sync::{atomic::Ordering, Arc, RwLock};
use std::time::Duration;
Expand Down Expand Up @@ -64,13 +64,26 @@ pub enum ProfileType {
Normal { name: String },
}

#[derive(Debug, Clone)]
pub enum NostrKeySource {
/// We derive the nostr key from our mutiny seed
Derived,
/// Import nsec from the user
Imported(SecretKey),
/// Get keys from NIP-07 extension
#[cfg(target_arch = "wasm32")]
Extension(XOnlyPublicKey),
}

/// Manages Nostr keys and has different utilities for nostr specific things
#[derive(Clone)]
pub struct NostrManager<S: MutinyStorage> {
/// Extended private key that is the root seed of the wallet
xprivkey: ExtendedPrivKey,
/// Primary key used for nostr, this will be used for signing events
pub primary_key: Keys,
pub(crate) primary_key: ClientSigner,
/// Primary key's public key
pub public_key: XOnlyPublicKey,
/// Separate profiles for each nostr wallet connect string
pub(crate) nwc: Arc<RwLock<Vec<NostrWalletConnect>>>,
pub storage: S,
Expand Down Expand Up @@ -117,7 +130,6 @@ impl<S: MutinyStorage> NostrManager<S> {
fn get_dm_filters(&self) -> Result<Vec<Filter>, MutinyError> {
let contacts = self.storage.get_contacts()?;
let last_sync_time = self.storage.get_dm_sync_time()?;
let me = self.primary_key.public_key();
let npubs: HashSet<XOnlyPublicKey> = contacts.into_values().flat_map(|c| c.npub).collect();

// if we haven't synced before, use now and save to storage
Expand All @@ -132,14 +144,14 @@ impl<S: MutinyStorage> NostrManager<S> {

let sent_dm_filter = Filter::new()
.kind(Kind::EncryptedDirectMessage)
.author(me)
.author(self.public_key)
.pubkeys(npubs.clone())
.since(time_stamp);

let received_dm_filter = Filter::new()
.kind(Kind::EncryptedDirectMessage)
.authors(npubs)
.pubkey(me)
.pubkey(self.public_key)
.since(time_stamp);

Ok(vec![sent_dm_filter, received_dm_filter])
Expand Down Expand Up @@ -483,7 +495,7 @@ impl<S: MutinyStorage> NostrManager<S> {
});

if let Some(info_event) = info_event {
let client = Client::new(&self.primary_key);
let client = Client::new(self.primary_key.clone());

client
.add_relay(profile.relay.as_str())
Expand Down Expand Up @@ -543,7 +555,7 @@ impl<S: MutinyStorage> NostrManager<S> {
});

if let Some(nwc) = nwc {
let client = Client::new(&self.primary_key);
let client = Client::new(self.primary_key.clone());

client
.add_relays(vec![relay, profile.relay.to_string()])
Expand Down Expand Up @@ -616,7 +628,7 @@ impl<S: MutinyStorage> NostrManager<S> {
nwc: NostrWalletConnect,
inv: PendingNwcInvoice,
) -> Result<EventId, MutinyError> {
let client = Client::new(&self.primary_key);
let client = Client::new(self.primary_key.clone());

client
.add_relay(nwc.profile.relay.as_str())
Expand Down Expand Up @@ -754,7 +766,7 @@ impl<S: MutinyStorage> NostrManager<S> {
// doesn't work in test environment
#[cfg(not(test))]
{
let client = Client::new(&self.primary_key);
let client = Client::new(self.primary_key.clone());

client
.add_relays(self.get_relays())
Expand Down Expand Up @@ -854,12 +866,7 @@ impl<S: MutinyStorage> NostrManager<S> {
// update sync time
self.storage.set_dm_sync_time(event.created_at.as_u64())?;

// todo we should handle NIP-44 as well
let decrypted = decrypt(
&self.primary_key.secret_key()?,
&event.pubkey,
&event.content,
)?;
let decrypted = self.decrypt_dm(event.pubkey, &event.content).await?;

if let Ok(invoice) = Bolt11Invoice::from_str(&decrypted) {
self.save_pending_nwc_invoice(None, event.id, event.pubkey, invoice)
Expand Down Expand Up @@ -1087,6 +1094,27 @@ impl<S: MutinyStorage> NostrManager<S> {
Ok(None)
}

/// Decrypts a DM using the primary key
pub async fn decrypt_dm(
&self,
pubkey: XOnlyPublicKey,
message: &str,
) -> Result<String, MutinyError> {
// todo we should handle NIP-44 as well
match &self.primary_key {
ClientSigner::Keys(key) => {
let secret = key.secret_key().expect("must have");
let decrypted = decrypt(&secret, &pubkey, message)?;
Ok(decrypted)
}
#[cfg(target_arch = "wasm32")]
ClientSigner::NIP07(nip07) => {
let decrypted = nip07.nip04_decrypt(pubkey, message).await?;
Ok(decrypted)
}
}
}

/// Derives the client and server keys for Nostr Wallet Connect given a profile index
/// The left key is the client key and the right key is the server key
pub(crate) fn derive_nwc_keys<C: Signing>(
Expand Down Expand Up @@ -1140,14 +1168,34 @@ impl<S: MutinyStorage> NostrManager<S> {
/// Creates a new NostrManager
pub fn from_mnemonic(
xprivkey: ExtendedPrivKey,
key_source: NostrKeySource,
storage: S,
logger: Arc<MutinyLogger>,
stop: Arc<AtomicBool>,
) -> Result<Self, MutinyError> {
let context = Secp256k1::new();

// generate the default primary key
let primary_key = Self::derive_nostr_key(&context, xprivkey, 0, None, None)?;
// use provided nsec, otherwise generate it from seed
let (primary_key, public_key) = match key_source {
NostrKeySource::Derived => {
let keys = Self::derive_nostr_key(&context, xprivkey, 0, None, None)?;
let public_key = keys.public_key();
let signer = ClientSigner::Keys(keys);
(signer, public_key)
}
NostrKeySource::Imported(nsec) => {
let keys = Keys::new(nsec);
let public_key = keys.public_key();
let signer = ClientSigner::Keys(keys);
(signer, public_key)
}
#[cfg(target_arch = "wasm32")]
NostrKeySource::Extension(public_key) => {
let nip07 = nostr::prelude::Nip07Signer::new()?;
let signer = ClientSigner::NIP07(nip07);
(signer, public_key)
}
};

// get from storage
let profiles: Vec<Profile> = storage.get_data(NWC_STORAGE_KEY)?.unwrap_or_default();
Expand All @@ -1161,6 +1209,7 @@ impl<S: MutinyStorage> NostrManager<S> {
Ok(Self {
xprivkey,
primary_key,
public_key,
nwc: Arc::new(RwLock::new(nwc)),
storage,
pending_nwc_lock: Arc::new(Mutex::new(())),
Expand Down Expand Up @@ -1223,7 +1272,8 @@ mod test {

let stop = Arc::new(AtomicBool::new(false));

NostrManager::from_mnemonic(xprivkey, storage, logger, stop).unwrap()
NostrManager::from_mnemonic(xprivkey, NostrKeySource::Derived, storage, logger, stop)
.unwrap()
}

#[test]
Expand All @@ -1234,7 +1284,7 @@ mod test {

let dm = EventBuilder::encrypted_direct_msg(
&user,
nostr_manager.primary_key.public_key(),
nostr_manager.public_key,
"not an invoice",
None,
)
Expand Down Expand Up @@ -1262,7 +1312,7 @@ mod test {

let dm = EventBuilder::encrypted_direct_msg(
&user,
nostr_manager.primary_key.public_key(),
nostr_manager.public_key,
invoice.to_string(),
None,
)
Expand Down
Loading

0 comments on commit 9d877c8

Please sign in to comment.