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

multi: add support for password recovery using seed #2477

Merged
merged 9 commits into from
Oct 16, 2023
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
175 changes: 143 additions & 32 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ const (
// tier, so we calculate our bonus/revoked tier from the score in the
// ConnectResult.
defaultPenaltyThreshold = 20

// seedLen is the length of the generated app seed used for app protection.
seedLen = 64
)

var (
Expand Down Expand Up @@ -3265,15 +3268,24 @@ func (c *Core) ChangeAppPass(appPW, newAppPW []byte) error {
if err != nil {
return newError(authErr, "old password error: %w", err)
}
defer outerCrypter.Close()
innerKey, err := outerCrypter.Decrypt(creds.EncInnerKey)
if err != nil {
return fmt.Errorf("inner key decryption error: %w", err)
}

return c.changeAppPass(newAppPW, innerKey, creds)
}

// changeAppPass is a shared method to reset or change user password.
func (c *Core) changeAppPass(newAppPW, innerKey []byte, creds *db.PrimaryCredentials) error {
newOuterCrypter := c.newCrypter(newAppPW)
defer newOuterCrypter.Close()
newEncInnerKey, err := newOuterCrypter.Encrypt(innerKey)
if err != nil {
return fmt.Errorf("encryption error: %v", err)
}

newCreds := &db.PrimaryCredentials{
EncSeed: creds.EncSeed,
EncInnerKey: newEncInnerKey,
Expand All @@ -3291,6 +3303,35 @@ func (c *Core) ChangeAppPass(appPW, newAppPW []byte) error {
return nil
}

// ResetAppPass resets the application password to the provided new password.
func (c *Core) ResetAppPass(newPass []byte, seed []byte) error {
if !c.IsInitialized() {
return fmt.Errorf("cannot reset password before client is initialized")
}

if len(newPass) == 0 {
return fmt.Errorf("application password cannot be empty")
}

if len(seed) != seedLen {
return fmt.Errorf("invalid seed length %d", len(seed))
}

creds := c.creds()
if creds == nil {
return fmt.Errorf("no credentials stored")
}

innerKey := seedInnerKey(seed)
_, err := c.reCrypter(innerKey[:], creds.InnerKeyParams)
if err != nil {
c.log.Errorf("Error reseting password with seed: %v", err)
return errors.New("incorrect seed")
}

return c.changeAppPass(newPass, innerKey[:], creds)
}

// ReconfigureWallet updates the wallet configuration settings, it also updates
// the password if newWalletPW is non-nil. Do not make concurrent calls to
// ReconfigureWallet for the same asset.
Expand Down Expand Up @@ -4535,14 +4576,12 @@ func (c *Core) InitializeClient(pw, restorationSeed []byte) error {
return fmt.Errorf("SetSeedGenerationTime error: %w", err)
}
c.seedGenerationTime = now
}

c.setCredentials(creds)

if len(restorationSeed) == 0 {
subject, details := c.formatDetails(TopicSeedNeedsSaving)
c.notify(newSecurityNote(TopicSeedNeedsSaving, subject, details, db.Success))
}

c.setCredentials(creds)
return nil
}

Expand Down Expand Up @@ -4575,41 +4614,60 @@ func (c *Core) generateCredentials(pw, seed []byte) (encrypt.Crypter, *db.Primar
return nil, nil, fmt.Errorf("empty password not allowed")
}

// Generate an inner key and it's Crypter.
innerKey := encode.RandomBytes(32)
innerCrypter := c.newCrypter(innerKey)

// Generate the outer key.
outerCrypter := c.newCrypter(pw)
encInnerKey, err := outerCrypter.Encrypt(innerKey)
if err != nil {
return nil, nil, fmt.Errorf("inner key encryption error: %w", err)
}

// Generate a seed to use as the root for all future key generation.
const seedLen = 64
if len(seed) == 0 {
seed = encode.RandomBytes(seedLen)
} else if len(seed) != seedLen {
return nil, nil, fmt.Errorf("invalid seed length %d. expected %d", len(seed), seedLen)
}
defer encode.ClearBytes(seed)

// Generate an inner key and it's Crypter.
innerKey := seedInnerKey(seed)
innerCrypter := c.newCrypter(innerKey[:])
encSeed, err := innerCrypter.Encrypt(seed)
if err != nil {
return nil, nil, fmt.Errorf("client seed encryption error: %w", err)
}

// Generate the outer key.
outerCrypter := c.newCrypter(pw)
encInnerKey, err := outerCrypter.Encrypt(innerKey[:])
if err != nil {
return nil, nil, fmt.Errorf("inner key encryption error: %w", err)
}

creds := &db.PrimaryCredentials{
EncSeed: encSeed,
EncInnerKey: encInnerKey,
InnerKeyParams: innerCrypter.Serialize(),
OuterKeyParams: outerCrypter.Serialize(),
Version: 1,
}

return innerCrypter, creds, nil
}

func seedInnerKey(seed []byte) []byte {
// keyParam is a domain-specific value to ensure the resulting key is unique
// for the specific use case of deriving an inner encryption key from the
// seed. Any other uses of derivation from the seed should similarly create
// their own domain-specific value to ensure uniqueness.
//
// It is equal to BLAKE-256([]byte("DCRDEX-InnerKey-v0")).
keyParam := [32]byte{
0x75, 0x25, 0xb1, 0xb6, 0x53, 0x33, 0x9e, 0x33,
0xbe, 0x11, 0x61, 0x45, 0x1a, 0x88, 0x6f, 0x37,
0xe7, 0x74, 0xdf, 0xca, 0xb4, 0x8a, 0xee, 0x0e,
0x7c, 0x84, 0x60, 0x01, 0xed, 0xe5, 0xf6, 0x97,
}
key := make([]byte, len(seed)+len(keyParam))
copy(key, seed)
copy(key[len(seed):], keyParam[:])
innerKey := blake256.Sum256(key)
return innerKey[:]
}

func (c *Core) bondKeysReady() bool {
c.loginMtx.Lock()
defer c.loginMtx.Unlock()
Expand Down Expand Up @@ -4649,6 +4707,13 @@ func (c *Core) Login(pw []byte) error {
}
defer crypter.Close()

switch creds.Version {
case 0:
if crypter, creds, err = c.upgradeV0CredsToV1(pw, *creds); err != nil {
return fmt.Errorf("error upgrading primary credentials from version 0 to 1: %w", err)
}
}

c.loginMtx.Lock()
defer c.loginMtx.Unlock()

Expand Down Expand Up @@ -4681,6 +4746,45 @@ func (c *Core) Login(pw []byte) error {
return nil
}

// upgradeV0CredsToV1 upgrades version 0 credentials to version 1. This update
// changes the inner key to be derived from the seed.
func (c *Core) upgradeV0CredsToV1(appPW []byte, creds db.PrimaryCredentials) (encrypt.Crypter, *db.PrimaryCredentials, error) {
outerCrypter, err := c.reCrypter(appPW, creds.OuterKeyParams)
if err != nil {
return nil, nil, fmt.Errorf("app password error: %w", err)
}
innerKey, err := outerCrypter.Decrypt(creds.EncInnerKey)
if err != nil {
return nil, nil, fmt.Errorf("inner key decryption error: %w", err)
}
innerCrypter, err := c.reCrypter(innerKey, creds.InnerKeyParams)
if err != nil {
return nil, nil, fmt.Errorf("inner key deserialization error: %w", err)
}
seed, err := innerCrypter.Decrypt(creds.EncSeed)
if err != nil {
return nil, nil, fmt.Errorf("app seed decryption error: %w", err)
}

// Update all the fields.
newInnerKey := seedInnerKey(seed)
newInnerCrypter := c.newCrypter(newInnerKey[:])
creds.Version = 1
creds.InnerKeyParams = newInnerCrypter.Serialize()
if creds.EncSeed, err = newInnerCrypter.Encrypt(seed); err != nil {
return nil, nil, fmt.Errorf("error encrypting version 1 seed: %w", err)
}
if creds.EncInnerKey, err = outerCrypter.Encrypt(newInnerKey[:]); err != nil {
return nil, nil, fmt.Errorf("error encrypting version 1 inner key: %w", err)
}
if err := c.recrypt(&creds, innerCrypter, newInnerCrypter); err != nil {
return nil, nil, fmt.Errorf("recrypt error during v0 -> v1 credentials upgrade: %w", err)
}

c.log.Infof("Upgraded to version 1 credentials")
return newInnerCrypter, &creds, nil
}

// connectWallets attempts to connect to and retrieve balance from all known
// wallets. This should be done only ONCE on Login.
func (c *Core) connectWallets() {
Expand Down Expand Up @@ -4747,29 +4851,14 @@ func (c *Core) Notifications(n int) ([]*db.Notification, error) {
return notes, nil
}

// initializePrimaryCredentials sets the PrimaryCredential fields after the DB
// upgrade.
func (c *Core) initializePrimaryCredentials(pw []byte, oldKeyParams []byte) error {
oldCrypter, err := c.reCrypter(pw, oldKeyParams)
if err != nil {
return fmt.Errorf("legacy encryption key deserialization error: %w", err)
}

newCrypter, creds, err := c.generateCredentials(pw, nil)
if err != nil {
return err
}

func (c *Core) recrypt(creds *db.PrimaryCredentials, oldCrypter, newCrypter encrypt.Crypter) error {
walletUpdates, acctUpdates, err := c.db.Recrypt(creds, oldCrypter, newCrypter)
if err != nil {
return err
}

c.setCredentials(creds)

subject, details := c.formatDetails(TopicUpgradedToSeed)
c.notify(newSecurityNote(TopicUpgradedToSeed, subject, details, db.WarningLevel))

for assetID, newEncPW := range walletUpdates {
w, found := c.wallet(assetID)
if !found {
Expand All @@ -4794,6 +4883,28 @@ func (c *Core) initializePrimaryCredentials(pw []byte, oldKeyParams []byte) erro
return nil
}

// initializePrimaryCredentials sets the PrimaryCredential fields after the DB
// upgrade.
func (c *Core) initializePrimaryCredentials(pw []byte, oldKeyParams []byte) error {
oldCrypter, err := c.reCrypter(pw, oldKeyParams)
if err != nil {
return fmt.Errorf("legacy encryption key deserialization error: %w", err)
}

newCrypter, creds, err := c.generateCredentials(pw, nil)
if err != nil {
return err
}

if err := c.recrypt(creds, oldCrypter, newCrypter); err != nil {
return err
}

subject, details := c.formatDetails(TopicUpgradedToSeed)
c.notify(newSecurityNote(TopicUpgradedToSeed, subject, details, db.WarningLevel))
return nil
}

// Active indicates if there are any active orders across all configured
// accounts. This includes booked orders and trades that are settling.
func (c *Core) Active() bool {
Expand Down
39 changes: 39 additions & 0 deletions client/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7263,6 +7263,45 @@ func TestChangeAppPass(t *testing.T) {
}
}

func TestResetAppPass(t *testing.T) {
rig := newTestRig()
defer rig.shutdown()
crypter := newTCrypterSmart()
rig.crypter = crypter
rig.core.newCrypter = func([]byte) encrypt.Crypter { return crypter }
rig.core.reCrypter = func([]byte, []byte) (encrypt.Crypter, error) { return rig.crypter, crypter.recryptErr }

tCore := rig.core
seed, err := tCore.ExportSeed(tPW)
if err != nil {
t.Fatalf("seed export failed: %v", err)
}

// Invalid seed error
invalidSeed := seed[:24]
err = tCore.ResetAppPass(tPW, invalidSeed)
if !strings.Contains(err.Error(), "invalid seed length") {
t.Fatalf("wrong error for invalid seed length: %v", err)
}

// Want incorrect seed error.
rig.crypter.(*tCrypterSmart).recryptErr = tErr
// tCrypter is used to encode the orginal seed but we don't need it here, so
// we need to add 8 bytes to commplete the expected seed lenght(64).
seed = append(seed, randBytes(8)...)
err = tCore.ResetAppPass(tPW, seed)
if err.Error() != "incorrect seed" {
t.Fatalf("wrong error for incorrect seed: %v", err)
}

// ok, no crypter error.
rig.crypter.(*tCrypterSmart).recryptErr = nil
err = tCore.ResetAppPass(tPW, seed)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
ukane-philemon marked this conversation as resolved.
Show resolved Hide resolved

func TestReconfigureWallet(t *testing.T) {
rig := newTestRig()
defer rig.shutdown()
Expand Down
Loading
Loading