commit 9ac699cb599631a19d575916995cee0bb3a77907
parent 65029bace3c827adb56f3bb0cef2af0e7ee96b55
Author: William Casarin <jb55@jb55.com>
Date: Wed, 17 Aug 2022 13:48:23 -0700
lnurl-auth support
Changelog-Added: Added LNUrl-Auth support
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
10 files changed, 354 insertions(+), 18 deletions(-)
diff --git a/lightninglink-c/lightninglink.h b/lightninglink-c/lightninglink.h
@@ -11,6 +11,8 @@
#include "lnsocket.h"
#include "commando.h"
#include "bech32.h"
+#include "secp256k1.h"
+#include "secp256k1_schnorrsig.h"
void fd_do_zero(fd_set *);
void fd_do_set(int, fd_set *);
diff --git a/lightninglink.xcodeproj/project.pbxproj b/lightninglink.xcodeproj/project.pbxproj
@@ -33,6 +33,8 @@
4CCB0E2727C979F30026461C /* ScannerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCB0E2327C979F30026461C /* ScannerCoordinator.swift */; };
4CCB0E2827C979F30026461C /* ScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCB0E2427C979F30026461C /* ScannerViewController.swift */; };
4CCB0E2B27CA71CA0026461C /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCB0E2A27CA71CA0026461C /* SetupView.swift */; };
+ 4CD7641D28A205AB00B6928F /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641C28A205AB00B6928F /* AuthView.swift */; };
+ 4CD7642028A28D7100B6928F /* Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641F28A28D7100B6928F /* Hex.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -87,6 +89,8 @@
4CCB0E2327C979F30026461C /* ScannerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerCoordinator.swift; sourceTree = "<group>"; };
4CCB0E2427C979F30026461C /* ScannerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerViewController.swift; sourceTree = "<group>"; };
4CCB0E2A27CA71CA0026461C /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
+ 4CD7641C28A205AB00B6928F /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
+ 4CD7641F28A28D7100B6928F /* Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hex.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -143,6 +147,7 @@
4C641D172788FF2F002A36C9 /* lightninglink */ = {
isa = PBXGroup;
children = (
+ 4CD7641E28A28D6700B6928F /* Util */,
4CCB0E2927CA71AA0026461C /* Views */,
4CCB0E1F27C979F30026461C /* CodeScanner */,
4C0359FE27AEEE8500FF92CE /* Info.plist */,
@@ -225,10 +230,19 @@
4C3DC97027D4829A00773021 /* SettingsView.swift */,
4C5EAC5527EE8544002A6FE5 /* ReceiveView.swift */,
4C5EAC5727EE8942002A6FE5 /* AmountInputView.swift */,
+ 4CD7641C28A205AB00B6928F /* AuthView.swift */,
);
path = Views;
sourceTree = "<group>";
};
+ 4CD7641E28A28D6700B6928F /* Util */ = {
+ isa = PBXGroup;
+ children = (
+ 4CD7641F28A28D7100B6928F /* Hex.swift */,
+ );
+ path = Util;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -361,6 +375,7 @@
buildActionMask = 2147483647;
files = (
4C5EAC5827EE8942002A6FE5 /* AmountInputView.swift in Sources */,
+ 4CD7642028A28D7100B6928F /* Hex.swift in Sources */,
4C035A0027AEF90000FF92CE /* PayView.swift in Sources */,
4C873FD527A6EF3F008C972C /* LNSocket.swift in Sources */,
4C641D1B2788FF2F002A36C9 /* ContentView.swift in Sources */,
@@ -368,6 +383,7 @@
4CCB0E2727C979F30026461C /* ScannerCoordinator.swift in Sources */,
4C5EAC5427ED30A8002A6FE5 /* Rates.swift in Sources */,
4C641D492789083E002A36C9 /* lightninglink.c in Sources */,
+ 4CD7641D28A205AB00B6928F /* AuthView.swift in Sources */,
4CCB0E2627C979F30026461C /* CodeScanner.swift in Sources */,
4CCB0E2827C979F30026461C /* ScannerViewController.swift in Sources */,
4C72DB2527DD9D9B00834545 /* QR.swift in Sources */,
diff --git a/lightninglink/LNSocket.swift b/lightninglink/LNSocket.swift
@@ -36,9 +36,8 @@ public class LNSocket {
}
func write(_ data: Data) -> Bool {
- data.withUnsafeBytes { msg in
- return lnsocket_write(self.ln, msg, UInt16(data.count)) != 0
- }
+ var msg = Array(data)
+ return lnsocket_write(self.ln, &msg, UInt16(data.count)) != 0
}
func fd() -> Int32 {
@@ -66,9 +65,8 @@ public class LNSocket {
}
func pong(ping: Data) {
- ping.withUnsafeBytes{ ping_ptr in
- lnsocket_pong(self.ln, ping_ptr, UInt16(ping.count))
- }
+ var ping_ptr = Array(ping)
+ lnsocket_pong(self.ln, &ping_ptr, UInt16(ping.count))
}
func perform_init() -> Bool {
diff --git a/lightninglink/LNUrl.swift b/lightninglink/LNUrl.swift
@@ -146,6 +146,47 @@ func handle_lnurl_request<T: Decodable>(_ url: URL, completion: @escaping (Eithe
task.resume()
}
+public struct LNUrlAuth {
+ let k1: String
+ let tag: String
+ let url: URL
+ let host: String
+}
+
+func is_lnurl_auth(_ url: URL) -> LNUrlAuth? {
+ var components = URLComponents()
+ components.query = url.query
+
+ guard let items = components.queryItems else {
+ return nil
+ }
+
+ guard let host = url.host else {
+ return nil
+ }
+
+ var k1: String? = nil
+ var tag: String? = nil
+
+ for item in items {
+ if item.name == "k1" {
+ k1 = item.value
+ } else if item.name == "tag" && item.value == "login" {
+ tag = item.value
+ }
+ }
+
+ guard let k1 = k1 else {
+ return nil
+ }
+
+ guard let tag = tag else {
+ return nil
+ }
+
+ return LNUrlAuth(k1: k1, tag: tag, url: url, host: host)
+}
+
func handle_lnurl(_ url: URL, completion: @escaping (LNUrl?) -> ()) {
let task = URLSession.shared.dataTask(with: url) { (mdata, response, error) in
guard let data = mdata else {
diff --git a/lightninglink/RPC.swift b/lightninglink/RPC.swift
@@ -93,7 +93,7 @@ public enum Decode {
}
}
- func amount_msat() -> String? {
+ func amount_msat() -> Int64? {
switch self {
case .invoice(let inv):
return inv.amount_msat
@@ -114,7 +114,7 @@ public struct InvoiceDecode: Decodable {
public var quantity_min: Int?
public var description: String?
public var node_id: String?
- public var amount_msat: String?
+ public var amount_msat: Int64?
public var vendor: String?
}
@@ -139,6 +139,10 @@ public struct ListFunds: Decodable {
public static var empty = ListFunds(outputs: [], channels: [])
}
+public struct MakeSecret: Decodable {
+ public let secret: String
+}
+
public struct Pay: Decodable {
public var destination: String
public var payment_hash: String
@@ -428,6 +432,12 @@ public func rpc_listfunds(ln: LNSocket, token: String) -> RequestRes<ListFunds>
return performRpc(ln: ln, operation: "listfunds", authToken: token, timeout_ms: default_timeout, params: params)
}
+public func rpc_makesecret(ln: LNSocket, token: String, hex: String) -> RequestRes<MakeSecret>
+{
+ let params: Array<String> = [hex]
+ return performRpc(ln: ln, operation: "makesecret", authToken: token, timeout_ms: 1000, params: params)
+}
+
public func rpc_decode(ln: LNSocket, token: String, inv: String) -> RequestRes<InvoiceDecode>
{
let params = [ inv ];
diff --git a/lightninglink/Util/Hex.swift b/lightninglink/Util/Hex.swift
@@ -0,0 +1,81 @@
+//
+// Hex.swift
+// lightninglink
+//
+// Created by William Casarin on 2022-08-09.
+//
+
+import Foundation
+
+func hexchar(_ val: UInt8) -> UInt8 {
+ if val < 10 {
+ return 48 + val;
+ }
+ if val < 16 {
+ return 97 + val - 10;
+ }
+ assertionFailure("impossiburu")
+ return 0
+}
+
+
+func hex_encode(_ data: [UInt8]) -> String {
+ var str = ""
+ for c in data {
+ let c1 = hexchar(c >> 4)
+ let c2 = hexchar(c & 0xF)
+
+ str.append(Character(Unicode.Scalar(c1)))
+ str.append(Character(Unicode.Scalar(c2)))
+ }
+ return str
+}
+
+
+func char_to_hex(_ c: UInt8) -> UInt8?
+{
+ // 0 && 9
+ if (c >= 48 && c <= 57) {
+ return c - 48 // 0
+ }
+ // a && f
+ if (c >= 97 && c <= 102) {
+ return c - 97 + 10;
+ }
+ // A && F
+ if (c >= 65 && c <= 70) {
+ return c - 65 + 10;
+ }
+ return nil;
+}
+
+
+func hex_decode(_ str: String) -> [UInt8]?
+{
+ if str.count == 0 {
+ return nil
+ }
+ var ret: [UInt8] = []
+ let chars = Array(str.utf8)
+ var i: Int = 0
+ for c in zip(chars, chars[1...]) {
+ i += 1
+
+ if i % 2 == 0 {
+ continue
+ }
+
+ guard let c1 = char_to_hex(c.0) else {
+ return nil
+ }
+
+ guard let c2 = char_to_hex(c.1) else {
+ return nil
+ }
+
+ ret.append((c1 << 4) | c2)
+ }
+
+ return ret
+}
+
diff --git a/lightninglink/Views/AuthView.swift b/lightninglink/Views/AuthView.swift
@@ -0,0 +1,164 @@
+//
+// AuthView.swift
+// lightninglink
+//
+// Created by William Casarin on 2022-08-08.
+//
+
+import SwiftUI
+import CryptoKit
+import CommonCrypto
+
+
+struct AuthView: View {
+ let auth: LNUrlAuth
+ let lnlink: LNLink
+ @State var error: String? = nil
+
+ @Environment(\.dismiss) var dismiss
+
+ var body: some View {
+ VStack(spacing: 20) {
+ Text(auth.url.host ?? "Unknown")
+ .font(.largeTitle)
+
+ Button(action: { login() }) {
+ Text(auth.tag.capitalized)
+ .padding()
+ .font(.largeTitle)
+ }
+ .background {
+ Color.accentColor
+ }
+ .foregroundColor(Color.white)
+ .cornerRadius(20)
+
+ if let error = error {
+ Text(error)
+ .foregroundColor(Color.red)
+ .padding(20)
+ }
+ }
+ }
+
+ func login() {
+ let ln = LNSocket()
+ guard ln.connect_and_init(node_id: lnlink.node_id, host: lnlink.host) else {
+ error = "Could not connect to node"
+ return
+ }
+ guard let hex = make_secret_hex(self.auth) else {
+ error = "Could not make secret"
+ return
+ }
+ Task.init {
+ let res = rpc_makesecret(ln: ln, token: lnlink.token, hex: hex)
+ switch res {
+ case .failure(let err):
+ self.error = err.description
+ case .success(let makesec):
+ let sec = makesec.secret
+ await do_login(sec, auth: self.auth)
+ }
+ }
+ }
+
+ func do_login(_ hexsec: String, auth: LNUrlAuth) async {
+ var url_str = auth.url.absoluteString
+
+ var dersig = Array<UInt8>.init(repeating: 0, count: 72)
+ var pk = Array<UInt8>.init(repeating: 0, count: 33)
+ var sig = secp256k1_ecdsa_signature()
+
+ guard let sec = hex_decode(hexsec) else {
+ self.error = "Could not hex decode secret key"
+ return
+ }
+
+ guard var msg = hex_decode(auth.k1) else {
+ self.error = "Could not decode k1 challenge string as hex: '\(auth.k1)'"
+ return
+ }
+
+
+ let opts = UInt32(SECP256K1_CONTEXT_SIGN)
+ guard let ctx = secp256k1_context_create(opts) else {
+ return
+ }
+
+ var pubkey = secp256k1_pubkey()
+
+ //let msg2 = sha256(msg)
+
+ guard secp256k1_ecdsa_sign(ctx, &sig, msg, sec, nil, nil) == 1 else {
+ self.error = "Failed to sign"
+ return
+ }
+
+ var siglen: Int = 72
+ guard secp256k1_ecdsa_signature_serialize_der(ctx, &dersig, &siglen, &sig) == 1 else {
+ self.error = "Failed to encode DER ecdsa signature"
+ return
+ }
+
+ dersig = Array(dersig[..<siglen])
+
+ defer { secp256k1_context_destroy(ctx) }
+
+ guard secp256k1_ec_pubkey_create(ctx, &pubkey, sec) == 1 else {
+ self.error = "Failed to get pubkey from keypair"
+ return
+ }
+
+ var pklen: Int = 33
+ guard secp256k1_ec_pubkey_serialize(ctx, &pk, &pklen, &pubkey, UInt32(SECP256K1_EC_COMPRESSED)) == 1 else {
+ self.error = "Failed to serialize pubkey"
+ return
+ }
+
+ let hex_key = hex_encode(pk)
+ let hex_sig = hex_encode(dersig)
+
+ url_str += "&sig=" + hex_sig + "&key=" + hex_key
+
+ guard let url = URL(string: url_str) else {
+ self.error = "Invalid url: \(url_str)"
+ return
+ }
+
+ // (data, resp)
+ guard let (data, resp) = try? await URLSession.shared.data(from: url) else {
+ self.error = "Login failed"
+ return
+ }
+
+ print("\(resp)")
+ print("\(data)")
+
+ dismiss()
+ }
+
+}
+
+func make_secret_hex(_ auth: LNUrlAuth) -> String? {
+ guard let host_data = auth.host.data(using: .utf8) else {
+ return nil
+ }
+ return hex_encode(Array(host_data))
+}
+
+struct AuthView_Previews: PreviewProvider {
+
+ static var previews: some View {
+ let auth = LNUrlAuth(k1: "k1", tag: "login", url: URL(string: "jb55.com")!, host: "jb55.com")
+ let lnlink = LNLink(token: "", host: "", node_id: "")
+ AuthView(auth: auth, lnlink: lnlink)
+ }
+}
+
+func sha256(_ data: [UInt8]) -> [UInt8] {
+ var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
+ var data = data
+ CC_SHA256(&data, CC_LONG(data.count), &hash)
+ return hash
+}
diff --git a/lightninglink/Views/ContentView.swift b/lightninglink/Views/ContentView.swift
@@ -38,6 +38,8 @@ public enum ActiveSheet: Identifiable {
switch self {
case .receive:
return "receive"
+ case .auth:
+ return "auth"
case .qr:
return "qrcode"
case .pay:
@@ -48,6 +50,7 @@ public enum ActiveSheet: Identifiable {
case qr
case receive
case pay(DecodeType)
+ case auth(LNUrlAuth)
}
struct Funds {
@@ -205,6 +208,9 @@ struct ContentView: View {
}
.sheet(item: $active_sheet) { sheet in
switch sheet {
+ case .auth(let auth):
+ AuthView(auth: auth, lnlink: lnlink)
+
case .qr:
CodeScannerView(codeTypes: SCAN_TYPES) { res in
switch res {
@@ -266,7 +272,11 @@ struct ContentView: View {
// TODO: report that this is an lnlink, not an invoice
case .lnurl(let lnurl):
let decode: DecodeType = .lnurl(lnurl)
- self.active_sheet = .pay(decode)
+ if let auth = is_lnurl_auth(lnurl) {
+ self.active_sheet = .auth(auth)
+ } else {
+ self.active_sheet = .pay(decode)
+ }
}
}
diff --git a/lightninglink/Views/PayView.swift b/lightninglink/Views/PayView.swift
@@ -77,6 +77,7 @@ public enum PayState {
case decoded(DecodeType)
case ready(Invoice)
case invoice_request(RequestInvoice)
+ case auth(LNUrlAuth)
}
let default_placeholder = "10,000"
@@ -253,10 +254,7 @@ struct PayView: View {
guard let invoice = self.invoice else {
return
}
- guard let amount_msat_str = invoice.amount_msat() else {
- return
- }
- guard let amount_msat = parse_msat(amount_msat_str) else {
+ guard let amount_msat = invoice.amount_msat() else {
return
}
@@ -468,12 +466,18 @@ struct PayView: View {
}
}
}
-
+
+ func handle_confirm_auth(ln mln: LNSocket?, auth: LNUrlAuth) {
+ }
+
func handle_confirm(ln mln: LNSocket?) {
// clear last error on confirm
self.error = nil
switch self.state {
+ case .auth(let auth):
+ return handle_confirm_auth(ln: mln, auth: auth)
+
case .invoice_request(let reqinv):
switch reqinv {
case .offer(let offer):
@@ -507,6 +511,8 @@ struct PayView: View {
func handle_state_change() {
switch self.state {
+ case .auth:
+ break
case .ready:
break
case .invoice_request:
@@ -539,7 +545,11 @@ struct PayView: View {
switch_state(.invoice_request(.lnurl(lnurlp)))
}
-
+
+ func handle_lnurl_auth(ln: LNSocket, auth: LNUrlAuth) {
+ switch_state(.auth(auth))
+ }
+
func handle_decode(_ oldln: LNSocket?, decode: DecodeType) {
let ln = oldln ?? LNSocket()
if oldln == nil {
@@ -577,7 +587,7 @@ struct PayView: View {
} else if decoded.type == "bolt11 invoice" || decoded.type == "bolt12 invoice" {
var amount: InvoiceAmount = .any
if decoded.amount_msat != nil {
- guard let amt = parse_msat(decoded.amount_msat!) else {
+ guard let amt = decoded.amount_msat else {
self.error = "invalid msat amount: \(decoded.amount_msat!)"
return
}
@@ -723,6 +733,7 @@ func is_ready(_ state: PayState) -> ReadyInvoice? {
return .direct(invoice)
case .invoice_request(let invreq):
return .requested(invreq)
+ case .auth: fallthrough
case .initial: fallthrough
case .decoding: fallthrough
case .decoded:
@@ -774,7 +785,7 @@ struct PayView_Previews: PreviewProvider {
func handle_bolt12_offer(ln: LNSocket, decoded: InvoiceDecode, inv: String) -> Either<String, PayState> {
if decoded.amount_msat != nil {
- guard let min_amt = parse_msat(decoded.amount_msat!) else {
+ guard let min_amt = decoded.amount_msat else {
return .left("Error parsing amount_msat: '\(decoded.amount_msat!)'")
}
let offer = Offer(offer: inv, amount: .min(min_amt), decoded: decoded)
@@ -789,6 +800,7 @@ func handle_bolt12_offer(ln: LNSocket, decoded: InvoiceDecode, inv: String) -> E
func should_show_confirm(_ state: PayState) -> Bool {
switch state {
case .ready: fallthrough
+ case .auth: fallthrough
case .invoice_request:
return true
diff --git a/lightninglink/Views/ReceiveView.swift b/lightninglink/Views/ReceiveView.swift
@@ -110,7 +110,9 @@ struct ReceiveView: View {
}
Spacer()
-
+
+
+
if !self.making && self.qr_data == nil {
Button("Receive") {
self.making = true