adding lnurl auth (#294)

* adding lnurl auth poc

* add response and translation

* refactor

* simpler

* using http bot client with proxy transport

* functions

* translations

* clean

* comment

* revert

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
gohumble 2022-01-22 23:25:21 +01:00 committed by GitHub
parent 9522418272
commit 96292604c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 259 additions and 19 deletions

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.15
require (
github.com/BurntSushi/toml v0.3.1
github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 // indirect
github.com/eko/gocache v1.2.0
github.com/fiatjaf/go-lnurl v1.8.4
github.com/fiatjaf/ln-decodepay v1.1.0

2
go.sum
View file

@ -83,6 +83,7 @@ github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJ
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
@ -259,6 +260,7 @@ github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQ
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=

View file

@ -1,8 +1,13 @@
package lnbits
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/imroc/req"
tb "gopkg.in/lightningtipbot/telebot.v2"
)
@ -15,18 +20,18 @@ type Client struct {
}
type User struct {
ID string `json:"id"`
Name string `json:"name" gorm:"primaryKey"`
Initialized bool `json:"initialized"`
Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"`
Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"`
StateKey UserStateKey `json:"stateKey"`
StateData string `json:"stateData"`
CreatedAt time.Time `json:"created"`
UpdatedAt time.Time `json:"updated"`
AnonID string `json:"anon_id"`
ID string `json:"id"`
Name string `json:"name" gorm:"primaryKey"`
Initialized bool `json:"initialized"`
Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"`
Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"`
StateKey UserStateKey `json:"stateKey"`
StateData string `json:"stateData"`
CreatedAt time.Time `json:"created"`
UpdatedAt time.Time `json:"updated"`
AnonID string `json:"anon_id"`
AnonIDSha256 string `json:"anon_id_sha256"`
Banned bool `json:"banned"`
Banned bool `json:"banned"`
}
const (
@ -103,3 +108,31 @@ type BitInvoice struct {
PaymentHash string `json:"payment_hash"`
PaymentRequest string `json:"payment_request"`
}
// from fiatjaf/lnurl-go
func (u User) LinkingKey(domain string) (*btcec.PrivateKey, *btcec.PublicKey) {
seedhash := sha256.Sum256([]byte(
fmt.Sprintf("lnurlkeyseed:%s:%s",
domain, u.ID)))
return btcec.PrivKeyFromBytes(btcec.S256(), seedhash[:])
}
func (u User) SignKeyAuth(domain string, k1hex string) (key string, sig string, err error) {
// lnurl-auth: create a key based on the user id and sign with it
sk, pk := u.LinkingKey(domain)
k1, err := hex.DecodeString(k1hex)
if err != nil {
return "", "", fmt.Errorf("invalid k1 hex '%s': %w", k1hex, err)
}
signature, err := sk.Sign(k1)
if err != nil {
return "", "", fmt.Errorf("error signing k1: %w", err)
}
sig = hex.EncodeToString(signature.Serialize())
key = hex.EncodeToString(pk.SerializeCompressed())
return key, sig, nil
}

View file

@ -651,6 +651,36 @@ func (bot TipBot) getHandler() []Handler {
},
},
},
{
Endpoints: []interface{}{&btnAuth},
Handler: bot.confirmLnurlAuthHandler,
Interceptor: &Interceptor{
Type: CallbackInterceptor,
Before: []intercept.Func{
bot.localizerInterceptor,
bot.requireUserInterceptor,
bot.lockInterceptor,
},
OnDefer: []intercept.Func{
bot.unlockInterceptor,
},
},
},
{
Endpoints: []interface{}{&btnCancelAuth},
Handler: bot.cancelLnurlAuthHandler,
Interceptor: &Interceptor{
Type: CallbackInterceptor,
Before: []intercept.Func{
bot.localizerInterceptor,
bot.requireUserInterceptor,
bot.lockInterceptor,
},
OnDefer: []intercept.Func{
bot.unlockInterceptor,
},
},
},
{
Endpoints: []interface{}{&shopNewShopButton},
Handler: bot.shopNewShopHandler,

View file

@ -0,0 +1,151 @@
package telegram
import (
"context"
"encoding/json"
"fmt"
"net/url"
"github.com/LightningTipBot/LightningTipBot/internal/errors"
"github.com/LightningTipBot/LightningTipBot/internal/i18n"
"github.com/LightningTipBot/LightningTipBot/internal/runtime"
"github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex"
"github.com/LightningTipBot/LightningTipBot/internal/storage"
"github.com/imroc/req"
"github.com/LightningTipBot/LightningTipBot/internal/lnbits"
lnurl "github.com/fiatjaf/go-lnurl"
log "github.com/sirupsen/logrus"
tb "gopkg.in/lightningtipbot/telebot.v2"
)
var (
authConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true}
btnCancelAuth = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_login")
btnAuth = paymentConfirmationMenu.Data("✅ Login", "confirm_login")
)
type LnurlAuthState struct {
*storage.Base
From *lnbits.User `json:"from"`
LNURLAuthParams lnurl.LNURLAuthParams `json:"LNURLAuthParams"`
Comment string `json:"comment"`
LanguageCode string `json:"languagecode"`
Message *tb.Message `json:"message"`
}
// lnurlPayHandler1 is invoked when the first lnurl response was a lnurlpay response
// at this point, the user hans't necessarily entered an amount yet
func (bot *TipBot) lnurlAuthHandler(ctx context.Context, m *tb.Message, authParams LnurlAuthState) (context.Context, error) {
user := LoadUser(ctx)
if user.Wallet == nil {
return ctx, errors.Create(errors.UserNoWalletError)
}
// object that holds all information about the send payment
id := fmt.Sprintf("lnurlauth-%d-%s", m.Sender.ID, RandStringRunes(5))
lnurlAuthState := &LnurlAuthState{
Base: storage.New(storage.ID(id)),
From: user,
LNURLAuthParams: authParams.LNURLAuthParams,
LanguageCode: ctx.Value("publicLanguageCode").(string),
}
// // // create inline buttons
btnAuth = paymentConfirmationMenu.Data(Translate(ctx, "loginButtonMessage"), "confirm_login", id)
btnCancelAuth = paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_login", id)
paymentConfirmationMenu.Inline(
paymentConfirmationMenu.Row(
btnAuth,
btnCancelAuth),
)
lnurlAuthState.Message = bot.trySendMessageEditable(m.Chat,
fmt.Sprintf(Translate(ctx, "confirmLnurlAuthMessager"),
lnurlAuthState.LNURLAuthParams.CallbackURL.Host,
),
paymentConfirmationMenu,
)
// save to bunt
runtime.IgnoreError(lnurlAuthState.Set(lnurlAuthState, bot.Bunt))
return ctx, nil
}
func (bot *TipBot) confirmLnurlAuthHandler(ctx context.Context, c *tb.Callback) (context.Context, error) {
tx := &LnurlAuthState{Base: storage.New(storage.ID(c.Data))}
mutex.LockWithContext(ctx, tx.ID)
defer mutex.UnlockWithContext(ctx, tx.ID)
sn, err := tx.Get(tx, bot.Bunt)
// immediatelly set intransaction to block duplicate calls
if err != nil {
log.Errorf("[confirmPayHandler] %s", err.Error())
return ctx, err
}
lnurlAuthState := sn.(*LnurlAuthState)
if !lnurlAuthState.Active {
return ctx, fmt.Errorf("LnurlAuthData not active.")
}
user := LoadUser(ctx)
if user.Wallet == nil {
return ctx, errors.Create(errors.UserNoWalletError)
}
// statusMsg := bot.trySendMessageEditable(c.Sender,
// Translate(ctx, "lnurlResolvingUrlMessage"),
// )
bot.editSingleButton(ctx, c.Message, lnurlAuthState.Message.Text, Translate(ctx, "lnurlResolvingUrlMessage"))
// from fiatjaf/go-lnurl
p := lnurlAuthState.LNURLAuthParams
key, sig, err := user.SignKeyAuth(p.Host, p.K1)
if err != nil {
return ctx, err
}
var sentsigres lnurl.LNURLResponse
client, err := bot.GetHttpClient()
if err != nil {
return ctx, err
}
r := req.New()
r.SetClient(client)
res, err := r.Get(p.CallbackURL.String(), url.Values{"sig": {sig}, "key": {key}})
if err != nil {
return ctx, err
}
err = json.Unmarshal(res.Bytes(), &sentsigres)
if err != nil {
return ctx, err
}
if sentsigres.Status == "ERROR" {
bot.tryEditMessage(c.Message, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), sentsigres.Reason))
return ctx, err
}
bot.editSingleButton(ctx, c.Message, lnurlAuthState.Message.Text, Translate(ctx, "lnurlSuccessfulLogin"))
return ctx, lnurlAuthState.Inactivate(lnurlAuthState, bot.Bunt)
}
// cancelPaymentHandler invoked when user clicked cancel on payment confirmation
func (bot *TipBot) cancelLnurlAuthHandler(ctx context.Context, c *tb.Callback) (context.Context, error) {
tx := &LnurlAuthState{Base: storage.New(storage.ID(c.Data))}
mutex.LockWithContext(ctx, tx.ID)
defer mutex.UnlockWithContext(ctx, tx.ID)
sn, err := tx.Get(tx, bot.Bunt)
// immediatelly set intransaction to block duplicate calls
if err != nil {
log.Errorf("[confirmPayHandler] %s", err.Error())
return ctx, err
}
lnurlAuthState := sn.(*LnurlAuthState)
// onnly the correct user can press
if lnurlAuthState.From.Telegram.ID != c.Sender.ID {
return ctx, errors.Create(errors.UnknownError)
}
// delete and send instead of edit for the keyboard to pop up after sending
bot.tryEditMessage(c.Message, i18n.Translate(lnurlAuthState.LanguageCode, "loginCancelledMessage"), &tb.ReplyMarkup{})
// bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{})
return ctx, lnurlAuthState.Inactivate(lnurlAuthState, bot.Bunt)
}

View file

@ -43,6 +43,7 @@ type LnurlWithdrawState struct {
Message string `json:"message"`
}
// editSingleButton edits a message to display a single button (for something like a progress indicator)
func (bot *TipBot) editSingleButton(ctx context.Context, m *tb.Message, message string, button string) {
bot.tryEditMessage(
m,
@ -169,10 +170,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa
lnurlWithdrawState.Message = confirmText
// create inline buttons
withdrawButton := paymentConfirmationMenu.Data(Translate(ctx, "withdrawButtonMessage"), "confirm_withdraw")
btnCancelWithdraw := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_withdraw")
withdrawButton.Data = lnurlWithdrawState.ID
btnCancelWithdraw.Data = lnurlWithdrawState.ID
withdrawButton := paymentConfirmationMenu.Data(Translate(ctx, "withdrawButtonMessage"), "confirm_withdraw", lnurlWithdrawState.ID)
btnCancelWithdraw := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_withdraw", lnurlWithdrawState.ID)
withdrawConfirmationMenu.Inline(
withdrawConfirmationMenu.Row(

View file

@ -74,6 +74,8 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con
// get rid of the URI prefix
lnurlSplit = strings.TrimPrefix(lnurlSplit, "lightning:")
log.Debugf("[lnurlHandler] lnurlSplit: %s", lnurlSplit)
// HandleLNURL by fiatjaf/go-lnurl
_, params, err := bot.HandleLNURL(lnurlSplit)
if err != nil {
@ -83,6 +85,12 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con
return ctx, err
}
switch params.(type) {
case lnurl.LNURLAuthParams:
authParams := &LnurlAuthState{LNURLAuthParams: params.(lnurl.LNURLAuthParams)}
log.Infof("[LNURL-auth] %s", authParams.LNURLAuthParams.Callback)
bot.tryDeleteMessage(statusMsg)
return bot.lnurlAuthHandler(ctx, m, *authParams)
case lnurl.LNURLPayParams:
payParams := &LnurlPayState{LNURLPayParams: params.(lnurl.LNURLPayParams)}
log.Infof("[LNURL-p] %s", payParams.LNURLPayParams.Callback)

View file

@ -121,10 +121,8 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte
id := fmt.Sprintf("pay-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5))
// // // create inline buttons
payButton := paymentConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "confirm_pay")
cancelButton := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_pay")
payButton.Data = id
cancelButton.Data = id
payButton := paymentConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "confirm_pay", id)
cancelButton := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_pay", id)
paymentConfirmationMenu.Inline(
paymentConfirmationMenu.Row(

View file

@ -237,6 +237,13 @@ lnurlHelpText = """📖 Ups, das hat nicht geklappt. %s
*Befehl:* `/lnurl [betrag] <lnurl>`
*Beispiel:* `/lnurl LNURL1DP68GUR...`"""
# LNURL LOGIN
confirmLnurlAuthMessager = """Möchtest du dich bei %s einloggen?"""
lnurlSuccessfulLogin = """✅ Login erfolgreich."""
loginButtonMessage = """✅ Login"""
loginCancelledMessage = """🚫 Login abgebrochen."""
# LNURL WITHDRAW
confirmLnurlWithdrawMessage = """Möchtest du diese Abhebung tätigen?\n\n💸 Betrag: %d sat"""

View file

@ -241,6 +241,13 @@ lnurlHelpText = """📖 Oops, that didn't work. %s
*Usage:* `/lnurl [amount] <lnurl>`
*Example:* `/lnurl LNURL1DP68GUR...`"""
# LNURL LOGIN
confirmLnurlAuthMessager = """Do you want to login to %s?"""
lnurlSuccessfulLogin = """✅ Login successful."""
loginButtonMessage = """✅ Login"""
loginCancelledMessage = """🚫 Login cancelled."""
# LNURL WITHDRAW
confirmLnurlWithdrawMessage = """Do you want to make this withdrawal?\n\n💸 Amount: %d sat"""

View file

@ -237,6 +237,10 @@ lnurlHelpText = """📖 Oops, eso no funcionó. %s
*Uso:* `/lnurl [monto] <lnurl>`
*Ejemplo:* `/lnurl LNURL1DP68GUR...`"""
# LNURL LOGIN
lnurlSuccessfulLogin = """✅ Inicio de sesión exitoso."""
# LNURL WITHDRAW
confirmLnurlWithdrawMessage = """¿Desea realizar este retiro?\n\n💸 Monto: %d sat"""