diff --git a/Cargo.toml b/Cargo.toml index 859ed72..314a672 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,12 +15,28 @@ name = "lnurl" [features] # Include nothing by default default = [] -service = ["bitcoin_hashes", "hex", "secp256k1"] +service = ["hex", "secp256k1"] [dependencies] -bitcoin_hashes = { version = "^0.7.6", optional = true } hex = { version = "0.4.2", optional = true } secp256k1 = { version = "^0.17.2", optional = true } serde = { version = "^1.0.93", features =["derive"]} serde_json = "^1.0.39" +[dev-dependencies] +bech32 = "0.7.1" +hex = "0.4.2" +image = "0.22.3" +qrcode = "0.11.0" +rand = "0.7.3" +serde_derive = "1.0" +tokio = { version = "0.2", features = ["macros"] } +tracing = "0.1" +tracing-subscriber = "0.2" +warp = "0.2.4" + +[examples] + +[[example]] +name = "lnurl_auth" +required-features = ["service"] diff --git a/examples/lnurl_auth.rs b/examples/lnurl_auth.rs new file mode 100644 index 0000000..9e52415 --- /dev/null +++ b/examples/lnurl_auth.rs @@ -0,0 +1,195 @@ +use std::env; +use warp; +use warp::Filter; + +use tracing_subscriber::fmt::format::FmtSpan; + +#[tokio::main] +async fn main() { + let filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "tracing=info,warp=debug".to_owned()); + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_span_events(FmtSpan::CLOSE) + .init(); + + let url = env::var("SERVICE_URL").unwrap(); + let verifier = lnurl::service::AuthVerifier::new(); + let db = model::new_db(); + let api = filter::api(url, db, verifier).with(warp::log("api")); + warp::serve(api).run(([127, 0, 0, 1], 8383)).await; +} + +mod filter { + use super::auth; + use super::handler; + use super::model::DB; + use lnurl::service::AuthVerifier; + use warp::Filter; + + pub fn api( + url: String, + db: DB, + verifier: AuthVerifier, + ) -> impl Filter + Clone { + auth(db.clone(), verifier) + .or(login(db.clone(), url)) + .or(users_list(db)) + .with(warp::trace::request()) + } + + /// GET /auth?sig=&key= + pub fn auth( + db: DB, + verifier: AuthVerifier, + ) -> impl Filter + Clone { + warp::path!("auth") + .and(warp::get()) + .and(with_db(db)) + .and(with_verifier(verifier)) + .and(warp::query::()) + .and_then(handler::auth) + } + + /// GET /login + pub fn login( + db: DB, + url: String, + ) -> impl Filter + Clone { + warp::path!("login") + .and(warp::get()) + .and(with_db(db)) + .and(warp::any().map(move || url.clone())) + .and_then(handler::login) + } + + /// GET /users + pub fn users_list( + db: DB, + ) -> impl Filter + Clone { + warp::path!("users") + .and(warp::get()) + .and(with_db(db)) + .and_then(handler::list_users) + } + + fn with_db(db: DB) -> impl Filter + Clone { + warp::any().map(move || db.clone()) + } + + fn with_verifier( + v: AuthVerifier, + ) -> impl Filter + Clone { + warp::any().map(move || v.clone()) + } +} + +mod auth { + use serde::{Deserialize, Serialize}; + + #[derive(Deserialize, Serialize)] + pub struct Auth { + pub k1: String, + pub sig: String, + pub key: String, + } +} + +mod handler { + use super::auth; + use super::img; + use super::model::{Sessions, User, DB}; + use hex::encode; + use rand::random; + use std::convert::Infallible; + + pub async fn auth( + db: DB, + verifier: lnurl::service::AuthVerifier, + credentials: auth::Auth, + ) -> Result { + let mut sessions = db.lock().await; + if sessions.get(&credentials.k1).is_none() { + return Ok(warp::reply::json(&lnurl::Response::Error { + reason: format!("{} does not exist", &credentials.k1), + })); + } + let res = verifier + .verify(&credentials.k1, &credentials.sig, &credentials.key) + .unwrap(); + if !res { + return Ok(warp::reply::json(&lnurl::Response::Error { + reason: format!( + "{}, {}, {}", + &credentials.k1, &credentials.sig, &credentials.key + ), + })); + } + sessions.insert( + credentials.k1, + Some(User { + pk: credentials.key, + }), + ); + Ok(warp::reply::json(&lnurl::Response::Ok { + event: Some(lnurl::Event::LoggedIn), + })) + } + + pub async fn list_users(db: DB) -> Result { + let sessions = db.lock().await; + let users = sessions + .values() + .filter_map(|o| o.as_ref()) + .map(|u| u.clone()) + .collect(); + let list = Sessions { users: users }; + Ok(warp::reply::json(&list)) + } + + pub async fn login(db: DB, url: String) -> Result { + let challenge: [u8; 32] = random(); + let k1 = encode(challenge); + let url = format!("{}/auth?tag=login&k1={}", url, &k1); + let mut sessions = db.lock().await; + sessions.insert(k1, None); + Ok(warp::http::Response::builder().body(img::create_qrcode(&url))) + } +} + +mod img { + use bech32::ToBase32; + use image::{DynamicImage, ImageOutputFormat, Luma}; + use qrcode::QrCode; + + pub fn create_qrcode(url: &str) -> Vec { + let encoded = bech32::encode("lnurl", url.as_bytes().to_base32()).unwrap(); + let code = QrCode::new(encoded.to_string()).unwrap(); + let mut image: Vec = Vec::new(); + let img = DynamicImage::ImageLuma8(code.render::>().build()); + img.write_to(&mut image, ImageOutputFormat::PNG).unwrap(); + image + } +} + +mod model { + use serde_derive::{Deserialize, Serialize}; + use std::collections::HashMap; + use std::sync::Arc; + use tokio::sync::Mutex; + + #[derive(Debug, Deserialize, Serialize, Clone)] + pub struct Sessions { + pub users: Vec, + } + + #[derive(Debug, Deserialize, Serialize, Clone)] + pub struct User { + pub pk: String, + } + + pub type DB = Arc>>>; + + pub fn new_db() -> DB { + Arc::new(Mutex::new(HashMap::new())) + } +} diff --git a/src/service.rs b/src/service.rs index 84c23fc..a3ab235 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,17 +1,17 @@ -extern crate bitcoin_hashes; extern crate hex; extern crate secp256k1; -use bitcoin_hashes::{sha256, Hash}; use secp256k1::{Error, Message, PublicKey, Secp256k1, Signature, Verification, VerifyOnly}; /// VerifierError is the AuthVerifier errors. +#[derive(Debug)] pub enum VerifierError { Secp256k1Error(Error), HexError(hex::FromHexError), } /// AuthVerifier verifies the secp256k1 signature of a message with a given pubkey. +#[derive(Clone)] pub struct AuthVerifier { secp: Secp256k1, } @@ -40,49 +40,9 @@ pub fn verify_sig( sig: &[u8], pubkey: &[u8], ) -> Result { - let msg = sha256::Hash::hash(msg); let msg = Message::from_slice(&msg)?; - let sig = Signature::from_compact(sig)?; + let sig = Signature::from_der(sig)?; let pubkey = PublicKey::from_slice(pubkey)?; - - Ok(secp.verify(&msg, &sig, &pubkey).is_ok()) -} - -#[cfg(test)] -mod tests { - use super::*; - - extern crate bitcoin_hashes; - use secp256k1::{Error, Message, Secp256k1, SecretKey, Signature, Signing}; - - fn sign( - secp: &Secp256k1, - msg: &[u8], - seckey: [u8; 32], - ) -> Result { - let msg = sha256::Hash::hash(msg); - let msg = Message::from_slice(&msg)?; - let seckey = SecretKey::from_slice(&seckey)?; - Ok(secp.sign(&msg, &seckey)) - } - - #[test] - fn test_verify_sig() { - let secp = Secp256k1::new(); - let seckey = [ - 59, 148, 11, 85, 134, 130, 61, 253, 2, 174, 59, 70, 27, 180, 51, 107, 94, 203, 174, - 253, 102, 39, 170, 146, 46, 252, 4, 143, 236, 12, 136, 28, - ]; - let pubkey = [ - 2, 29, 21, 35, 7, 198, 183, 43, 14, 208, 65, 139, 14, 112, 205, 128, 231, 245, 41, 91, - 141, 134, 245, 114, 45, 63, 82, 19, 251, 210, 57, 79, 54, - ]; - let msg = b"This is some message"; - - let signature = sign(&secp, msg, seckey).unwrap(); - - let serialize_sig = signature.serialize_compact(); - - assert!(verify_sig(&secp, msg, &serialize_sig, &pubkey).unwrap()); - } + secp.verify(&msg, &sig, &pubkey)?; + Ok(true) }