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
1 change: 1 addition & 0 deletions cgo/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ type Config struct {
Birthday int64 `json:"birthday"`
Pass string `json:"pass"`
Mnemonic string `json:"mnemonic"`
SeedPass string `json:"seedpass"`
// If the wallet existed before but the db was deleted to reduce
// storage, restore from the local encrypted seed using the provided
// password. Also works for watching only wallets with no password.
Expand Down
1 change: 1 addition & 0 deletions cgo/walletloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func createWallet(cConfig *C.char) *C.char {
}
recoveryConfig = &dcr.RecoveryCfg{
Seed: seed,
SeedPass: []byte(cfg.SeedPass),
SeedType: seedType,
Birthday: birthday,
}
Expand Down
51 changes: 37 additions & 14 deletions dcr/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
Expand All @@ -14,6 +15,7 @@ import (
"decred.org/dcrwallet/v4/wallet/udb"
"github.com/decred/dcrd/crypto/blake256"
"github.com/decred/dcrd/hdkeychain/v3"
"github.com/decred/libwallet/mnemonic"
)

const (
Expand Down Expand Up @@ -49,10 +51,9 @@ func CreateWallet(ctx context.Context, params CreateWalletParams, recovery *Reco
}

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

if recovery != nil {
Expand All @@ -69,10 +70,20 @@ func CreateWallet(ctx context.Context, params CreateWalletParams, recovery *Reco
if err != nil {
return nil, fmt.Errorf("unable to decrypt wallet seed: %v", err)
}
if len(wd.EncryptedSeedPassHex) != 0 {
encSeedPass, err := hex.DecodeString(wd.EncryptedSeedPassHex)
if err != nil {
return nil, fmt.Errorf("unable to decode encrypted seed pass: %v", err)
}
seedPass, err = DecryptData(encSeedPass, params.Pass)
if err != nil {
return nil, fmt.Errorf("unable to decrypt wallet seed pass: %v", err)
}
}
birthday = time.Unix(wd.Birthday, 0)
seedType = wd.SeedType
} else {
seed, birthday, seedType = recovery.Seed, recovery.Birthday, recovery.SeedType
seed, seedPass, birthday, seedType = recovery.Seed, recovery.SeedPass, recovery.Birthday, recovery.SeedType
}
} else {
seed, err = hdkeychain.GenerateSeed(entropyBytes)
Expand All @@ -83,24 +94,36 @@ func CreateWallet(ctx context.Context, params CreateWalletParams, recovery *Reco
// Seed type is default fifteen words.
}

if seedType == STFifteenWords {
// Adjust seed to create the same wallet as dex.
switch seedType {
case STFifteenWords:
// Applying the seed pass to 15 word wallets breaks existing
// wallets when no pass is supplied. Also, the mnemonic cannot
// be used by other decred software if a passphrase is applied.
if len(seedPass) != 0 {
return nil, errors.New("seed passphrase cannot be used with 15 word mnemonics")
}
// Adjust seed to create the same wallet as bison wallet.
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 }
tweakedSeed = ts[:]
case STTwelveWords, STTwentyFourWords:
words, err := mnemonic.GenerateMnemonic(seed)
if err != nil {
return nil, fmt.Errorf("unable to recreate seed mnemonic: %v", err)
}
// Apply even if password is null.
tweakedSeed = mnemonic.ApplyPassphrase(seed, seedPass, words)
}

_, _, _, 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, seedType)
wd, err := saveWalletData(seed, seedPass, xpub.String(), birthday, params.DataDir, params.Pass, seedType)
if err != nil {
return nil, fmt.Errorf("saveWalletData error: %v", err)
}
Expand Down Expand Up @@ -130,7 +153,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 @@ -208,7 +231,7 @@ 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, 0) // password not required
wd, err := saveWalletData(nil, nil, xpub.String(), time.Time{}, params.DataDir, nil, 0) // password not required
if err != nil {
return nil, fmt.Errorf("saveWalletData error: %v", 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
SeedPass []byte
SeedType SeedType
Birthday time.Time
UseLocalSeed bool
Expand Down
17 changes: 15 additions & 2 deletions dcr/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,28 @@ func (w *Wallet) ReEncryptSeed(oldPass, newPass []byte) error {
return err
}

var seedPass []byte
if len(w.metaData.EncryptedSeedPassHex) != 0 {
encSeedPass, err := hex.DecodeString(w.metaData.EncryptedSeedPassHex)
if err != nil {
return fmt.Errorf("unable to decode encrypted seed pass: %v", err)
}
seedPass, err = DecryptData(encSeedPass, oldPass)
if err != nil {
return fmt.Errorf("unable to decrypt wallet seed pass: %v", err)
}
}

birthday := time.Unix(w.metaData.Birthday, 0)
updatedMetaData, err := saveWalletData(seed, w.metaData.DefaultAccountXPub, birthday, w.dir, newPass, w.metaData.SeedType)
updatedMetaData, err := saveWalletData(seed, seedPass, w.metaData.DefaultAccountXPub, birthday, w.dir, newPass, w.metaData.SeedType)
if err != nil {
return err
}

// Update only the EncryptedSeedHex field since we've held the seedMtx lock
// Update only the EncryptedSeedHex and pass field since we've held the seedMtx lock
// above.
w.metaData.EncryptedSeedHex = updatedMetaData.EncryptedSeedHex
w.metaData.EncryptedSeedPassHex = updatedMetaData.EncryptedSeedPassHex
return nil
}

Expand Down
29 changes: 20 additions & 9 deletions dcr/walletdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,35 @@ const (
const walletDataFileName = "walletdata.json"

type walletData struct {
EncryptedSeedHex string `json:"encryptedseedhex,omitempty"`
SeedType SeedType `json:"seedtype,omitempty"`
DefaultAccountXPub string `json:"defaultaccountxpub,omitempty"`
Birthday int64 `json:"birthday,omitempty"`
EncryptedSeedHex string `json:"encryptedseedhex,omitempty"`
EncryptedSeedPassHex string `json:"encryptedseedpasshex,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, seedType SeedType) (*walletData, error) {
func saveWalletData(seed, seedPass []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)
}

encSeedPassHex := ""
if len(seedPass) != 0 {
encSeedPass, err := EncryptData(seedPass, walletPass)
if err != nil {
return nil, fmt.Errorf("seed pass encryption error: %v", err)
}
encSeedPassHex = hex.EncodeToString(encSeedPass)
}

encSeedHex := hex.EncodeToString(encSeed)
wd := &walletData{
EncryptedSeedHex: encSeedHex,
DefaultAccountXPub: defaultAccountXPub,
Birthday: birthday.Unix(),
SeedType: seedType,
EncryptedSeedHex: encSeedHex,
EncryptedSeedPassHex: encSeedPassHex,
DefaultAccountXPub: defaultAccountXPub,
Birthday: birthday.Unix(),
SeedType: seedType,
}
file, err := json.MarshalIndent(wd, "", " ")
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/jrick/logrotate v1.1.2
github.com/kevinburke/nacl v0.0.0-20210405173606-cd9060f5f776
golang.org/x/crypto v0.33.0
golang.org/x/text v0.22.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
Expand Down
16 changes: 16 additions & 0 deletions mnemonic/seed.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package mnemonic

import (
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"sort"
"strings"

"golang.org/x/crypto/pbkdf2"
"golang.org/x/text/unicode/norm"
)

// DecodeMnemonic decodes the entropy and checksum from mnemonic and validates
Expand Down Expand Up @@ -132,3 +136,15 @@ func wordIndex(word string) (uint16, error) {
}
return uint16(i), nil
}

// ApplyPassphrase returns the seed after applying passphrase. Follows
// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
func ApplyPassphrase(seed, pass []byte, words string) []byte {
const (
iterations = 2048
keyLen = 64
)
password := norm.NFC.String(words)
salt := "mnemonic" + norm.NFC.String(string(pass))
return pbkdf2.Key([]byte(password), []byte(salt), iterations, keyLen, sha512.New)
}
Loading