Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example lnurl auth #1

Merged
merged 3 commits into from
Jun 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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