diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 031a370a..4a4244ae 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -723,46 +723,53 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve } } -func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.Event) error { - bkpEvent, ok := event.Properties.(*events.StaticChannelsBackupEvent) - if !ok { - return fmt.Errorf("invalid nwc_backup_channels event properties, could not cast to the expected type: %+v", event.Properties) - } +type channelsBackup struct { + Description string `json:"description"` + Data string `json:"data"` +} - token, err := svc.fetchUserToken(ctx) +func (svc *albyOAuthService) createEncryptedChannelBackup(event *events.StaticChannelsBackupEvent) (*channelsBackup, error) { + + eventData := bytes.NewBuffer([]byte{}) + err := json.NewEncoder(eventData).Encode(event) if err != nil { - return fmt.Errorf("failed to fetch user token: %w", err) + return nil, fmt.Errorf("failed to encode channels backup data: %w", err) } - client := svc.oauthConf.Client(ctx, token) + // use the encrypted mnemonic as the password to encrypt the backup data - type channelsBackup struct { - Description string `json:"description"` - Data string `json:"data"` + encrypted, err := svc.keys.EncryptChannelBackupData(eventData.String()) + if err != nil { + return nil, fmt.Errorf("failed to encrypt channels backup data: %w", err) } - eventData := bytes.NewBuffer([]byte{}) - err = json.NewEncoder(eventData).Encode(bkpEvent) - if err != nil { - return fmt.Errorf("failed to encode channels backup data: %w", err) + backup := &channelsBackup{ + Description: "channels_v2", + Data: encrypted, } + return backup, nil +} - // use the encrypted mnemonic as the password to encrypt the backup data - encryptedMnemonic, err := svc.cfg.Get("Mnemonic", "") +func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.Event) error { + bkpEvent, ok := event.Properties.(*events.StaticChannelsBackupEvent) + if !ok { + return fmt.Errorf("invalid nwc_backup_channels event properties, could not cast to the expected type: %+v", event.Properties) + } + + backup, err := svc.createEncryptedChannelBackup(bkpEvent) if err != nil { - return fmt.Errorf("failed to fetch encryption key: %w", err) + return fmt.Errorf("failed to encrypt channel backup: %w", err) } - encrypted, err := config.AesGcmEncrypt(eventData.String(), encryptedMnemonic) + token, err := svc.fetchUserToken(ctx) if err != nil { - return fmt.Errorf("failed to encrypt channels backup data: %w", err) + return fmt.Errorf("failed to fetch user token: %w", err) } + client := svc.oauthConf.Client(ctx, token) + body := bytes.NewBuffer([]byte{}) - err = json.NewEncoder(body).Encode(&channelsBackup{ - Description: "channels", - Data: encrypted, - }) + err = json.NewEncoder(body).Encode(backup) if err != nil { return fmt.Errorf("failed to encode channels backup request payload: %w", err) } diff --git a/alby/alby_oauth_service_test.go b/alby/alby_oauth_service_test.go new file mode 100644 index 00000000..92e7ffc4 --- /dev/null +++ b/alby/alby_oauth_service_test.go @@ -0,0 +1,48 @@ +package alby + +import ( + "encoding/hex" + "testing" + + "github.com/getAlby/hub/config" + "github.com/getAlby/hub/events" + "github.com/getAlby/hub/tests" + "github.com/stretchr/testify/assert" + "github.com/tyler-smith/go-bip32" + "github.com/tyler-smith/go-bip39" +) + +func TestEncryptedBackup(t *testing.T) { + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + mnemonic := "limit reward expect search tissue call visa fit thank cream brave jump" + unlockPassword := "123" + svc.Cfg.SetUpdate("Mnemonic", mnemonic, unlockPassword) + err = svc.Keys.Init(svc.Cfg, unlockPassword) + assert.Nil(t, err) + + albyOAuthSvc := NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + encryptedBackup, err := albyOAuthSvc.createEncryptedChannelBackup(&events.StaticChannelsBackupEvent{ + NodeID: "037e702144c4fa485d42f0f69864e943605823763866cf4bf619d2d2cf2eda420b", + Channels: []events.ChannelBackup{}, + Monitors: []events.EncodedChannelMonitorBackup{}, + }) + + assert.Nil(t, err) + assert.Equal(t, "channels_v2", encryptedBackup.Description) + + masterKey, err := bip32.NewMasterKey(bip39.NewSeed(mnemonic, "")) + assert.Nil(t, err) + + appKey, err := masterKey.NewChildKey(0) + assert.Nil(t, err) + encryptedChannelsBackupKey, err := appKey.NewChildKey(0) + assert.Nil(t, err) + + decrypted, err := config.AesGcmDecrypt(encryptedBackup.Data, hex.EncodeToString(encryptedChannelsBackupKey.Key)) + assert.Nil(t, err) + + assert.Equal(t, "{\"node_id\":\"037e702144c4fa485d42f0f69864e943605823763866cf4bf619d2d2cf2eda420b\",\"channels\":[],\"monitors\":[]}\n", decrypted) +} diff --git a/go.mod b/go.mod index 51d93e48..932166ab 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,8 @@ require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/DataDog/datadog-go/v5 v5.3.0 // indirect github.com/DataDog/gostackparse v0.7.0 // indirect + github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect + github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect @@ -237,6 +239,7 @@ require ( github.com/labstack/echo-jwt/v4 v4.2.0 github.com/lightningnetwork/lnd v0.18.2-beta github.com/sirupsen/logrus v1.9.3 + github.com/tyler-smith/go-bip32 v1.0.0 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect gorm.io/datatypes v1.2.4 ) diff --git a/go.sum b/go.sum index 1f507f75..a87bcae3 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,10 @@ github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/ github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= github.com/DataDog/sketches-go v1.4.5 h1:ki7VfeNz7IcNafq7yI/j5U/YCkO3LJiMDtXz9OMQbyE= github.com/DataDog/sketches-go v1.4.5/go.mod h1:7Y8GN8Jf66DLyDhc94zuWA3uHEt/7ttt8jHOBWWrSOg= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -106,6 +110,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= @@ -601,6 +606,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -634,6 +640,8 @@ github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQ github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE= +github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -720,6 +728,7 @@ go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -947,6 +956,7 @@ gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= diff --git a/service/keys/keys.go b/service/keys/keys.go index 803f3846..3f247aff 100644 --- a/service/keys/keys.go +++ b/service/keys/keys.go @@ -1,22 +1,29 @@ package keys import ( + "encoding/hex" + "github.com/getAlby/hub/config" "github.com/getAlby/hub/logger" "github.com/nbd-wtf/go-nostr" + "github.com/tyler-smith/go-bip32" + "github.com/tyler-smith/go-bip39" ) type Keys interface { Init(cfg config.Config, encryptionKey string) error - // Wallet Service Nostr pubkey + // Wallet Service Nostr pubkey (DEPRECATED) GetNostrPublicKey() string - // Wallet Service Nostr secret key + // Wallet Service Nostr secret key (DEPRECATED) GetNostrSecretKey() string + + EncryptChannelBackupData(channelBackupData string) (string, error) } type keys struct { - nostrSecretKey string - nostrPublicKey string + nostrSecretKey string + nostrPublicKey string + encryptedChannelsBackupKey string } func NewKeys() *keys { @@ -41,6 +48,36 @@ func (keys *keys) Init(cfg config.Config, encryptionKey string) error { } keys.nostrSecretKey = nostrSecretKey keys.nostrPublicKey = nostrPublicKey + + mnemonic, err := cfg.Get("Mnemonic", encryptionKey) + if err != nil { + logger.Logger.WithError(err).Error("Failed to decrypt mnemonic") + return err + } + + if mnemonic != "" { + masterKey, err := bip32.NewMasterKey(bip39.NewSeed(mnemonic, "")) + if err != nil { + logger.Logger.WithError(err).Error("Failed to create seed from mnemonic") + return err + } + + APP_INDEX := uint32(0) // TODO: choose an index + appKey, err := masterKey.NewChildKey(APP_INDEX) + if err != nil { + logger.Logger.WithError(err).Error("Failed to create seed from mnemonic") + return err + } + + ENCRYPTED_SCB_INDEX := uint32(0) // TODO: choose an index + encryptedChannelsBackupKey, err := appKey.NewChildKey(ENCRYPTED_SCB_INDEX) + if err != nil { + logger.Logger.WithError(err).Error("Failed to create seed from mnemonic") + return err + } + keys.encryptedChannelsBackupKey = hex.EncodeToString(encryptedChannelsBackupKey.Key) + } + return nil } @@ -51,3 +88,8 @@ func (keys *keys) GetNostrPublicKey() string { func (keys *keys) GetNostrSecretKey() string { return keys.nostrSecretKey } + +func (keys *keys) EncryptChannelBackupData(channelBackupData string) (string, error) { + // FIXME: this is not the right way to encrypt the data (we already have a key, no need to generate a new one) + return config.AesGcmEncrypt(channelBackupData, keys.encryptedChannelsBackupKey) +}