Skip to content

Commit b6e3d67

Browse files
committed
Change Password flow
1 parent d21aa76 commit b6e3d67

File tree

8 files changed

+432
-59
lines changed

8 files changed

+432
-59
lines changed

src/background/Wallet/Wallet.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,22 @@ export class Wallet {
315315
await this.syncWithWalletStore();
316316
}
317317

318+
async assignNewCredentials({
319+
params: { credentials, newCredentials },
320+
}: PublicMethodParams<{
321+
credentials: SessionCredentials;
322+
newCredentials: SessionCredentials;
323+
}>) {
324+
this.ensureRecord(this.record);
325+
this.record = await Model.reEncryptRecord(this.record, {
326+
credentials,
327+
newCredentials,
328+
});
329+
this.userCredentials = newCredentials;
330+
await this.updateWalletStore(this.record);
331+
this.setExpirationForSeedPhraseEncryptionKey(1000 * 120);
332+
}
333+
318334
async resetCredentials() {
319335
this.userCredentials = null;
320336
}

src/background/Wallet/WalletRecord.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { decrypt, encrypt } from 'src/modules/crypto';
2-
import { produce } from 'immer';
2+
import { createDraft, finishDraft, produce } from 'immer';
33
import { nanoid } from 'nanoid';
44
import sortBy from 'lodash/sortBy';
55
import { toChecksumAddress } from 'src/modules/ethereum/toChecksumAddress';
@@ -477,7 +477,27 @@ export class WalletRecordModel {
477477
})
478478
);
479479

480-
return WalletRecordModel.verifyStateIntegrity(entry as WalletRecord);
480+
return WalletRecordModel.verifyStateIntegrity(entry);
481+
}
482+
483+
static async reEncryptRecord(
484+
record: WalletRecord,
485+
{
486+
credentials,
487+
newCredentials,
488+
}: { credentials: SessionCredentials; newCredentials: SessionCredentials }
489+
) {
490+
// Async update flow for Immer: https://immerjs.github.io/immer/async/
491+
const draft = createDraft(record);
492+
for (const group of draft.walletManager.groups) {
493+
if (isMnemonicContainer(group.walletContainer)) {
494+
await group.walletContainer.reEncryptWallets({
495+
credentials,
496+
newCredentials,
497+
});
498+
}
499+
}
500+
return finishDraft(draft);
481501
}
482502

483503
static async getRecoveryPhrase(

src/background/Wallet/model/WalletContainer.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,25 @@ export class MnemonicWalletContainer extends WalletContainerImpl {
210210
}
211211
this.wallets.push(wallet);
212212
}
213+
214+
async reEncryptWallets({
215+
credentials,
216+
newCredentials,
217+
}: {
218+
credentials: SessionCredentials;
219+
newCredentials: SessionCredentials;
220+
}) {
221+
const { mnemonic: encryptedMnemonic } = this.getFirstWallet();
222+
invariant(encryptedMnemonic, 'Must be a Mnemonic WalletContainer');
223+
const phrase = await decryptMnemonic(encryptedMnemonic.phrase, credentials);
224+
const { seedPhraseEncryptionKey } = newCredentials;
225+
const updatedPhrase = await encrypt(seedPhraseEncryptionKey, phrase);
226+
for (const wallet of this.wallets) {
227+
if (wallet.mnemonic) {
228+
wallet.mnemonic.phrase = updatedPhrase;
229+
}
230+
}
231+
}
213232
}
214233

215234
export class PrivateKeyWalletContainer extends WalletContainerImpl {

src/background/account/Account.ts

Lines changed: 120 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,51 @@ import { eraseAndUpdateToLatestVersion } from 'src/shared/core/version';
99
import { currentUserKey } from 'src/shared/getCurrentUser';
1010
import type { PublicUser, User } from 'src/shared/types/User';
1111
import { payloadId } from '@walletconnect/jsonrpc-utils';
12+
import { invariant } from 'src/shared/invariant';
1213
import { Wallet } from '../Wallet/Wallet';
1314
import { peakSavedWalletState } from '../Wallet/persistence';
1415
import type { NotificationWindow } from '../NotificationWindow/NotificationWindow';
1516
import { credentialsKey } from './storage-keys';
17+
import { isSessionCredentials } from './Credentials';
1618

1719
const TEMPORARY_ID = 'temporary';
1820

1921
async function sha256({ password, salt }: { password: string; salt: string }) {
2022
return await getSHA256HexDigest(`${salt}:${password}`);
2123
}
2224

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

@@ -77,17 +111,27 @@ export class Account extends EventEmitter<AccountEvents> {
77111
}
78112
}
79113

80-
static async createUser(password: string): Promise<User> {
114+
static validatePassword(password: string) {
81115
const validity = validate({ password });
82116
if (!validity.valid) {
83117
throw new Error(validity.message);
84118
}
119+
}
120+
121+
static async createUser(password: string): Promise<User> {
122+
Account.validatePassword(password);
85123
const id = nanoid(36); // use longer id than default (21)
86124
const salt = createSalt(); // used to encrypt seed phrases
87125
const record = { id, salt /* passwordHash: hash */ };
88126
return record;
89127
}
90128

129+
/** Updates salt */
130+
static async updateUser(user: User): Promise<User> {
131+
const salt = createSalt(); // used to encrypt seed phrases
132+
return { id: user.id, salt };
133+
}
134+
91135
constructor({
92136
notificationWindow,
93137
}: {
@@ -100,6 +144,7 @@ export class Account extends EventEmitter<AccountEvents> {
100144
this.notificationWindow = notificationWindow;
101145
this.wallet = new Wallet(TEMPORARY_ID, null, this.notificationWindow);
102146
this.on('authenticated', () => {
147+
// TODO: Call Account.writeCurrentUser() here, too?
103148
if (this.encryptionKey) {
104149
Account.writeCredentials({ encryptionKey: this.encryptionKey });
105150
}
@@ -152,39 +197,60 @@ export class Account extends EventEmitter<AccountEvents> {
152197
await this.setUser(user, { password }, { isNewUser: false });
153198
}
154199

200+
async changePassword({
201+
currentPassword,
202+
newPassword,
203+
user: currentUser,
204+
}: {
205+
user: User;
206+
currentPassword: string;
207+
newPassword: string;
208+
}) {
209+
Account.validatePassword(newPassword);
210+
await this.login(currentUser, currentPassword);
211+
invariant(this.user, 'User must be set');
212+
const updatedUser = await Account.updateUser(this.user);
213+
const currentCredentials = await deriveUserKeys({
214+
user: currentUser,
215+
credentials: { password: currentPassword },
216+
});
217+
const newCredentials = await deriveUserKeys({
218+
user: updatedUser,
219+
credentials: { password: newPassword },
220+
});
221+
console.log({ currentCredentials, newCredentials });
222+
if (
223+
!isSessionCredentials(currentCredentials) ||
224+
!isSessionCredentials(newCredentials)
225+
) {
226+
throw new Error('Full credentials are expected');
227+
}
228+
await this.wallet.assignNewCredentials({
229+
id: payloadId(),
230+
params: { newCredentials, credentials: currentCredentials },
231+
});
232+
// Update local state only if the above call was successful
233+
this.user = updatedUser;
234+
this.encryptionKey = newCredentials.encryptionKey;
235+
await Account.writeCurrentUser(this.user);
236+
this.emit('authenticated');
237+
}
238+
155239
async setUser(
156240
user: User,
157-
credentials: { password: string } | { encryptionKey: string },
241+
partialCredentials: { password: string } | { encryptionKey: string },
158242
{ isNewUser = false } = {}
159243
) {
160244
this.user = user;
161245
this.isPendingNewUser = isNewUser;
162-
let seedPhraseEncryptionKey: string | null = null;
163-
let seedPhraseEncryptionKey_deprecated: CryptoKey | null = null;
164-
if ('password' in credentials) {
165-
const { password } = credentials;
166-
const [key1, key2, key3] = await Promise.all([
167-
sha256({ salt: user.id, password }),
168-
sha256({ salt: user.salt, password }),
169-
createCryptoKey(password, user.salt),
170-
]);
171-
this.encryptionKey = key1;
172-
seedPhraseEncryptionKey = key2;
173-
seedPhraseEncryptionKey_deprecated = key3;
174-
} else {
175-
this.encryptionKey = credentials.encryptionKey;
176-
}
246+
const credentials = await deriveUserKeys({
247+
user,
248+
credentials: partialCredentials,
249+
});
250+
this.encryptionKey = credentials.encryptionKey;
177251
await this.wallet.updateCredentials({
178252
id: payloadId(),
179-
params: {
180-
credentials: {
181-
id: user.id,
182-
encryptionKey: this.encryptionKey,
183-
seedPhraseEncryptionKey,
184-
seedPhraseEncryptionKey_deprecated,
185-
},
186-
isNewUser,
187-
},
253+
params: { credentials, isNewUser },
188254
});
189255
if (!this.isPendingNewUser) {
190256
this.emit('authenticated');
@@ -272,16 +338,25 @@ export class AccountPublicRPC {
272338
return null;
273339
}
274340

341+
async verifyUser(user: PublicUser) {
342+
const currentUser = await Account.readCurrentUser();
343+
if (!currentUser || currentUser.id !== user.id) {
344+
throw new Error(`User ${user.id} not found`);
345+
}
346+
return currentUser;
347+
}
348+
275349
async login({
276350
params: { user, password },
277351
}: PublicMethodParams<{
278352
user: PublicUser;
279353
password: string;
280354
}>): Promise<PublicUser | null> {
281-
const currentUser = await Account.readCurrentUser();
282-
if (!currentUser || currentUser.id !== user.id) {
283-
throw new Error(`User ${user.id} not found`);
284-
}
355+
const currentUser = await this.verifyUser(user);
356+
// const currentUser = await Account.readCurrentUser();
357+
// if (!currentUser || currentUser.id !== user.id) {
358+
// throw new Error(`User ${user.id} not found`);
359+
// }
285360
const canAuthorize = await this.account.verifyPassword(
286361
currentUser,
287362
password
@@ -294,6 +369,21 @@ export class AccountPublicRPC {
294369
}
295370
}
296371

372+
async changePassword({
373+
params: { user, currentPassword, newPassword },
374+
}: PublicMethodParams<{
375+
user: PublicUser;
376+
currentPassword: string;
377+
newPassword: string;
378+
}>) {
379+
const currentUser = await this.verifyUser(user);
380+
await this.account.changePassword({
381+
user: currentUser,
382+
currentPassword,
383+
newPassword,
384+
});
385+
}
386+
297387
async hasActivePasswordSession() {
298388
return this.account.hasActivePasswordSession();
299389
}

0 commit comments

Comments
 (0)