-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from edouardparis/add-example-lnurl_auth
Add example lnurl auth
- Loading branch information
Showing
7 changed files
with
276 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[package] | ||
name = "lnurl" | ||
version = "0.1.1" | ||
version = "0.2.0" | ||
authors = ["Edouard Paris <[email protected]>"] | ||
description = "Helpers for LNURL" | ||
readme = "README.md" | ||
|
@@ -15,12 +15,28 @@ name = "lnurl" | |
[features] | ||
# Include nothing by default | ||
default = [] | ||
service = ["bitcoin_hashes", "hex", "secp256k1"] | ||
auth = ["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 = ["auth"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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::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; | ||
} | ||
|
||
mod filter { | ||
use super::auth; | ||
use super::handler; | ||
use super::model::DB; | ||
use lnurl::auth::AuthVerifier; | ||
use warp::Filter; | ||
|
||
pub fn api( | ||
url: String, | ||
db: DB, | ||
verifier: AuthVerifier, | ||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { | ||
auth(db.clone(), verifier) | ||
.or(login(db.clone(), url)) | ||
.or(users_list(db)) | ||
.with(warp::trace::request()) | ||
} | ||
|
||
/// GET /auth?sig=<sig>&key=<key> | ||
pub fn auth( | ||
db: DB, | ||
verifier: AuthVerifier, | ||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { | ||
warp::path!("auth") | ||
.and(warp::get()) | ||
.and(with_db(db)) | ||
.and(with_verifier(verifier)) | ||
.and(warp::query::<auth::Auth>()) | ||
.and_then(handler::auth) | ||
} | ||
|
||
/// GET /login | ||
pub fn login( | ||
db: DB, | ||
url: String, | ||
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + 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<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { | ||
warp::path!("users") | ||
.and(warp::get()) | ||
.and(with_db(db)) | ||
.and_then(handler::list_users) | ||
} | ||
|
||
fn with_db(db: DB) -> impl Filter<Extract = (DB,), Error = std::convert::Infallible> + Clone { | ||
warp::any().map(move || db.clone()) | ||
} | ||
|
||
fn with_verifier( | ||
v: AuthVerifier, | ||
) -> impl Filter<Extract = (AuthVerifier,), Error = std::convert::Infallible> + 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::auth::AuthVerifier, | ||
credentials: auth::Auth, | ||
) -> Result<impl warp::Reply, Infallible> { | ||
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<impl warp::Reply, Infallible> { | ||
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<impl warp::Reply, Infallible> { | ||
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<u8> { | ||
let encoded = bech32::encode("lnurl", url.as_bytes().to_base32()).unwrap(); | ||
let code = QrCode::new(encoded.to_string()).unwrap(); | ||
let mut image: Vec<u8> = Vec::new(); | ||
let img = DynamicImage::ImageLuma8(code.render::<Luma<u8>>().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<User>, | ||
} | ||
|
||
#[derive(Debug, Deserialize, Serialize, Clone)] | ||
pub struct User { | ||
pub pk: String, | ||
} | ||
|
||
pub type DB = Arc<Mutex<HashMap<String, Option<User>>>>; | ||
|
||
pub fn new_db() -> DB { | ||
Arc::new(Mutex::new(HashMap::new())) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
extern crate hex; | ||
extern crate secp256k1; | ||
|
||
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<VerifyOnly>, | ||
} | ||
|
||
impl AuthVerifier { | ||
pub fn new() -> Self { | ||
AuthVerifier { | ||
secp: Secp256k1::verification_only(), | ||
} | ||
} | ||
|
||
/// verifies the secp256k1 signature of a message with a given pubkey. | ||
pub fn verify(self, hk1: &str, hsig: &str, hpubkey: &str) -> Result<bool, VerifierError> { | ||
let msg = hex::decode(hk1).map_err(|e| VerifierError::HexError(e))?; | ||
let sig = hex::decode(hsig).map_err(|e| VerifierError::HexError(e))?; | ||
let pubkey = hex::decode(hpubkey).map_err(|e| VerifierError::HexError(e))?; | ||
return verify_sig(&self.secp, &msg, &sig, &pubkey) | ||
.map_err(|e| VerifierError::Secp256k1Error(e)); | ||
} | ||
} | ||
|
||
/// verify_sig checks if the signature of a key for a given message is valid. | ||
pub fn verify_sig<C: Verification>( | ||
secp: &Secp256k1<C>, | ||
msg: &[u8], | ||
sig: &[u8], | ||
pubkey: &[u8], | ||
) -> Result<bool, Error> { | ||
let msg = Message::from_slice(&msg)?; | ||
let sig = Signature::from_der(sig)?; | ||
let pubkey = PublicKey::from_slice(pubkey)?; | ||
secp.verify(&msg, &sig, &pubkey)?; | ||
Ok(true) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.