Blossom Auth

This commit is contained in:
Mike Dilger 2024-11-17 14:08:57 +13:00
parent da4ec4b5c8
commit bcb0fc1c95
No known key found for this signature in database
GPG Key ID: 47581A78D4329BA4
2 changed files with 157 additions and 0 deletions

146
src/web/blossom/auth.rs Normal file
View File

@ -0,0 +1,146 @@
use crate::error::{ChorusError, Error};
use crate::globals::GLOBALS;
use base64::prelude::*;
use http::header::AUTHORIZATION;
use hyper::body::Incoming;
use hyper::Request;
use pocket_types::Event;
fn s_err(s: &str) -> Result<AuthData, Error> {
Err(ChorusError::BlossomAuthFailure(s.to_owned()).into())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AuthVerb {
Upload,
List,
Delete,
Mirror,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AuthData {
/// If a verb was included, this is it
pub verb: Option<AuthVerb>,
/// If an 'x' tag was included, this is the hash
pub hash: Option<[u8; 32]>,
}
pub fn verify_auth(request: &Request<Incoming>) -> Result<AuthData, Error> {
// Force every other error into a BlossomAuthFailure error
match verify_auth_inner(request) {
Ok(ad) => Ok(ad),
Err(e) => match e.inner {
ChorusError::BlossomAuthFailure(_) => return Err(e),
_ => return Err(ChorusError::BlossomAuthFailure(format!("{e}")).into()),
},
}
}
fn verify_auth_inner(request: &Request<Incoming>) -> Result<AuthData, Error> {
// Must have AUTHORIZATION header
let authz = match request.headers().get(AUTHORIZATION) {
Some(h) => h,
None => return s_err("Authorization Required"),
};
// Authorization header must be type "nostr"
if !authz.to_str()?.to_ascii_lowercase().starts_with("nostr ") {
return s_err("You must use the Nostr authorization scheme");
}
let base64 = match authz.to_str()?.get(6..) {
Some(x) => x,
None => return s_err("Missing auth base64 encoded event"),
};
// Authorization header must be base64
let event_bytes = BASE64_STANDARD.decode(base64)?;
// Authorization header base64 must decode to a nostr Event
let mut buffer = vec![0; base64.len()];
let (_size, event) = Event::from_json(&event_bytes, &mut buffer)?;
// Nostr event must be valid
if let Err(e) = event.verify() {
return s_err(&format!("Authorization event is invalid: {}", e));
}
// Nostr event must be signed by a chorus user
if !GLOBALS.config.read().user_keys.contains(&event.pubkey()) {
return s_err("You are not an authorized user");
}
// Event kind must be 24242
if event.kind().as_u16() != 24242 {
return s_err("Authorization event not kind 24242");
}
// Event created_at must be in the past (we give 30 seconds leeway)
use pocket_types::Time;
let now = Time::now();
if event.created_at() > now + 30 {
return s_err("Authorization event too far in the future");
}
let tags = event.tags()?;
// Expiration tag must be in the future
if let Some(v) = tags.get_value(b"expiration") {
let u = parse_u64(v)?;
let expiration = Time::from_u64(u);
if expiration < now {
return s_err("Authorization event has expired");
}
} else {
return s_err("Authorization event missing expiration tag");
}
// We let the caller check the verb and hash since those are specific
// to the endpoint (and the 'x' must be checked later on)
let verb: Option<AuthVerb> = if let Some(t) = tags.get_value(b"t") {
if t == b"upload" {
Some(AuthVerb::Upload)
} else if t == b"list" {
Some(AuthVerb::List)
} else if t == b"delete" {
Some(AuthVerb::Delete)
} else {
None
}
} else {
None
};
let hash: Option<[u8; 32]> = if let Some(v) = tags.get_value(b"x") {
let vec = hex::decode(v)?;
if vec.len() == 32 {
Some(vec.try_into().unwrap())
} else {
return s_err("Authorization event x tag is of the wrong length");
}
} else {
None
};
Ok(AuthData { verb, hash })
}
// FIXME, expose these from pocket-types
fn parse_u64(input: &[u8]) -> Result<u64, Error> {
let mut pos = 0;
let mut value: u64 = 0;
let mut any: bool = false;
while pos < input.len() && b"0123456789".contains(&input[pos]) {
any = true;
value = (value * 10) + (input[pos] - 48) as u64;
pos += 1;
}
if !any {
Err(ChorusError::General("Auth event expiration is not a number".to_string()).into())
} else {
Ok(value)
}
}

View File

@ -11,6 +11,9 @@ use http_body_util::{BodyExt, Empty};
use hyper::body::{Bytes, Incoming};
use hyper::{Request, Response};
mod auth;
use auth::verify_auth;
pub async fn handle(request: &Request<Incoming>) -> Result<Response<BoxBody<Bytes, Error>>, Error> {
match route(request).await {
Ok(response) => Ok(response),
@ -102,6 +105,8 @@ pub async fn handle_hash(
return options_response(request, "OPTIONS, HEAD, GET, DELETE");
}
let _auth_data = verify_auth(request)?;
unimplemented!()
}
@ -112,6 +117,8 @@ pub async fn handle_upload(
return options_response(request, "OPTIONS, HEAD, PUT");
}
let _auth_data = verify_auth(request)?;
unimplemented!()
}
@ -122,6 +129,8 @@ pub async fn handle_list(
return options_response(request, "OPTIONS, GET");
}
let _auth_data = verify_auth(request)?;
unimplemented!()
}
@ -132,5 +141,7 @@ pub async fn handle_mirror(
return options_response(request, "OPTIONS, PUT");
}
let _auth_data = verify_auth(request)?;
unimplemented!()
}