Skip to content

Commit

Permalink
Merge pull request #1 from edouardparis/add-example-lnurl_auth
Browse files Browse the repository at this point in the history
Add example lnurl auth
  • Loading branch information
edouardparis authored Jun 19, 2021
2 parents 9d10152 + 7b448ac commit 67887f4
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 97 deletions.
22 changes: 19 additions & 3 deletions Cargo.toml
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"
Expand All @@ -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"]
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ _Readings about **lnurl**_
## Progress

- [x] lnurl-withdraw
- [ ] lnurl-auth
- [x] lnurl-auth
- [ ] lnurl-pay
- [ ] lnurl-channel

Expand Down Expand Up @@ -57,6 +57,3 @@ if let Err(_) = invoice.parse::<lightning_invoice::SignedRawInvoice>() {
.body(Body::from(res)).unwrap())
}
```

See [lnurl-examples](https://github.com/edouardparis/lnurl-examples)

11 changes: 11 additions & 0 deletions examples/README.md
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`
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::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()))
}
}
48 changes: 48 additions & 0 deletions src/auth.rs
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)
}
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Loading

0 comments on commit 67887f4

Please sign in to comment.