Skip to content

Commit 185e2db

Browse files
committed
Change Password flow
1 parent b95dc9b commit 185e2db

File tree

8 files changed

+427
-59
lines changed

8 files changed

+427
-59
lines changed

src/background/Wallet/Wallet.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,22 @@ export class Wallet {
294294
await this.syncWithWalletStore();
295295
}
296296

297+
async assignNewCredentials({
298+
params: { credentials, newCredentials },
299+
}: PublicMethodParams<{
300+
credentials: SessionCredentials;
301+
newCredentials: SessionCredentials;
302+
}>) {
303+
this.ensureRecord(this.record);
304+
this.record = await Model.reEncryptRecord(this.record, {
305+
credentials,
306+
newCredentials,
307+
});
308+
this.userCredentials = newCredentials;
309+
await this.updateWalletStore(this.record);
310+
this.setExpirationForSeedPhraseEncryptionKey(1000 * 120);
311+
}
312+
297313
async resetCredentials() {
298314
this.userCredentials = null;
299315
}

src/background/Wallet/WalletRecord.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { decrypt, encrypt } from 'src/modules/crypto';
22
import type { Draft } from 'immer';
3-
import { produce } from 'immer';
3+
import { createDraft, finishDraft, produce } from 'immer';
44
import { nanoid } from 'nanoid';
55
import sortBy from 'lodash/sortBy';
66
import { toChecksumAddress } from 'src/modules/ethereum/toChecksumAddress';
@@ -492,7 +492,27 @@ export class WalletRecordModel {
492492
})
493493
);
494494

495-
return WalletRecordModel.verifyStateIntegrity(entry as WalletRecord);
495+
return WalletRecordModel.verifyStateIntegrity(entry);
496+
}
497+
498+
static async reEncryptRecord(
499+
record: WalletRecord,
500+
{
501+
credentials,
502+
newCredentials,
503+
}: { credentials: SessionCredentials; newCredentials: SessionCredentials }
504+
) {
505+
// Async update flow for Immer: https://immerjs.github.io/immer/async/
506+
const draft = createDraft(record);
507+
for (const group of draft.walletManager.groups) {
508+
if (isMnemonicContainer(group.walletContainer)) {
509+
await group.walletContainer.reEncryptWallets({
510+
credentials,
511+
newCredentials,
512+
});
513+
}
514+
}
515+
return finishDraft(draft);
496516
}
497517

498518
static async getRecoveryPhrase(

src/background/Wallet/model/WalletContainer.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,25 @@ export class MnemonicWalletContainer extends WalletContainerImpl {
227227
}
228228
this.wallets.push(wallet);
229229
}
230+
231+
async reEncryptWallets({
232+
credentials,
233+
newCredentials,
234+
}: {
235+
credentials: SessionCredentials;
236+
newCredentials: SessionCredentials;
237+
}) {
238+
const { mnemonic: encryptedMnemonic } = this.getFirstWallet();
239+
invariant(encryptedMnemonic, 'Must be a Mnemonic WalletContainer');
240+
const phrase = await decryptMnemonic(encryptedMnemonic.phrase, credentials);
241+
const { seedPhraseEncryptionKey } = newCredentials;
242+
const updatedPhrase = await encrypt(seedPhraseEncryptionKey, phrase);
243+
for (const wallet of this.wallets) {
244+
if (wallet.mnemonic) {
245+
wallet.mnemonic.phrase = updatedPhrase;
246+
}
247+
}
248+
}
230249
}
231250

232251
export class PrivateKeyWalletContainer extends WalletContainerImpl {

src/background/account/Account.ts

Lines changed: 115 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,52 @@ import { eraseAndUpdateToLatestVersion } from 'src/shared/core/version/shared';
1313
import { currentUserKey } from 'src/shared/getCurrentUser';
1414
import type { PublicUser, User } from 'src/shared/types/User';
1515
import { payloadId } from '@walletconnect/jsonrpc-utils';
16+
import { invariant } from 'src/shared/invariant';
1617
import { Wallet } from '../Wallet/Wallet';
1718
import { peakSavedWalletState } from '../Wallet/persistence';
1819
import type { NotificationWindow } from '../NotificationWindow/NotificationWindow';
1920
import { globalPreferences } from '../Wallet/GlobalPreferences';
2021
import { credentialsKey } from './storage-keys';
22+
import { isSessionCredentials } from './Credentials';
2123

2224
const TEMPORARY_ID = 'temporary';
2325

2426
async function sha256({ password, salt }: { password: string; salt: string }) {
2527
return await getSHA256HexDigest(`${salt}:${password}`);
2628
}
2729

30+
async function deriveUserKeys({
31+
user,
32+
credentials,
33+
}: {
34+
user: User;
35+
credentials: { password: string } | { encryptionKey: string };
36+
}) {
37+
let encryptionKey: string | null = null;
38+
let seedPhraseEncryptionKey: string | null = null;
39+
let seedPhraseEncryptionKey_deprecated: CryptoKey | null = null;
40+
if ('password' in credentials) {
41+
const { password } = credentials;
42+
const [key1, key2, key3] = await Promise.all([
43+
sha256({ salt: user.id, password }),
44+
sha256({ salt: user.salt, password }),
45+
createCryptoKey(password, user.salt),
46+
]);
47+
encryptionKey = key1;
48+
seedPhraseEncryptionKey = key2;
49+
seedPhraseEncryptionKey_deprecated = key3;
50+
} else {
51+
encryptionKey = credentials.encryptionKey;
52+
}
53+
54+
return {
55+
id: user.id,
56+
encryptionKey,
57+
seedPhraseEncryptionKey,
58+
seedPhraseEncryptionKey_deprecated,
59+
};
60+
}
61+
2862
class EventEmitter<Events extends EventsMap> {
2963
private emitter = createNanoEvents<Events>();
3064

@@ -133,17 +167,27 @@ export class Account extends EventEmitter<AccountEvents> {
133167
}
134168
}
135169

136-
static async createUser(password: string): Promise<User> {
170+
static validatePassword(password: string) {
137171
const validity = validate({ password });
138172
if (!validity.valid) {
139173
throw new Error(validity.message);
140174
}
175+
}
176+
177+
static async createUser(password: string): Promise<User> {
178+
Account.validatePassword(password);
141179
const id = nanoid(36); // use longer id than default (21)
142180
const salt = createSalt(); // used to encrypt seed phrases
143181
const record = { id, salt /* passwordHash: hash */ };
144182
return record;
145183
}
146184

185+
/** Updates salt */
186+
static async updateUser(user: User): Promise<User> {
187+
const salt = createSalt(); // used to encrypt seed phrases
188+
return { id: user.id, salt };
189+
}
190+
147191
constructor({
148192
notificationWindow,
149193
}: {
@@ -156,6 +200,7 @@ export class Account extends EventEmitter<AccountEvents> {
156200
this.notificationWindow = notificationWindow;
157201
this.wallet = new Wallet(TEMPORARY_ID, null, this.notificationWindow);
158202
this.on('authenticated', () => {
203+
// TODO: Call Account.writeCurrentUser() here, too?
159204
if (this.encryptionKey) {
160205
Account.writeCredentials({ encryptionKey: this.encryptionKey });
161206
}
@@ -223,39 +268,59 @@ export class Account extends EventEmitter<AccountEvents> {
223268
return Boolean(user);
224269
}
225270

271+
async changePassword({
272+
currentPassword,
273+
newPassword,
274+
user: currentUser,
275+
}: {
276+
user: User;
277+
currentPassword: string;
278+
newPassword: string;
279+
}) {
280+
Account.validatePassword(newPassword);
281+
await this.login(currentUser, currentPassword);
282+
invariant(this.user, 'User must be set');
283+
const updatedUser = await Account.updateUser(this.user);
284+
const currentCredentials = await deriveUserKeys({
285+
user: currentUser,
286+
credentials: { password: currentPassword },
287+
});
288+
const newCredentials = await deriveUserKeys({
289+
user: updatedUser,
290+
credentials: { password: newPassword },
291+
});
292+
if (
293+
!isSessionCredentials(currentCredentials) ||
294+
!isSessionCredentials(newCredentials)
295+
) {
296+
throw new Error('Full credentials are expected');
297+
}
298+
await this.wallet.assignNewCredentials({
299+
id: payloadId(),
300+
params: { newCredentials, credentials: currentCredentials },
301+
});
302+
// Update local state only if the above call was successful
303+
this.user = updatedUser;
304+
this.encryptionKey = newCredentials.encryptionKey;
305+
await Account.writeCurrentUser(this.user);
306+
this.emit('authenticated');
307+
}
308+
226309
async setUser(
227310
user: User,
228-
credentials: { password: string } | { encryptionKey: string },
311+
partialCredentials: { password: string } | { encryptionKey: string },
229312
{ isNewUser = false } = {}
230313
) {
231314
this.user = user;
232315
this.isPendingNewUser = isNewUser;
233-
let seedPhraseEncryptionKey: string | null = null;
234-
let seedPhraseEncryptionKey_deprecated: CryptoKey | null = null;
235-
if ('password' in credentials) {
236-
const { password } = credentials;
237-
const [key1, key2, key3] = await Promise.all([
238-
sha256({ salt: user.id, password }),
239-
sha256({ salt: user.salt, password }),
240-
createCryptoKey(password, user.salt),
241-
]);
242-
this.encryptionKey = key1;
243-
seedPhraseEncryptionKey = key2;
244-
seedPhraseEncryptionKey_deprecated = key3;
245-
} else {
246-
this.encryptionKey = credentials.encryptionKey;
247-
}
316+
const credentials = await deriveUserKeys({
317+
user,
318+
credentials: partialCredentials,
319+
});
320+
this.encryptionKey = credentials.encryptionKey;
248321
await this.wallet.updateCredentials({
249322
id: payloadId(),
250-
params: {
251-
credentials: {
252-
id: user.id,
253-
encryptionKey: this.encryptionKey,
254-
seedPhraseEncryptionKey,
255-
seedPhraseEncryptionKey_deprecated,
256-
},
257-
isNewUser,
258-
},
323+
params: { credentials, isNewUser },
259324
});
260325
if (!this.isPendingNewUser) {
261326
this.emit('authenticated');
@@ -343,16 +408,21 @@ export class AccountPublicRPC {
343408
return null;
344409
}
345410

411+
async verifyUser(user: PublicUser) {
412+
const currentUser = await Account.readCurrentUser();
413+
if (!currentUser || currentUser.id !== user.id) {
414+
throw new Error(`User ${user.id} not found`);
415+
}
416+
return currentUser;
417+
}
418+
346419
async login({
347420
params: { user, password },
348421
}: PublicMethodParams<{
349422
user: PublicUser;
350423
password: string;
351424
}>): Promise<PublicUser | null> {
352-
const currentUser = await Account.readCurrentUser();
353-
if (!currentUser || currentUser.id !== user.id) {
354-
throw new Error(`User ${user.id} not found`);
355-
}
425+
const currentUser = await this.verifyUser(user);
356426
const canAuthorize = await this.account.verifyPassword(
357427
currentUser,
358428
password
@@ -365,6 +435,21 @@ export class AccountPublicRPC {
365435
}
366436
}
367437

438+
async changePassword({
439+
params: { user, currentPassword, newPassword },
440+
}: PublicMethodParams<{
441+
user: PublicUser;
442+
currentPassword: string;
443+
newPassword: string;
444+
}>) {
445+
const currentUser = await this.verifyUser(user);
446+
await this.account.changePassword({
447+
user: currentUser,
448+
currentPassword,
449+
newPassword,
450+
});
451+
}
452+
368453
async hasActivePasswordSession() {
369454
return this.account.hasActivePasswordSession();
370455
}

0 commit comments

Comments
 (0)