mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
* feat: do not require unlock password to recover encrypted scb * fix: use seed as key to encrypt static channel backup * chore: move encrypted channel backup derivation to alby oauth service * chore: add extra test * chore: reduce duplicated code * fix: change key derivation paths * chore: simplify backup page copy for connected alby accounts * chore: improve copy * fix: initialize keys before starting ln backend * chore: improve test assertions
374 lines
10 KiB
Go
374 lines
10 KiB
Go
package nip47
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/getAlby/hub/constants"
|
|
"github.com/getAlby/hub/db"
|
|
"github.com/getAlby/hub/nip47/models"
|
|
"github.com/getAlby/hub/nip47/permissions"
|
|
"github.com/getAlby/hub/tests"
|
|
"github.com/nbd-wtf/go-nostr"
|
|
"github.com/nbd-wtf/go-nostr/nip04"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TODO: test HandleEvent
|
|
// TODO: test a request cannot be processed twice
|
|
// TODO: test if an app doesn't exist it returns the right error code
|
|
|
|
func TestCreateResponse(t *testing.T) {
|
|
defer tests.RemoveTestService()
|
|
svc, err := tests.CreateTestService()
|
|
require.NoError(t, err)
|
|
|
|
reqPrivateKey := nostr.GeneratePrivateKey()
|
|
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
reqEvent := &nostr.Event{
|
|
Kind: models.REQUEST_KIND,
|
|
PubKey: reqPubkey,
|
|
Content: "1",
|
|
}
|
|
|
|
reqEvent.ID = "12345"
|
|
|
|
ss, err := nip04.ComputeSharedSecret(reqPubkey, svc.Keys.GetNostrSecretKey())
|
|
assert.NoError(t, err)
|
|
|
|
type dummyResponse struct {
|
|
Foo int
|
|
}
|
|
|
|
nip47Response := &models.Response{
|
|
ResultType: "dummy_method",
|
|
Result: dummyResponse{
|
|
Foo: 1000,
|
|
},
|
|
}
|
|
|
|
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
|
|
|
|
res, err := nip47svc.CreateResponse(reqEvent, nip47Response, nostr.Tags{}, ss)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, reqPubkey, res.Tags.GetFirst([]string{"p"}).Value())
|
|
assert.Equal(t, reqEvent.ID, res.Tags.GetFirst([]string{"e"}).Value())
|
|
assert.Equal(t, svc.Keys.GetNostrPublicKey(), res.PubKey)
|
|
|
|
decrypted, err := nip04.Decrypt(res.Content, ss)
|
|
assert.NoError(t, err)
|
|
unmarshalledResponse := models.Response{
|
|
Result: &dummyResponse{},
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, nip47Response.Error)
|
|
assert.Equal(t, nip47Response.ResultType, unmarshalledResponse.ResultType)
|
|
assert.Equal(t, nip47Response.Result, *unmarshalledResponse.Result.(*dummyResponse))
|
|
}
|
|
|
|
func TestHandleResponse_WithPermission(t *testing.T) {
|
|
defer tests.RemoveTestService()
|
|
svc, err := tests.CreateTestService()
|
|
require.NoError(t, err)
|
|
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
|
|
|
|
reqPrivateKey := nostr.GeneratePrivateKey()
|
|
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
appPermission := &db.AppPermission{
|
|
AppId: app.ID,
|
|
App: *app,
|
|
Scope: constants.GET_BALANCE_SCOPE,
|
|
}
|
|
err = svc.DB.Create(appPermission).Error
|
|
assert.NoError(t, err)
|
|
|
|
content := map[string]interface{}{
|
|
"method": models.GET_INFO_METHOD,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(content)
|
|
assert.NoError(t, err)
|
|
|
|
msg, err := nip04.Encrypt(string(payloadBytes), ss)
|
|
assert.NoError(t, err)
|
|
|
|
reqEvent := &nostr.Event{
|
|
Kind: models.REQUEST_KIND,
|
|
PubKey: reqPubkey,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{},
|
|
Content: msg,
|
|
}
|
|
err = reqEvent.Sign(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
relay := tests.NewMockRelay()
|
|
|
|
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
|
|
|
|
assert.NotNil(t, relay.PublishedEvent)
|
|
assert.NotEmpty(t, relay.PublishedEvent.Content)
|
|
|
|
ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
|
|
assert.NoError(t, err)
|
|
|
|
type getInfoResult struct {
|
|
Methods []string `json:"methods"`
|
|
}
|
|
|
|
type getInfoResponseWrapper struct {
|
|
models.Response
|
|
Result getInfoResult `json:"result"`
|
|
}
|
|
|
|
unmarshalledResponse := getInfoResponseWrapper{}
|
|
|
|
err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, unmarshalledResponse.Error)
|
|
assert.Equal(t, models.GET_INFO_METHOD, unmarshalledResponse.ResultType)
|
|
expectedMethods := slices.Concat([]string{constants.GET_BALANCE_SCOPE}, permissions.GetAlwaysGrantedMethods())
|
|
assert.Equal(t, expectedMethods, unmarshalledResponse.Result.Methods)
|
|
}
|
|
|
|
func TestHandleResponse_DuplicateRequest(t *testing.T) {
|
|
defer tests.RemoveTestService()
|
|
svc, err := tests.CreateTestService()
|
|
require.NoError(t, err)
|
|
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
|
|
|
|
reqPrivateKey := nostr.GeneratePrivateKey()
|
|
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
appPermission := &db.AppPermission{
|
|
AppId: app.ID,
|
|
App: *app,
|
|
Scope: constants.GET_BALANCE_SCOPE,
|
|
}
|
|
err = svc.DB.Create(appPermission).Error
|
|
assert.NoError(t, err)
|
|
|
|
content := map[string]interface{}{
|
|
"method": models.GET_INFO_METHOD,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(content)
|
|
assert.NoError(t, err)
|
|
|
|
msg, err := nip04.Encrypt(string(payloadBytes), ss)
|
|
assert.NoError(t, err)
|
|
|
|
reqEvent := &nostr.Event{
|
|
Kind: models.REQUEST_KIND,
|
|
PubKey: reqPubkey,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{},
|
|
Content: msg,
|
|
}
|
|
err = reqEvent.Sign(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
relay := tests.NewMockRelay()
|
|
|
|
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
|
|
|
|
assert.NotNil(t, relay.PublishedEvent)
|
|
assert.NotEmpty(t, relay.PublishedEvent.Content)
|
|
|
|
relay.PublishedEvent = nil
|
|
|
|
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
|
|
|
|
// second time it should not publish
|
|
assert.Nil(t, relay.PublishedEvent)
|
|
}
|
|
|
|
func TestHandleResponse_NoPermission(t *testing.T) {
|
|
defer tests.RemoveTestService()
|
|
svc, err := tests.CreateTestService()
|
|
require.NoError(t, err)
|
|
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
|
|
|
|
reqPrivateKey := nostr.GeneratePrivateKey()
|
|
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
_, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
content := map[string]interface{}{
|
|
"method": models.GET_BALANCE_METHOD,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(content)
|
|
assert.NoError(t, err)
|
|
|
|
msg, err := nip04.Encrypt(string(payloadBytes), ss)
|
|
assert.NoError(t, err)
|
|
|
|
reqEvent := &nostr.Event{
|
|
Kind: models.REQUEST_KIND,
|
|
PubKey: reqPubkey,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{},
|
|
Content: msg,
|
|
}
|
|
err = reqEvent.Sign(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
relay := tests.NewMockRelay()
|
|
|
|
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
|
|
|
|
assert.NotNil(t, relay.PublishedEvent)
|
|
assert.NotEmpty(t, relay.PublishedEvent.Content)
|
|
|
|
ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
|
|
assert.NoError(t, err)
|
|
|
|
unmarshalledResponse := models.Response{}
|
|
|
|
err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, unmarshalledResponse.Result)
|
|
assert.Equal(t, models.GET_BALANCE_METHOD, unmarshalledResponse.ResultType)
|
|
assert.Equal(t, "RESTRICTED", unmarshalledResponse.Error.Code)
|
|
assert.Equal(t, "This app does not have the get_balance scope", unmarshalledResponse.Error.Message)
|
|
}
|
|
|
|
func TestHandleResponse_NoApp(t *testing.T) {
|
|
defer tests.RemoveTestService()
|
|
svc, err := tests.CreateTestService()
|
|
require.NoError(t, err)
|
|
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
|
|
|
|
reqPrivateKey := nostr.GeneratePrivateKey()
|
|
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
// delete the app
|
|
err = svc.DB.Delete(app).Error
|
|
assert.NoError(t, err)
|
|
|
|
content := map[string]interface{}{
|
|
"method": models.GET_BALANCE_METHOD,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(content)
|
|
assert.NoError(t, err)
|
|
|
|
msg, err := nip04.Encrypt(string(payloadBytes), ss)
|
|
assert.NoError(t, err)
|
|
|
|
reqEvent := &nostr.Event{
|
|
Kind: models.REQUEST_KIND,
|
|
PubKey: reqPubkey,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{},
|
|
Content: msg,
|
|
}
|
|
err = reqEvent.Sign(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
relay := tests.NewMockRelay()
|
|
|
|
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
|
|
|
|
assert.NotNil(t, relay.PublishedEvent)
|
|
assert.NotEmpty(t, relay.PublishedEvent.Content)
|
|
|
|
ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
|
|
assert.NoError(t, err)
|
|
|
|
unmarshalledResponse := models.Response{}
|
|
|
|
err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, unmarshalledResponse.Result)
|
|
assert.Equal(t, "", unmarshalledResponse.ResultType)
|
|
assert.Equal(t, "UNAUTHORIZED", unmarshalledResponse.Error.Code)
|
|
assert.Equal(t, "The public key does not have a wallet connected.", unmarshalledResponse.Error.Message)
|
|
}
|
|
|
|
func TestHandleResponse_IncorrectPubkey(t *testing.T) {
|
|
defer tests.RemoveTestService()
|
|
svc, err := tests.CreateTestService()
|
|
require.NoError(t, err)
|
|
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
|
|
|
|
reqPrivateKey := nostr.GeneratePrivateKey()
|
|
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
reqPrivateKey2 := nostr.GeneratePrivateKey()
|
|
|
|
app, _, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
|
|
assert.NoError(t, err)
|
|
|
|
appPermission := &db.AppPermission{
|
|
AppId: app.ID,
|
|
App: *app,
|
|
Scope: constants.GET_BALANCE_SCOPE,
|
|
}
|
|
err = svc.DB.Create(appPermission).Error
|
|
assert.NoError(t, err)
|
|
|
|
_, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey2)
|
|
assert.NoError(t, err)
|
|
|
|
content := map[string]interface{}{
|
|
"method": models.GET_BALANCE_METHOD,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(content)
|
|
assert.NoError(t, err)
|
|
|
|
msg, err := nip04.Encrypt(string(payloadBytes), ss)
|
|
assert.NoError(t, err)
|
|
|
|
reqEvent := &nostr.Event{
|
|
Kind: models.REQUEST_KIND,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{},
|
|
Content: msg,
|
|
}
|
|
err = reqEvent.Sign(reqPrivateKey2)
|
|
assert.NoError(t, err)
|
|
|
|
// set a different pubkey (this will not pass validation)
|
|
reqEvent.PubKey = reqPubkey
|
|
|
|
relay := tests.NewMockRelay()
|
|
|
|
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
|
|
|
|
assert.Nil(t, relay.PublishedEvent)
|
|
}
|