Skip to content

Commit

Permalink
examples: add lnurl_auth
Browse files Browse the repository at this point in the history
  • Loading branch information
edouardparis committed Jun 19, 2021
1 parent 9d10152 commit 9fc47c7
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 47 deletions.
20 changes: 18 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
195 changes: 195 additions & 0 deletions examples/lnurl_auth.rs
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::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<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::service::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()))
}
}
50 changes: 5 additions & 45 deletions src/service.rs
Original file line number Diff line number Diff line change
@@ -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<VerifyOnly>,
}
Expand Down Expand Up @@ -40,49 +40,9 @@ pub fn verify_sig<C: Verification>(
sig: &[u8],
pubkey: &[u8],
) -> Result<bool, Error> {
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<C: Signing>(
secp: &Secp256k1<C>,
msg: &[u8],
seckey: [u8; 32],
) -> Result<Signature, Error> {
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)
}

0 comments on commit 9fc47c7

Please sign in to comment.