From 9fc47c72120820228b3ead0fb5dd6247e7276f25 Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Thu, 20 Aug 2020 09:41:16 +0200 Subject: [PATCH 1/3] examples: add lnurl_auth --- Cargo.toml | 20 ++++- examples/lnurl_auth.rs | 195 +++++++++++++++++++++++++++++++++++++++++ src/service.rs | 50 ++--------- 3 files changed, 218 insertions(+), 47 deletions(-) create mode 100644 examples/lnurl_auth.rs 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) } From b4fe78ebdc61a9f9724524b661d688aa3483a0e5 Mon Sep 17 00:00:00 2001 From: edouard Date: Sat, 19 Jun 2021 18:00:50 +0200 Subject: [PATCH 2/3] add examples README --- examples/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..38c4fa5 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# lnurl_auth + +usage: + +* `ngrok http 8383` +* `SERVICE_URL=https://xxx.ngrok.io cargo run --example lnurl_auth --features="service"` + +then: + +* get qrcode: `localhost:8383/login` +* list connected users: `localhost:8383/users` From 7b448acfbb4c1d41a769352e5aea0e70f97bb716 Mon Sep 17 00:00:00 2001 From: edouard Date: Sat, 19 Jun 2021 18:11:38 +0200 Subject: [PATCH 3/3] rename service for auth --- Cargo.toml | 6 +++--- README.md | 5 +---- examples/lnurl_auth.rs | 6 +++--- src/{service.rs => auth.rs} | 0 src/lib.rs | 4 ++-- 5 files changed, 9 insertions(+), 12 deletions(-) rename src/{service.rs => auth.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index 314a672..a8f7c57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lnurl" -version = "0.1.1" +version = "0.2.0" authors = ["Edouard Paris "] description = "Helpers for LNURL" readme = "README.md" @@ -15,7 +15,7 @@ name = "lnurl" [features] # Include nothing by default default = [] -service = ["hex", "secp256k1"] +auth = ["hex", "secp256k1"] [dependencies] hex = { version = "0.4.2", optional = true } @@ -39,4 +39,4 @@ warp = "0.2.4" [[example]] name = "lnurl_auth" -required-features = ["service"] +required-features = ["auth"] diff --git a/README.md b/README.md index 3ba79b3..f6f0a7c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ _Readings about **lnurl**_ ## Progress - [x] lnurl-withdraw -- [ ] lnurl-auth +- [x] lnurl-auth - [ ] lnurl-pay - [ ] lnurl-channel @@ -57,6 +57,3 @@ if let Err(_) = invoice.parse::() { .body(Body::from(res)).unwrap()) } ``` - -See [lnurl-examples](https://github.com/edouardparis/lnurl-examples) - diff --git a/examples/lnurl_auth.rs b/examples/lnurl_auth.rs index 9e52415..170cdf9 100644 --- a/examples/lnurl_auth.rs +++ b/examples/lnurl_auth.rs @@ -13,7 +13,7 @@ async fn main() { .init(); let url = env::var("SERVICE_URL").unwrap(); - let verifier = lnurl::service::AuthVerifier::new(); + let verifier = lnurl::auth::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; @@ -23,7 +23,7 @@ mod filter { use super::auth; use super::handler; use super::model::DB; - use lnurl::service::AuthVerifier; + use lnurl::auth::AuthVerifier; use warp::Filter; pub fn api( @@ -104,7 +104,7 @@ mod handler { pub async fn auth( db: DB, - verifier: lnurl::service::AuthVerifier, + verifier: lnurl::auth::AuthVerifier, credentials: auth::Auth, ) -> Result { let mut sessions = db.lock().await; diff --git a/src/service.rs b/src/auth.rs similarity index 100% rename from src/service.rs rename to src/auth.rs diff --git a/src/lib.rs b/src/lib.rs index 20a13b0..bafec73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; -#[cfg(feature = "service")] -pub mod service; +#[cfg(feature = "auth")] +pub mod auth; #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum Event {