1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
use crate::{error::MutinyError, logging::MutinyLogger};
use anyhow::anyhow;
use bdk_chain::collections::HashMap;
use bitcoin::hashes::hex::FromHex;
use bitcoin::secp256k1::{ecdsa, All, Message, PublicKey, Secp256k1, SecretKey};
use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey};
use lightning::util::logger::*;
use lightning::{log_error, log_info};
use lnurl::lnurl::LnUrl;
use lnurl::{AsyncClient as LnUrlClient, Response};
use std::str::FromStr;
use std::sync::Arc;
use url::Url;

#[derive(Clone)]
pub struct AuthManager {
    hashing_key: SecretKey,
    xprivkey: ExtendedPrivKey,
    context: Secp256k1<All>,
}

impl AuthManager {
    pub fn new(xprivkey: ExtendedPrivKey) -> Result<Self, MutinyError> {
        let context = Secp256k1::new();

        let base_path = DerivationPath::from_str("m/138'/0")?;
        let key = xprivkey.derive_priv(&context, &base_path)?;
        let hashing_key = key.private_key;

        Ok(Self {
            hashing_key,
            xprivkey,
            context,
        })
    }

    pub(crate) fn get_secret_key(&self, url: Url) -> Result<SecretKey, MutinyError> {
        let path = lnurl::get_derivation_path(self.hashing_key.secret_bytes(), &url)?;
        let key = self
            .xprivkey
            .derive_priv(&self.context, &path)
            .map_err(|e| MutinyError::Other(anyhow!("Error deriving key for path {path}: {e}")))?;
        Ok(key.private_key)
    }

    pub fn sign(
        &self,
        url: Url,
        k1: &[u8; 32],
    ) -> Result<(ecdsa::Signature, PublicKey), MutinyError> {
        let sk = self.get_secret_key(url)?;
        let pubkey = sk.public_key(&self.context);

        let msg = Message::from_slice(k1).expect("32 bytes, guaranteed by type");
        let sig = self.context.sign_ecdsa(&msg, &sk);

        Ok((sig, pubkey))
    }
}

pub(crate) async fn make_lnurl_auth_connection(
    auth: AuthManager,
    lnurl_client: Arc<LnUrlClient>,
    lnurl: LnUrl,
    logger: Arc<MutinyLogger>,
) -> Result<(), MutinyError> {
    let url = Url::parse(&lnurl.url)?;
    let query_pairs: HashMap<String, String> = url
        .query_pairs()
        .map(|(k, v)| (k.to_string(), v.to_string()))
        .collect();

    let k1 = query_pairs.get("k1").ok_or(MutinyError::LnUrlFailure)?;
    let k1: [u8; 32] = FromHex::from_hex(k1).map_err(|_| MutinyError::LnUrlFailure)?;
    let (sig, key) = auth.sign(url.clone(), &k1)?;

    let response = lnurl_client.lnurl_auth(lnurl, sig, key).await;
    match response {
        Ok(Response::Ok { .. }) => {
            log_info!(logger, "LNURL auth successful!");
            Ok(())
        }
        Ok(Response::Error { reason }) => {
            log_error!(logger, "LNURL auth failed: {reason}");
            Err(MutinyError::LnUrlFailure)
        }
        Err(e) => {
            log_error!(logger, "LNURL auth failed: {e}");
            Err(MutinyError::LnUrlFailure)
        }
    }
}

#[cfg(test)]
mod test {
    use crate::test_utils::*;
    use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
    wasm_bindgen_test_configure!(run_in_browser);

    use super::*;

    #[test]
    async fn test_create_signature() {
        let test_name = "test_create_signature";
        log!("{}", test_name);

        let auth = create_manager();

        let k1 = [0; 32];

        let (sig, pk) = auth
            .sign(Url::parse("https://mutinywallet.com").unwrap(), &k1)
            .unwrap();

        auth.context
            .verify_ecdsa(&Message::from_slice(&k1).unwrap(), &sig, &pk)
            .unwrap();
    }
}