Skip to content

Commit

Permalink
relay-builder: add NIP42 support
Browse files Browse the repository at this point in the history
Signed-off-by: Yuki Kishimoto <[email protected]>
  • Loading branch information
yukibtc committed Dec 3, 2024
1 parent 8ea8f7d commit cc713e1
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
* nostr: add `Tags::challenge` method ([Yuki Kishimoto])
* database: impl PartialEq and Eq for `Events` ([Yuki Kishimoto])
* sdk: automatically resend event after NIP-42 authentication ([Yuki Kishimoto])
* relay-builder: add NIP42 support ([Yuki Kishimoto])

### Fixed

Expand Down
45 changes: 45 additions & 0 deletions crates/nostr-relay-builder/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,41 @@ pub struct RelayTestOptions {
pub unresponsive_connection: Option<Duration>,
}

/// NIP42 mode
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RelayBuilderNip42Mode {
/// Require authentication for writing
Write,
/// Require authentication for reading
Read,
/// Always require authentication
#[default]
Both,
}

impl RelayBuilderNip42Mode {
/// Check if is [`RelayBuilderNip42Mode::Read`] or [`RelayBuilderNip42Mode::Both`]
#[inline]
pub fn is_read(&self) -> bool {
matches!(self, Self::Read | Self::Both)
}

/// Check if is [`RelayBuilderNip42Mode::Write`] or [`RelayBuilderNip42Mode::Both`]
#[inline]
pub fn is_write(&self) -> bool {
matches!(self, Self::Write | Self::Both)
}
}

/// NIP42 options
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RelayBuilderNip42 {
/// Mode
pub mode: RelayBuilderNip42Mode,
// /// Allowed public keys
// pub allowed: HashSet<PublicKey>,
}

/// Relay builder
#[derive(Debug)]
pub struct RelayBuilder {
Expand All @@ -113,6 +148,8 @@ pub struct RelayBuilder {
pub(crate) mode: RelayBuilderMode,
/// Rate limit
pub(crate) rate_limit: RateLimit,
/// NIP42 options
pub(crate) nip42: Option<RelayBuilderNip42>,
/// Tor hidden service
#[cfg(feature = "tor")]
pub(crate) tor: Option<RelayBuilderHiddenService>,
Expand All @@ -135,6 +172,7 @@ impl Default for RelayBuilder {
})),
mode: RelayBuilderMode::default(),
rate_limit: RateLimit::default(),
nip42: None,
#[cfg(feature = "tor")]
tor: None,
max_connections: None,
Expand Down Expand Up @@ -183,6 +221,13 @@ impl RelayBuilder {
self
}

/// Require NIP42 authentication
#[inline]
pub fn nip42(mut self, opts: RelayBuilderNip42) -> Self {
self.nip42 = Some(opts);
self
}

/// Set tor options
#[inline]
#[cfg(feature = "tor")]
Expand Down
100 changes: 94 additions & 6 deletions crates/nostr-relay-builder/src/local/inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ use nostr_database::prelude::*;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{broadcast, Semaphore};

use super::session::{RateLimiterResponse, Session, Tokens};
use super::session::{Nip42Session, RateLimiterResponse, Session, Tokens};
use super::util;
use crate::builder::{RateLimit, RelayBuilder, RelayBuilderMode, RelayTestOptions};
use crate::builder::{
RateLimit, RelayBuilder, RelayBuilderMode, RelayBuilderNip42, RelayTestOptions,
};
use crate::error::Error;

type WsTx = SplitSink<WebSocketStream<TcpStream>, Message>;
Expand All @@ -36,6 +38,7 @@ pub(super) struct InnerLocalRelay {
min_pow: Option<u8>, // TODO: use AtomicU8 to allow to change it?
#[cfg(feature = "tor")]
hidden_service: Option<String>,
nip42: Option<RelayBuilderNip42>,
test: RelayTestOptions,
}

Expand Down Expand Up @@ -98,6 +101,7 @@ impl InnerLocalRelay {
min_pow: builder.min_pow,
#[cfg(feature = "tor")]
hidden_service,
nip42: builder.nip42,
test: builder.test,
};

Expand Down Expand Up @@ -169,6 +173,7 @@ impl InnerLocalRelay {

let mut session: Session = Session {
subscriptions: HashMap::new(),
nip42: Nip42Session::default(),
tokens: Tokens::new(self.rate_limit.notes_per_minute),
};

Expand Down Expand Up @@ -272,6 +277,38 @@ impl InnerLocalRelay {
}
}

// Check NIP42
if let Some(nip42) = &self.nip42 {
// TODO: check if public key allowed

// Check mode and if it's authenticated
if nip42.mode.is_write() && !session.nip42.is_authenticated() {
// Generate and send AUTH challenge
self.send_msg(
ws_tx,
RelayMessage::Auth {
challenge: session.nip42.generate_challenge(),
},
)
.await?;

// Return error
return self
.send_msg(
ws_tx,
RelayMessage::Ok {
event_id: event.id,
status: false,
message: format!(
"{}: you must auth",
MachineReadablePrefix::AuthRequired
),
},
)
.await;
}
}

// Check if event already exists
let event_status = self.database.check_id(&event.id).await?;
match event_status {
Expand Down Expand Up @@ -437,6 +474,37 @@ impl InnerLocalRelay {
.await;
}

// Check NIP42
if let Some(nip42) = &self.nip42 {
// TODO: check if public key allowed

// Check mode and if it's authenticated
if nip42.mode.is_read() && !session.nip42.is_authenticated() {
// Generate and send AUTH challenge
self.send_msg(
ws_tx,
RelayMessage::Auth {
challenge: session.nip42.generate_challenge(),
},
)
.await?;

// Return error
return self
.send_msg(
ws_tx,
RelayMessage::Closed {
subscription_id,
message: format!(
"{}: you must auth",
MachineReadablePrefix::AuthRequired
),
},
)
.await;
}
}

// Update session subscriptions
session
.subscriptions
Expand Down Expand Up @@ -474,10 +542,30 @@ impl InnerLocalRelay {
session.subscriptions.remove(&subscription_id);
Ok(())
}
ClientMessage::Auth(_event) => {
// TODO
Ok(())
}
ClientMessage::Auth(event) => match session.nip42.check_challenge(&event) {
Ok(()) => {
self.send_msg(
ws_tx,
RelayMessage::Ok {
event_id: event.id,
status: true,
message: String::new(),
},
)
.await
}
Err(e) => {
self.send_msg(
ws_tx,
RelayMessage::Ok {
event_id: event.id,
status: false,
message: format!("{}: {e}", MachineReadablePrefix::AuthRequired),
},
)
.await
}
},
ClientMessage::NegOpen { .. }
| ClientMessage::NegMsg { .. }
| ClientMessage::NegClose { .. } => Ok(()),
Expand Down
64 changes: 62 additions & 2 deletions crates/nostr-relay-builder/src/local/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,78 @@
// Copyright (c) 2023-2024 Rust Nostr Developers
// Distributed under the MIT software license

use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};

use nostr::{Filter, SubscriptionId};
use nostr::{Event, Filter, PublicKey, Result, SubscriptionId, Timestamp};

pub(super) enum RateLimiterResponse {
Allowed,
Limited,
}

#[derive(Default)]
pub(super) struct Nip42Session {
/// Is authenticated
pub public_key: Option<PublicKey>,
/// Challenges
pub challenges: HashSet<String>,
}

impl Nip42Session {
/// Get or generate challenge
pub fn generate_challenge(&mut self) -> String {
// TODO: alternatives?

// Too many challenges without reply
if self.challenges.len() > 20 {
// Clean to avoid possible attack where client never complete auth
self.challenges.clear();
}

let challenge: String = SubscriptionId::generate().to_string();
self.challenges.insert(challenge.clone());
challenge
}

#[inline]
pub fn is_authenticated(&self) -> bool {
self.public_key.is_some()
}

pub fn check_challenge(&mut self, event: &Event) -> Result<(), String> {
match event.tags.challenge() {
Some(challenge) => {
// Tried to remove challenge but wasn't in the set: return false.
if !self.challenges.remove(challenge) {
return Err(String::from("received invalid challenge"));
}

// Check created_at
let now = Timestamp::now();
let diff: u64 = now.as_u64().abs_diff(event.created_at.as_u64());
if diff > 120 {
return Err(String::from("challenge is too old (max allowed 2 min)"));
}

// Verify event
event.verify().map_err(|e| e.to_string())?;

// TODO: check `relay` tag

// Mark as authenticated
self.public_key = Some(event.pubkey);

Ok(())
}
None => Err(String::from("challenge not found")),
}
}
}

pub(super) struct Session {
pub subscriptions: HashMap<SubscriptionId, Vec<Filter>>,
pub nip42: Nip42Session,
pub tokens: Tokens,
}

Expand Down

0 comments on commit cc713e1

Please sign in to comment.