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 encryption/decryption functions #228

Merged
merged 19 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ node_modules
nostr-wallet-connect
nwc.db
.breez
.data

frontend/dist
frontend/node_modules
Expand Down
83 changes: 83 additions & 0 deletions aesgcm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"strings"

"golang.org/x/crypto/scrypt"
)

func DeriveKey(password string, salt []byte) ([]byte, []byte, error) {
if salt == nil {
salt = make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
return nil, nil, err
}
}

key, err := scrypt.Key([]byte(password), salt, 131072, 8, 1, 32)
if err != nil {
return nil, nil, err
}

return key, salt, nil
}

func AesGcmEncrypt(plaintext string, password string) (string, error) {
secretKey, salt, err := DeriveKey(password, nil)
if err != nil {
return "", err
}
plaintextBytes := []byte(plaintext)

aes, err := aes.NewCipher([]byte(secretKey))
if err != nil {
return "", err
}

aesgcm, err := cipher.NewGCM(aes)
if err != nil {
return "", err
}

nonce := make([]byte, aesgcm.NonceSize())
_, err = rand.Read(nonce)
if err != nil {
return "", err
}

ciphertext := aesgcm.Seal(nil, nonce, plaintextBytes, nil)

return hex.EncodeToString(salt) + "-" + hex.EncodeToString(nonce) + "-" + hex.EncodeToString(ciphertext), nil
}

func AesGcmDecrypt(ciphertext string, password string) (string, error) {
arr := strings.Split(ciphertext, "-")
rolznz marked this conversation as resolved.
Show resolved Hide resolved
salt, _ := hex.DecodeString(arr[0])
nonce, _ := hex.DecodeString(arr[1])
data, _ := hex.DecodeString(arr[2])

secretKey, salt, err := DeriveKey(password, salt)
if err != nil {
return "", err
}
aes, err := aes.NewCipher([]byte(secretKey))
if err != nil {
return "", err
}

aesgcm, err := cipher.NewGCM(aes)
if err != nil {
return "", err
}

plaintext, err := aesgcm.Open(nil, nonce, data, nil)
if err != nil {
return "", err
}

return string(plaintext), nil
}
53 changes: 19 additions & 34 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"time"

"github.com/getAlby/nostr-wallet-connect/models/api"
"github.com/getAlby/nostr-wallet-connect/models/db"
"github.com/nbd-wtf/go-nostr"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -90,10 +89,7 @@ func (svc *Service) CreateApp(createAppRequest *api.CreateAppRequest) (*api.Crea
return nil, err
}

publicRelayUrl := svc.cfg.PublicRelay
if publicRelayUrl == "" {
publicRelayUrl = svc.cfg.Relay
}
relayUrl, _ := svc.cfg.Get("Relay", "")

responseBody := &api.CreateAppResponse{}
responseBody.Name = name
Expand All @@ -104,8 +100,8 @@ func (svc *Service) CreateApp(createAppRequest *api.CreateAppRequest) (*api.Crea
returnToUrl, err := url.Parse(createAppRequest.ReturnTo)
if err == nil {
query := returnToUrl.Query()
query.Add("relay", publicRelayUrl)
query.Add("pubkey", svc.cfg.IdentityPubkey)
query.Add("relay", relayUrl)
query.Add("pubkey", svc.cfg.NostrPublicKey)
// if user.LightningAddress != "" {
// query.Add("lud16", user.LightningAddress)
// }
Expand All @@ -118,7 +114,7 @@ func (svc *Service) CreateApp(createAppRequest *api.CreateAppRequest) (*api.Crea
// if user.LightningAddress != "" {
// lud16 = fmt.Sprintf("&lud16=%s", user.LightningAddress)
// }
responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", svc.cfg.IdentityPubkey, publicRelayUrl, pairingSecretKey, lud16)
responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", svc.cfg.NostrPublicKey, relayUrl, pairingSecretKey, lud16)
return responseBody, nil
}

Expand Down Expand Up @@ -207,50 +203,39 @@ func (svc *Service) ListApps() ([]api.App, error) {

func (svc *Service) GetInfo() *api.InfoResponse {
info := api.InfoResponse{}
info.BackendType = svc.cfg.LNBackendType
info.SetupCompleted = svc.lnClient != nil
backend, _ := svc.cfg.Get("LNBackendType", "")
info.SetupCompleted = backend != ""
info.Running = svc.lnClient != nil
return &info
}

func (svc *Service) Setup(setupRequest *api.SetupRequest) error {

dbConfig := db.Config{}
err := svc.db.First(&dbConfig).Error

if err != nil {
svc.Logger.Errorf("Failed to get db config: %v", err)
return err
}
func (svc *Service) Start(startRequest *api.StartRequest) error {
return svc.StartApp(startRequest.UnlockPassword)
}

func (svc *Service) Setup(setupRequest *api.SetupRequest) error {
// only update non-empty values
if setupRequest.LNBackendType != "" {
dbConfig.LNBackendType = setupRequest.LNBackendType
svc.cfg.SetUpdate("LNBackendType", setupRequest.LNBackendType, "")
}
if setupRequest.BreezAPIKey != "" {
dbConfig.BreezAPIKey = setupRequest.BreezAPIKey
svc.cfg.SetUpdate("BreezAPIKey", setupRequest.BreezAPIKey, setupRequest.UnlockPassword)
}
if setupRequest.BreezMnemonic != "" {
dbConfig.BreezMnemonic = setupRequest.BreezMnemonic
svc.cfg.SetUpdate("BreezMnemonic", setupRequest.BreezMnemonic, setupRequest.UnlockPassword)
}
if setupRequest.GreenlightInviteCode != "" {
dbConfig.GreenlightInviteCode = setupRequest.GreenlightInviteCode
svc.cfg.SetUpdate("GreenlightInviteCode", setupRequest.GreenlightInviteCode, setupRequest.UnlockPassword)
}
if setupRequest.LNDAddress != "" {
dbConfig.LNDAddress = setupRequest.LNDAddress
svc.cfg.SetUpdate("LNDAddress", setupRequest.LNDAddress, setupRequest.UnlockPassword)
}
if setupRequest.LNDCertHex != "" {
dbConfig.LNDCertHex = setupRequest.LNDCertHex
svc.cfg.SetUpdate("LNDCertHex", setupRequest.LNDCertHex, setupRequest.UnlockPassword)
}
if setupRequest.LNDMacaroonHex != "" {
dbConfig.LNDMacaroonHex = setupRequest.LNDMacaroonHex
}

err = svc.db.Save(&dbConfig).Error

if err != nil {
svc.Logger.Errorf("Failed to update config: %v", err)
return err
svc.cfg.SetUpdate("LNDMacaroonHex", setupRequest.LNDMacaroonHex, setupRequest.UnlockPassword)
}

return svc.launchLNBackend()
return nil
}
142 changes: 122 additions & 20 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,133 @@
package main

import "github.com/getAlby/nostr-wallet-connect/models/db"
import (
"crypto/rand"
"encoding/hex"
"os"
"time"

"gorm.io/gorm"
"gorm.io/gorm/clause"
)

const (
LNDBackendType = "LND"
BreezBackendType = "BREEZ"
CookieName = "alby_nwc_session"
)

type AppConfig struct {
Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"`
LNBackendType string `envconfig:"LN_BACKEND_TYPE"`
LNDCertFile string `envconfig:"LND_CERT_FILE"`
LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"`
Workdir string `envconfig:"WORK_DIR" default:".data"`
Port string `envconfig:"PORT" default:"8080"`
DatabaseUri string `envconfig:"DATABASE_URI" default:"nostr-wallet-connect.db"`
CookieSecret string `envconfig:"COOKIE_SECRET"`
}

type UserConfig struct {
ID uint `gorm:"primaryKey"`
Key string `gorm:"unique;uniqueIndex;not null"`
Value string
Encrypted bool
CreatedAt time.Time
UpdatedAt time.Time
}

type Config struct {
// These config can be set either by .env or the database config table.
// database config always takes preference.
db.Config
CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
ClientPubkey string `envconfig:"CLIENT_NOSTR_PUBKEY"`
Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"`
PublicRelay string `envconfig:"PUBLIC_RELAY"`
LNDCertFile string `envconfig:"LND_CERT_FILE"`
LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"`
BreezWorkdir string `envconfig:"BREEZ_WORK_DIR" default:".breez"`
BasicAuthUser string `envconfig:"BASIC_AUTH_USER"`
BasicAuthPassword string `envconfig:"BASIC_AUTH_PASSWORD"`
Port string `envconfig:"PORT" default:"8080"`
DatabaseUri string `envconfig:"DATABASE_URI" default:"nostr-wallet-connect.db"`
DatabaseMaxConns int `envconfig:"DATABASE_MAX_CONNS" default:"10"`
DatabaseMaxIdleConns int `envconfig:"DATABASE_MAX_IDLE_CONNS" default:"5"`
DatabaseConnMaxLifetime int `envconfig:"DATABASE_CONN_MAX_LIFETIME" default:"1800"` // 30 minutes
IdentityPubkey string
Env *AppConfig
CookieSecret string
NostrSecretKey string
NostrPublicKey string
db *gorm.DB
}

func (cfg *Config) Init(db *gorm.DB, env *AppConfig) {
cfg.db = db
cfg.Env = env

if cfg.Env.Relay != "" {
cfg.SetUpdate("Relay", cfg.Env.Relay, "")
rolznz marked this conversation as resolved.
Show resolved Hide resolved
}
if cfg.Env.LNBackendType != "" {
cfg.SetUpdate("LNBackendType", cfg.Env.LNBackendType, "")
}
if cfg.Env.LNDCertFile != "" {
certBytes, err := os.ReadFile(cfg.Env.LNDCertFile)
if err != nil {
certHex := hex.EncodeToString(certBytes)
cfg.SetUpdate("LNDCertHex", certHex, "")
}
}
if cfg.Env.LNDMacaroonFile != "" {
macBytes, err := os.ReadFile(cfg.Env.LNDMacaroonFile)
if err != nil {
macHex := hex.EncodeToString(macBytes)
cfg.SetUpdate("LNDMacaroonHex", macHex, "")
}
}
// set the cookie secret to the one from the env
// if no cookie secret is configured we create a random one and store it in the DB
cfg.CookieSecret = cfg.Env.CookieSecret
rolznz marked this conversation as resolved.
Show resolved Hide resolved
if cfg.CookieSecret == "" {
hex, err := randomHex(20)
if err == nil {
cfg.SetIgnore("CookieSecret", hex, "")
}
cfg.CookieSecret, _ = cfg.Get("CookieSecret", "")
}
}

func (cfg *Config) Get(key string, encryptionKey string) (string, error) {
var userConfig UserConfig
cfg.db.Where("key = ?", key).Limit(1).Find(&userConfig)

value := userConfig.Value
if userConfig.Value != "" && encryptionKey != "" && userConfig.Encrypted {
decrypted, err := AesGcmDecrypt(value, encryptionKey)
if err != nil {
return "", err
}
value = decrypted
}
return value, nil
}

func (cfg *Config) set(key string, value string, clauses clause.OnConflict, encryptionKey string) bool {
if encryptionKey != "" {
decrypted, err := AesGcmEncrypt(value, encryptionKey)
if err == nil {
value = decrypted
}
}
userConfig := UserConfig{Key: key, Value: value, Encrypted: encryptionKey != ""}
result := cfg.db.Clauses(clauses).Create(&userConfig)

return result.Error == nil
}

func (cfg *Config) SetIgnore(key string, value string, encryptionKey string) bool {
clauses := clause.OnConflict{
Columns: []clause.Column{{Name: "key"}},
DoNothing: true,
}
return cfg.set(key, value, clauses, encryptionKey)
}

func (cfg *Config) SetUpdate(key string, value string, encryptionKey string) bool {
clauses := clause.OnConflict{
Columns: []clause.Column{{Name: "key"}},
DoUpdates: clause.AssignmentColumns([]string{"value"}),
}
return cfg.set(key, value, clauses, encryptionKey)
}

func randomHex(n int) (string, error) {
bytes := make([]byte, n)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
21 changes: 18 additions & 3 deletions echo_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func (svc *Service) RegisterSharedRoutes(e *echo.Echo) {
e.GET("/api/info", svc.InfoHandler)
e.POST("/api/logout", svc.LogoutHandler)
e.POST("/api/setup", svc.SetupHandler)
e.POST("/api/start", svc.StartHandler)

frontend.RegisterHandlers(e)
}
Expand All @@ -67,6 +68,23 @@ func (svc *Service) InfoHandler(c echo.Context) error {
return c.JSON(http.StatusOK, responseBody)
}

func (svc *Service) StartHandler(c echo.Context) error {
var startRequest api.StartRequest
if err := c.Bind(&startRequest); err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: fmt.Sprintf("Bad request: %s", err.Error()),
})
}

err := svc.Start(&startRequest)
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to start node: %s", err.Error()),
})
}
return c.NoContent(http.StatusNoContent)
}

func (svc *Service) LogoutHandler(c echo.Context) error {
sess, err := session.Get(CookieName, c)
if err != nil {
Expand All @@ -75,9 +93,6 @@ func (svc *Service) LogoutHandler(c echo.Context) error {
})
}
sess.Options.MaxAge = -1
if svc.cfg.CookieDomain != "" {
sess.Options.Domain = svc.cfg.CookieDomain
}
if err := sess.Save(c.Request(), c.Response()); err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: "Failed to save session",
Expand Down
Loading
Loading