Skip to content

Commit 29c832e

Browse files
authored
multi: Add 12 and 24 word seed restoration. (#33)
* github: Update linter. * multi: Add 12 and 24 word seed restoration.
1 parent f4d456d commit 29c832e

12 files changed

Lines changed: 2517 additions & 46 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
key: go-lint-${{ matrix.go }}-${{ hashFiles('./go.sum') }}
2626
restore-keys: go-lint-${{ matrix.go }}
2727
- name: Install Linters
28-
run: "go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.1"
28+
run: "curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0"
2929
- name: Build
3030
run: go build ./...
3131
- name: Test

.golangci.yml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1+
version: "2"
12
run:
2-
deadline: 10m
3-
4-
output:
5-
formats: colored-line-number
6-
3+
timeout: 10m
74
linters:
8-
disable-all: true
5+
default: none
96
enable:
107
- asciicheck
118
- bidichk
129
- durationcheck
13-
- copyloopvar
14-
- gofmt
15-
- goimports
16-
- gosimple
1710
- govet
1811
- grouper
1912
- ineffassign
@@ -22,6 +15,14 @@ linters:
2215
- reassign
2316
- rowserrcheck
2417
- sqlclosecheck
18+
- staticcheck
2519
- tparallel
26-
- typecheck
2720
- unconvert
21+
settings:
22+
staticcheck:
23+
checks:
24+
- S1*
25+
formatters:
26+
enable:
27+
- gofmt
28+
- goimports

cgo/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ type Config struct {
163163
Net string `json:"net"`
164164
DataDir string `json:"datadir"`
165165
// Only needed during creation.
166+
Birthday int64 `json:"birthday"`
166167
Pass string `json:"pass"`
167168
Mnemonic string `json:"mnemonic"`
168169
// If the wallet existed before but the db was deleted to reduce

cgo/walletloader.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import "C"
44
import (
55
"context"
66
"encoding/json"
7+
"strings"
78
"sync"
9+
"time"
810

9-
"decred.org/dcrdex/client/mnemonic"
11+
dexmnemonic "decred.org/dcrdex/client/mnemonic"
1012
"github.com/decred/libwallet/dcr"
13+
"github.com/decred/libwallet/mnemonic"
1114
"github.com/decred/slog"
1215
)
1316

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

6063
var recoveryConfig *dcr.RecoveryCfg
6164
if cfg.Mnemonic != "" {
62-
seed, birthday, err := mnemonic.DecodeMnemonic(cfg.Mnemonic)
65+
var (
66+
seed []byte
67+
birthday time.Time
68+
seedType dcr.SeedType
69+
err error
70+
)
71+
nWords := len(strings.Fields(cfg.Mnemonic))
72+
switch nWords {
73+
case 15:
74+
seed, birthday, err = dexmnemonic.DecodeMnemonic(cfg.Mnemonic)
75+
seedType = dcr.STFifteenWords
76+
case 12:
77+
seed, err = mnemonic.DecodeMnemonic(cfg.Mnemonic)
78+
birthday = time.Unix(cfg.Birthday, 0)
79+
seedType = dcr.STTwelveWords
80+
case 24:
81+
seed, err = mnemonic.DecodeMnemonic(cfg.Mnemonic)
82+
birthday = time.Unix(cfg.Birthday, 0)
83+
seedType = dcr.STTwentyFourWords
84+
default:
85+
return errCResponse("unknown mnemonic format. expected 12, 15, or 24 words, got %d", nWords)
86+
}
6387
if err != nil {
6488
return errCResponse("unable to decode wallet mnemonic: %v", err)
6589
}
6690
recoveryConfig = &dcr.RecoveryCfg{
6791
Seed: seed,
92+
SeedType: seedType,
6893
Birthday: birthday,
6994
}
7095
}

dcr/dcr_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,46 @@ func TestDecodeTx(t *testing.T) {
143143
})
144144
}
145145
}
146+
147+
func TestDecryptSeed(t *testing.T) {
148+
metaData := new(walletData)
149+
w := &Wallet{metaData: metaData}
150+
151+
tests := []struct {
152+
name, seed, wantMnemonic string
153+
pass []byte
154+
birthday int64
155+
seedType SeedType
156+
}{{
157+
name: "ok 15",
158+
seed: "3bb9a36312986f1bcf50ba8be3e07d158be3222c9354d4c65cc6f57863c3589bdd2fccd9dbf06d082a1abafb1d63707964c1cb4c14e8843abdb1",
159+
birthday: 1740614400,
160+
pass: []byte("pass"),
161+
wantMnemonic: "peace option follow minute useful proud orphan zero truck response satisfy shell need chef silly",
162+
}, {
163+
name: "ok 12",
164+
seed: "ce6561d42c8f54f47915b8261e83ecb6b97cf7ab11096ee1d4f2560740cb1646b2f0f6bd614caae03bdcef0241b1e7ee44caa36cbccefc10",
165+
pass: []byte("pass"),
166+
seedType: STTwelveWords,
167+
wantMnemonic: "length you letter page olive equip proud solve goose spirit easily orchard",
168+
}, {
169+
name: "ok 24",
170+
seed: "923713f26091c7587b31ebf695801bf1ab260ae98a7cdbc4d65c01167b088ca77f65996ca0d885d9cf73bded3b3ae84c396ec6c7170ff0a66b821cd3dc4cae9a4f8924dd5f39fd91",
171+
pass: []byte("pass"),
172+
seedType: STTwentyFourWords,
173+
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",
174+
}}
175+
176+
for _, test := range tests {
177+
t.Run(test.name, func(t *testing.T) {
178+
metaData.Birthday, metaData.EncryptedSeedHex, metaData.SeedType = test.birthday, test.seed, test.seedType
179+
mnemonic, err := w.DecryptSeed(test.pass)
180+
if err != nil {
181+
t.Fatalf("unexpected error %v", err)
182+
}
183+
if mnemonic != test.wantMnemonic {
184+
t.Fatalf("expected mnemonic %v but got %v", test.wantMnemonic, mnemonic)
185+
}
186+
})
187+
}
188+
}

dcr/loader.go

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,16 @@ func CreateWallet(ctx context.Context, params CreateWalletParams, recovery *Reco
4848
return nil, fmt.Errorf("check new wallet data directory error: %w", err)
4949
}
5050

51-
var seed []byte
52-
var birthday time.Time
51+
var (
52+
seed []byte
53+
tweakedSeed func() []byte
54+
birthday time.Time
55+
seedType SeedType
56+
)
57+
5358
if recovery != nil {
5459
if recovery.UseLocalSeed {
55-
wd, err := RetreiveWalletData(params.DataDir)
60+
wd, err := retrieveWalletData(params.DataDir)
5661
if err != nil {
5762
return nil, fmt.Errorf("unable to get wallet data: %v", err)
5863
}
@@ -65,33 +70,39 @@ func CreateWallet(ctx context.Context, params CreateWalletParams, recovery *Reco
6570
return nil, fmt.Errorf("unable to decrypt wallet seed: %v", err)
6671
}
6772
birthday = time.Unix(wd.Birthday, 0)
73+
seedType = wd.SeedType
6874
} else {
69-
seed, birthday = recovery.Seed, recovery.Birthday
75+
seed, birthday, seedType = recovery.Seed, recovery.Birthday, recovery.SeedType
7076
}
7177
} else {
7278
seed, err = hdkeychain.GenerateSeed(entropyBytes)
7379
if err != nil {
7480
return nil, fmt.Errorf("unable to generate random seed: %v", err)
7581
}
7682
birthday = time.Now()
83+
// Seed type is default fifteen words.
7784
}
7885

79-
// Adjust seed to create the same wallet as dex.
80-
b := make([]byte, len(seed)+4)
81-
copy(b, seed)
82-
binary.BigEndian.PutUint32(b[len(seed):], 42)
83-
tweakedSeed := blake256.Sum256(b)
86+
if seedType == STFifteenWords {
87+
// Adjust seed to create the same wallet as dex.
88+
b := make([]byte, len(seed)+4)
89+
copy(b, seed)
90+
binary.BigEndian.PutUint32(b[len(seed):], 42)
91+
ts := blake256.Sum256(b)
92+
tweakedSeed = func() []byte { return ts[:] }
93+
} else {
94+
tweakedSeed = func() []byte { return seed }
95+
}
8496

85-
_, _, _, acctKeySLIP0044Priv, err := udb.HDKeysFromSeed(tweakedSeed[:], chainParams)
97+
_, _, _, acctKeySLIP0044Priv, err := udb.HDKeysFromSeed(tweakedSeed(), chainParams)
8698
if err != nil {
8799
return nil, err
88100
}
89101
defer acctKeySLIP0044Priv.Zero()
90102
xpub := acctKeySLIP0044Priv.Neuter()
91-
92-
wd, err := SaveWalletData(seed, xpub.String(), birthday, params.DataDir, params.Pass)
103+
wd, err := saveWalletData(seed, xpub.String(), birthday, params.DataDir, params.Pass, seedType)
93104
if err != nil {
94-
return nil, fmt.Errorf("SaveWalletData error: %v", err)
105+
return nil, fmt.Errorf("saveWalletData error: %v", err)
95106
}
96107

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

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

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

200-
wd, err := SaveWalletData(nil, xpub.String(), time.Time{}, params.DataDir, nil) // password not required
211+
wd, err := saveWalletData(nil, xpub.String(), time.Time{}, params.DataDir, nil, 0) // password not required
201212
if err != nil {
202-
return nil, fmt.Errorf("SaveWalletData error: %v", err)
213+
return nil, fmt.Errorf("saveWalletData error: %v", err)
203214
}
204215

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

269-
wd, err := RetreiveWalletData(params.DataDir)
280+
wd, err := retrieveWalletData(params.DataDir)
270281
if err != nil {
271282
return nil, err
272283
}

dcr/params.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type CreateWalletParams struct {
2424
// RecoveryCfg is the information used to recover a wallet.
2525
type RecoveryCfg struct {
2626
Seed []byte
27+
SeedType SeedType
2728
Birthday time.Time
2829
UseLocalSeed bool
2930
NumExternalAddresses uint32

dcr/wallet.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ package dcr
33
import (
44
"context"
55
"encoding/hex"
6+
"errors"
67
"fmt"
78
"path/filepath"
89
"sync"
910
"time"
1011

11-
"decred.org/dcrdex/client/mnemonic"
12+
dexmnemonic "decred.org/dcrdex/client/mnemonic"
1213
"decred.org/dcrwallet/v4/spv"
1314
"decred.org/dcrwallet/v4/wallet"
1415
"github.com/decred/dcrd/chaincfg/v3"
16+
"github.com/decred/libwallet/mnemonic"
1517
"github.com/decred/slog"
1618
)
1719

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

49-
if w.metaData.EncryptedSeedHex == "" {
50-
return "", fmt.Errorf("seed has been verified")
51-
}
52-
5351
encryptedSeed, err := hex.DecodeString(w.metaData.EncryptedSeedHex)
5452
if err != nil {
5553
return "", fmt.Errorf("unable to decode encrypted hex seed: %v", err)
@@ -59,15 +57,24 @@ func (w *Wallet) DecryptSeed(passphrase []byte) (string, error) {
5957
if err != nil {
6058
return "", err
6159
}
62-
return mnemonic.GenerateMnemonic(seed, time.Unix(w.metaData.Birthday, 0))
60+
61+
switch w.metaData.SeedType {
62+
case STFifteenWords:
63+
return dexmnemonic.GenerateMnemonic(seed, time.Unix(w.metaData.Birthday, 0))
64+
case STTwelveWords, STTwentyFourWords:
65+
return mnemonic.GenerateMnemonic(seed)
66+
default:
67+
return "", fmt.Errorf("invalid saved seed length %d", len(seed))
68+
}
6369
}
6470

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

6976
if w.metaData.EncryptedSeedHex == "" {
70-
return nil
77+
return errors.New("encrypted seed does not exist")
7178
}
7279

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

8390
birthday := time.Unix(w.metaData.Birthday, 0)
84-
updatedMetaData, err := SaveWalletData(seed, w.metaData.DefaultAccountXPub, birthday, w.dir, newPass)
91+
updatedMetaData, err := saveWalletData(seed, w.metaData.DefaultAccountXPub, birthday, w.dir, newPass, w.metaData.SeedType)
8592
if err != nil {
8693
return err
8794
}

dcr/walletdata.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,29 @@ import (
99
"time"
1010
)
1111

12+
// SeedType defines the type of seed used by the wallet. Currently all use bip39
13+
// words but they are different lengths.
14+
type SeedType int
15+
16+
const (
17+
// STFifteenWords encodes a birthday along with the seed. It is also
18+
// used by bison wallet and has a tweak that causes it to produce the
19+
// same wallet.
20+
STFifteenWords SeedType = iota // 0
21+
STTwelveWords // 1
22+
STTwentyFourWords // 2
23+
)
24+
1225
const walletDataFileName = "walletdata.json"
1326

1427
type walletData struct {
15-
EncryptedSeedHex string `json:"encryptedseedhex,omitempty"`
16-
DefaultAccountXPub string `json:"defaultaccountxpub,omitempty"`
17-
Birthday int64 `json:"birthday,omitempty"`
28+
EncryptedSeedHex string `json:"encryptedseedhex,omitempty"`
29+
SeedType SeedType `json:"seedtype,omitempty"`
30+
DefaultAccountXPub string `json:"defaultaccountxpub,omitempty"`
31+
Birthday int64 `json:"birthday,omitempty"`
1832
}
1933

20-
func SaveWalletData(seed []byte, defaultAccountXPub string, birthday time.Time, dataDir string, walletPass []byte) (*walletData, error) {
34+
func saveWalletData(seed []byte, defaultAccountXPub string, birthday time.Time, dataDir string, walletPass []byte, seedType SeedType) (*walletData, error) {
2135
encSeed, err := EncryptData(seed, walletPass)
2236
if err != nil {
2337
return nil, fmt.Errorf("seed encryption error: %v", err)
@@ -28,6 +42,7 @@ func SaveWalletData(seed []byte, defaultAccountXPub string, birthday time.Time,
2842
EncryptedSeedHex: encSeedHex,
2943
DefaultAccountXPub: defaultAccountXPub,
3044
Birthday: birthday.Unix(),
45+
SeedType: seedType,
3146
}
3247
file, err := json.MarshalIndent(wd, "", " ")
3348
if err != nil {
@@ -41,8 +56,8 @@ func SaveWalletData(seed []byte, defaultAccountXPub string, birthday time.Time,
4156
return wd, nil
4257
}
4358

44-
// RetreiveWalletData returns the wallet data from the data dir.
45-
func RetreiveWalletData(dataDir string) (*walletData, error) {
59+
// retrieveWalletData returns the wallet data from the data dir.
60+
func retrieveWalletData(dataDir string) (*walletData, error) {
4661
fp := filepath.Join(dataDir, walletDataFileName)
4762
b, err := os.ReadFile(fp)
4863
if err != nil {

0 commit comments

Comments
 (0)