@@ -13,18 +13,52 @@ import { eraseAndUpdateToLatestVersion } from 'src/shared/core/version/shared';
1313import { currentUserKey } from 'src/shared/getCurrentUser' ;
1414import type { PublicUser , User } from 'src/shared/types/User' ;
1515import { payloadId } from '@walletconnect/jsonrpc-utils' ;
16+ import { invariant } from 'src/shared/invariant' ;
1617import { Wallet } from '../Wallet/Wallet' ;
1718import { peakSavedWalletState } from '../Wallet/persistence' ;
1819import type { NotificationWindow } from '../NotificationWindow/NotificationWindow' ;
1920import { globalPreferences } from '../Wallet/GlobalPreferences' ;
2021import { credentialsKey } from './storage-keys' ;
22+ import { isSessionCredentials } from './Credentials' ;
2123
2224const TEMPORARY_ID = 'temporary' ;
2325
2426async 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+
2862class 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