Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
key: go-lint-${{ matrix.go }}-${{ hashFiles('./go.sum') }}
restore-keys: go-lint-${{ matrix.go }}
- name: Install Linters
run: "go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.1"
run: "curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0"
- name: Build
run: go build ./...
- name: Test
Expand Down
23 changes: 12 additions & 11 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
version: "2"
run:
deadline: 10m

output:
formats: colored-line-number

timeout: 10m
linters:
disable-all: true
default: none
enable:
- asciicheck
- bidichk
- durationcheck
- copyloopvar
- gofmt
- goimports
- gosimple
- govet
- grouper
- ineffassign
Expand All @@ -22,6 +15,14 @@ linters:
- reassign
- rowserrcheck
- sqlclosecheck
- staticcheck
- tparallel
- typecheck
- unconvert
settings:
staticcheck:
checks:
- S1*
formatters:
enable:
- gofmt
- goimports
1 change: 1 addition & 0 deletions cgo/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ type Config struct {
Net string `json:"net"`
DataDir string `json:"datadir"`
// Only needed during creation.
Birthday int64 `json:"birthday"`
Pass string `json:"pass"`
Mnemonic string `json:"mnemonic"`
// If the wallet existed before but the db was deleted to reduce
Expand Down
29 changes: 27 additions & 2 deletions cgo/walletloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import "C"
import (
"context"
"encoding/json"
"strings"
"sync"
"time"

"decred.org/dcrdex/client/mnemonic"
dexmnemonic "decred.org/dcrdex/client/mnemonic"
"github.com/decred/libwallet/dcr"
"github.com/decred/libwallet/mnemonic"
"github.com/decred/slog"
)

Expand Down Expand Up @@ -59,12 +62,34 @@ func createWallet(cConfig *C.char) *C.char {

var recoveryConfig *dcr.RecoveryCfg
if cfg.Mnemonic != "" {
seed, birthday, err := mnemonic.DecodeMnemonic(cfg.Mnemonic)
var (
seed []byte
birthday time.Time
seedType dcr.SeedType
err error
)
nWords := len(strings.Fields(cfg.Mnemonic))
switch nWords {
case 15:
seed, birthday, err = dexmnemonic.DecodeMnemonic(cfg.Mnemonic)
seedType = dcr.STFifteenWords
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe doesn't even need the type... If this is how we tell in the beginning, can just always do this I guess

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving the type for now. I guess could have more types in the future that are the same length as current. This also reads better later.

case 12:
seed, err = mnemonic.DecodeMnemonic(cfg.Mnemonic)
birthday = time.Unix(cfg.Birthday, 0)
seedType = dcr.STTwelveWords
case 24:
seed, err = mnemonic.DecodeMnemonic(cfg.Mnemonic)
birthday = time.Unix(cfg.Birthday, 0)
seedType = dcr.STTwentyFourWords
default:
return errCResponse("unknown mnemonic format. expected 12, 15, or 24 words, got %d", nWords)
}
if err != nil {
return errCResponse("unable to decode wallet mnemonic: %v", err)
}
recoveryConfig = &dcr.RecoveryCfg{
Seed: seed,
SeedType: seedType,
Birthday: birthday,
}
}
Expand Down
43 changes: 43 additions & 0 deletions dcr/dcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,46 @@ func TestDecodeTx(t *testing.T) {
})
}
}

func TestDecryptSeed(t *testing.T) {
metaData := new(walletData)
w := &Wallet{metaData: metaData}

tests := []struct {
name, seed, wantMnemonic string
pass []byte
birthday int64
seedType SeedType
}{{
name: "ok 15",
seed: "3bb9a36312986f1bcf50ba8be3e07d158be3222c9354d4c65cc6f57863c3589bdd2fccd9dbf06d082a1abafb1d63707964c1cb4c14e8843abdb1",
birthday: 1740614400,
pass: []byte("pass"),
wantMnemonic: "peace option follow minute useful proud orphan zero truck response satisfy shell need chef silly",
}, {
name: "ok 12",
seed: "ce6561d42c8f54f47915b8261e83ecb6b97cf7ab11096ee1d4f2560740cb1646b2f0f6bd614caae03bdcef0241b1e7ee44caa36cbccefc10",
pass: []byte("pass"),
seedType: STTwelveWords,
wantMnemonic: "length you letter page olive equip proud solve goose spirit easily orchard",
}, {
name: "ok 24",
seed: "923713f26091c7587b31ebf695801bf1ab260ae98a7cdbc4d65c01167b088ca77f65996ca0d885d9cf73bded3b3ae84c396ec6c7170ff0a66b821cd3dc4cae9a4f8924dd5f39fd91",
pass: []byte("pass"),
seedType: STTwentyFourWords,
wantMnemonic: "flag thank useful eight cattle smile digital bar minute traffic kidney aunt heart capital glory salad alert brass neutral resource speak seat month follow",
}}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
metaData.Birthday, metaData.EncryptedSeedHex, metaData.SeedType = test.birthday, test.seed, test.seedType
mnemonic, err := w.DecryptSeed(test.pass)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
if mnemonic != test.wantMnemonic {
t.Fatalf("expected mnemonic %v but got %v", test.wantMnemonic, mnemonic)
}
})
}
}
47 changes: 29 additions & 18 deletions dcr/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,16 @@ func CreateWallet(ctx context.Context, params CreateWalletParams, recovery *Reco
return nil, fmt.Errorf("check new wallet data directory error: %w", err)
}

var seed []byte
var birthday time.Time
var (
seed []byte
tweakedSeed func() []byte
birthday time.Time
seedType SeedType
)

if recovery != nil {
if recovery.UseLocalSeed {
wd, err := RetreiveWalletData(params.DataDir)
wd, err := retrieveWalletData(params.DataDir)
if err != nil {
return nil, fmt.Errorf("unable to get wallet data: %v", err)
}
Expand All @@ -65,33 +70,39 @@ func CreateWallet(ctx context.Context, params CreateWalletParams, recovery *Reco
return nil, fmt.Errorf("unable to decrypt wallet seed: %v", err)
}
birthday = time.Unix(wd.Birthday, 0)
seedType = wd.SeedType
} else {
seed, birthday = recovery.Seed, recovery.Birthday
seed, birthday, seedType = recovery.Seed, recovery.Birthday, recovery.SeedType
}
} else {
seed, err = hdkeychain.GenerateSeed(entropyBytes)
if err != nil {
return nil, fmt.Errorf("unable to generate random seed: %v", err)
}
birthday = time.Now()
// Seed type is default fifteen words.
}

// Adjust seed to create the same wallet as dex.
b := make([]byte, len(seed)+4)
copy(b, seed)
binary.BigEndian.PutUint32(b[len(seed):], 42)
tweakedSeed := blake256.Sum256(b)
if seedType == STFifteenWords {
// Adjust seed to create the same wallet as dex.
b := make([]byte, len(seed)+4)
copy(b, seed)
binary.BigEndian.PutUint32(b[len(seed):], 42)
ts := blake256.Sum256(b)
tweakedSeed = func() []byte { return ts[:] }
} else {
tweakedSeed = func() []byte { return seed }
}

_, _, _, acctKeySLIP0044Priv, err := udb.HDKeysFromSeed(tweakedSeed[:], chainParams)
_, _, _, acctKeySLIP0044Priv, err := udb.HDKeysFromSeed(tweakedSeed(), chainParams)
if err != nil {
return nil, err
}
defer acctKeySLIP0044Priv.Zero()
xpub := acctKeySLIP0044Priv.Neuter()

wd, err := SaveWalletData(seed, xpub.String(), birthday, params.DataDir, params.Pass)
wd, err := saveWalletData(seed, xpub.String(), birthday, params.DataDir, params.Pass, seedType)
if err != nil {
return nil, fmt.Errorf("SaveWalletData error: %v", err)
return nil, fmt.Errorf("saveWalletData error: %v", err)
}

ctx, cancel := context.WithTimeout(ctx, time.Minute)
Expand Down Expand Up @@ -119,7 +130,7 @@ func CreateWallet(ctx context.Context, params CreateWalletParams, recovery *Reco
}()

// Initialize the newly created database for the wallet before opening.
err = wallet.Create(ctx, db, nil, params.Pass, tweakedSeed[:], chainParams)
err = wallet.Create(ctx, db, nil, params.Pass, tweakedSeed(), chainParams)
if err != nil {
return nil, fmt.Errorf("wallet.Create error: %w", err)
}
Expand Down Expand Up @@ -185,7 +196,7 @@ func CreateWatchOnlyWallet(ctx context.Context, extendedPubKey string, params Cr
}

if useLocalSeed {
wd, err := RetreiveWalletData(params.DataDir)
wd, err := retrieveWalletData(params.DataDir)
if err != nil {
return nil, fmt.Errorf("unable to get wallet data: %v", err)
}
Expand All @@ -197,9 +208,9 @@ func CreateWatchOnlyWallet(ctx context.Context, extendedPubKey string, params Cr
return nil, fmt.Errorf("unable to parse extended key: %w", err)
}

wd, err := SaveWalletData(nil, xpub.String(), time.Time{}, params.DataDir, nil) // password not required
wd, err := saveWalletData(nil, xpub.String(), time.Time{}, params.DataDir, nil, 0) // password not required
if err != nil {
return nil, fmt.Errorf("SaveWalletData error: %v", err)
return nil, fmt.Errorf("saveWalletData error: %v", err)
}

ctx, cancel := context.WithTimeout(ctx, time.Minute)
Expand Down Expand Up @@ -266,7 +277,7 @@ func LoadWallet(ctx context.Context, params OpenWalletParams) (*Wallet, error) {
return nil, fmt.Errorf("error parsing chain params: %w", err)
}

wd, err := RetreiveWalletData(params.DataDir)
wd, err := retrieveWalletData(params.DataDir)
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions dcr/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type CreateWalletParams struct {
// RecoveryCfg is the information used to recover a wallet.
type RecoveryCfg struct {
Seed []byte
SeedType SeedType
Birthday time.Time
UseLocalSeed bool
NumExternalAddresses uint32
Expand Down
23 changes: 15 additions & 8 deletions dcr/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ package dcr
import (
"context"
"encoding/hex"
"errors"
"fmt"
"path/filepath"
"sync"
"time"

"decred.org/dcrdex/client/mnemonic"
dexmnemonic "decred.org/dcrdex/client/mnemonic"
"decred.org/dcrwallet/v4/spv"
"decred.org/dcrwallet/v4/wallet"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/libwallet/mnemonic"
"github.com/decred/slog"
)

Expand Down Expand Up @@ -46,10 +48,6 @@ func (w *Wallet) DecryptSeed(passphrase []byte) (string, error) {
w.seedMtx.Lock()
defer w.seedMtx.Unlock()

if w.metaData.EncryptedSeedHex == "" {
return "", fmt.Errorf("seed has been verified")
}

encryptedSeed, err := hex.DecodeString(w.metaData.EncryptedSeedHex)
if err != nil {
return "", fmt.Errorf("unable to decode encrypted hex seed: %v", err)
Expand All @@ -59,15 +57,24 @@ func (w *Wallet) DecryptSeed(passphrase []byte) (string, error) {
if err != nil {
return "", err
}
return mnemonic.GenerateMnemonic(seed, time.Unix(w.metaData.Birthday, 0))

switch w.metaData.SeedType {
case STFifteenWords:
return dexmnemonic.GenerateMnemonic(seed, time.Unix(w.metaData.Birthday, 0))
case STTwelveWords, STTwentyFourWords:
return mnemonic.GenerateMnemonic(seed)
default:
return "", fmt.Errorf("invalid saved seed length %d", len(seed))
}
}

// ReEncryptSeed reads the seed with the old pass and encrypts it with the new pass.
func (w *Wallet) ReEncryptSeed(oldPass, newPass []byte) error {
w.seedMtx.Lock()
defer w.seedMtx.Unlock()

if w.metaData.EncryptedSeedHex == "" {
return nil
return errors.New("encrypted seed does not exist")
}

encryptedSeed, err := hex.DecodeString(w.metaData.EncryptedSeedHex)
Expand All @@ -81,7 +88,7 @@ func (w *Wallet) ReEncryptSeed(oldPass, newPass []byte) error {
}

birthday := time.Unix(w.metaData.Birthday, 0)
updatedMetaData, err := SaveWalletData(seed, w.metaData.DefaultAccountXPub, birthday, w.dir, newPass)
updatedMetaData, err := saveWalletData(seed, w.metaData.DefaultAccountXPub, birthday, w.dir, newPass, w.metaData.SeedType)
if err != nil {
return err
}
Expand Down
27 changes: 21 additions & 6 deletions dcr/walletdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,29 @@ import (
"time"
)

// SeedType defines the type of seed used by the wallet. Currently all use bip39
// words but they are different lengths.
type SeedType int

const (
// STFifteenWords encodes a birthday along with the seed. It is also
// used by bison wallet and has a tweak that causes it to produce the
// same wallet.
STFifteenWords SeedType = iota // 0
STTwelveWords // 1
STTwentyFourWords // 2
)

const walletDataFileName = "walletdata.json"

type walletData struct {
EncryptedSeedHex string `json:"encryptedseedhex,omitempty"`
DefaultAccountXPub string `json:"defaultaccountxpub,omitempty"`
Birthday int64 `json:"birthday,omitempty"`
EncryptedSeedHex string `json:"encryptedseedhex,omitempty"`
SeedType SeedType `json:"seedtype,omitempty"`
DefaultAccountXPub string `json:"defaultaccountxpub,omitempty"`
Birthday int64 `json:"birthday,omitempty"`
}

func SaveWalletData(seed []byte, defaultAccountXPub string, birthday time.Time, dataDir string, walletPass []byte) (*walletData, error) {
func saveWalletData(seed []byte, defaultAccountXPub string, birthday time.Time, dataDir string, walletPass []byte, seedType SeedType) (*walletData, error) {
encSeed, err := EncryptData(seed, walletPass)
if err != nil {
return nil, fmt.Errorf("seed encryption error: %v", err)
Expand All @@ -28,6 +42,7 @@ func SaveWalletData(seed []byte, defaultAccountXPub string, birthday time.Time,
EncryptedSeedHex: encSeedHex,
DefaultAccountXPub: defaultAccountXPub,
Birthday: birthday.Unix(),
SeedType: seedType,
}
file, err := json.MarshalIndent(wd, "", " ")
if err != nil {
Expand All @@ -41,8 +56,8 @@ func SaveWalletData(seed []byte, defaultAccountXPub string, birthday time.Time,
return wd, nil
}

// RetreiveWalletData returns the wallet data from the data dir.
func RetreiveWalletData(dataDir string) (*walletData, error) {
// retrieveWalletData returns the wallet data from the data dir.
func retrieveWalletData(dataDir string) (*walletData, error) {
fp := filepath.Join(dataDir, walletDataFileName)
b, err := os.ReadFile(fp)
if err != nil {
Expand Down
Loading