From 4cd2b2eb16392b1142a7ead189d73591e0dbbc05 Mon Sep 17 00:00:00 2001 From: Jackson Dean Date: Thu, 21 Sep 2023 16:32:44 -0700 Subject: [PATCH 1/2] migrate swal to mat dialog --- src/app/app.module.ts | 2 + .../grouped-account-select.component.ts | 63 ++++--------------- .../remove-account-dialog.component.html | 53 ++++++++++++++++ .../remove-account-dialog.component.scss | 0 .../remove-account-dialog.component.spec.ts | 21 +++++++ .../remove-account-dialog.component.ts | 43 +++++++++++++ 6 files changed, 132 insertions(+), 50 deletions(-) create mode 100644 src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.html create mode 100644 src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.scss create mode 100644 src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.spec.ts create mode 100644 src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a5155ca8..23933e92 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -35,6 +35,7 @@ import { GetDesoComponent } from './get-deso/get-deso.component'; import { BackupSeedDialogComponent } from './grouped-account-select/backup-seed-dialog/backup-seed-dialog.component'; import { GroupedAccountSelectComponent } from './grouped-account-select/grouped-account-select.component'; import { RecoverySecretComponent } from './grouped-account-select/recovery-secret/recovery-secret.component'; +import { RemoveAccountDialogComponent } from './grouped-account-select/remove-account-dialog/remove-account-dialog.component'; import { HomeComponent } from './home/home.component'; import { IconsModule } from './icons/icons.module'; import { IdentityService } from './identity.service'; @@ -105,6 +106,7 @@ import { TransactionSpendingLimitComponent } from './transaction-spending-limit/ GroupedAccountSelectComponent, RecoverySecretComponent, BackupSeedDialogComponent, + RemoveAccountDialogComponent, ], imports: [ BrowserModule, diff --git a/src/app/grouped-account-select/grouped-account-select.component.ts b/src/app/grouped-account-select/grouped-account-select.component.ts index 6d2ddb8b..eafcf4ea 100644 --- a/src/app/grouped-account-select/grouped-account-select.component.ts +++ b/src/app/grouped-account-select/grouped-account-select.component.ts @@ -1,6 +1,5 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { escape } from 'lodash'; import { finalize, take } from 'rxjs/operators'; import { LoginMethod, @@ -13,6 +12,7 @@ import { AccountService } from '../account.service'; import { BackendAPIService } from '../backend-api.service'; import { GlobalVarsService } from '../global-vars.service'; import { BackupSeedDialogComponent } from './backup-seed-dialog/backup-seed-dialog.component'; +import { RemoveAccountDialogComponent } from './remove-account-dialog/remove-account-dialog.component'; type AccountViewModel = SubAccountMetadata & UserProfile & { publicKey: string } & { lastUsed?: boolean }; @@ -197,55 +197,18 @@ export class GroupedAccountSelectComponent implements OnInit { const hiddenPreview = group.accounts .slice() .filter((a) => a.accountNumber !== account.accountNumber); - const hasAccountsAfterHiding = hiddenPreview.length > 0; - const { displayName, displayAccountNumber } = { - displayName: escape(this.getAccountDisplayName(account)), - displayAccountNumber: escape(account.accountNumber.toString()), - }; - Swal.fire({ - title: 'Remove Account?', - html: hasAccountsAfterHiding - ? ` -
-

- ${displayName} -

-

You can recover this account as long as you have the account number.

-

- -

-
- ` - : ` -
-

- ${displayName} -

-

- Your account will be irrecoverable if you lose your seed phrase. -

-

Make sure you have backed up your seed phrase before continuing!

-
- `, - showCancelButton: true, - }).then(({ isConfirmed }) => { - if (isConfirmed) { + + const dialogRef = this.dialog.open(RemoveAccountDialogComponent, { + data: { + publicKey: account.publicKey, + accountNumber: account.accountNumber, + username: account.username, + isLastAccountInGroup: hiddenPreview.length === 0, + }, + }); + + dialogRef.afterClosed().subscribe((confirmed) => { + if (confirmed) { this.accountService.updateAccountInfo(account.publicKey, { isHidden: true, }); diff --git a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.html b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.html new file mode 100644 index 00000000..96ea615c --- /dev/null +++ b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.html @@ -0,0 +1,53 @@ +
+
+ +

Remove Account

+
+
+ +

+ Your account will be irrecoverable if you lose your seed phrase. +

+

+ Make sure you have backed up your seed phrase before continuing! +

+
+ +

+ You can recover this account as long as you have the account number. +

+
+ Account number: {{ this.data.accountNumber }} + +
+
+
+
+ + +
+
diff --git a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.scss b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.spec.ts b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.spec.ts new file mode 100644 index 00000000..5b491d60 --- /dev/null +++ b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RemoveAccountDialogComponent } from './remove-account-dialog.component'; + +describe('RemoveAccountDialogComponent', () => { + let component: RemoveAccountDialogComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [RemoveAccountDialogComponent], + }); + fixture = TestBed.createComponent(RemoveAccountDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.ts b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.ts new file mode 100644 index 00000000..6d2046e2 --- /dev/null +++ b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.ts @@ -0,0 +1,43 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { AccountService } from '../../account.service'; + +@Component({ + selector: 'remove-account-dialog', + templateUrl: './remove-account-dialog.component.html', + styleUrls: ['./remove-account-dialog.component.scss'], +}) +export class RemoveAccountDialogComponent { + copySuccess: boolean = false; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) + public data: { + publicKey: string; + accountNumber: number; + username?: string; + isLastAccountInGroup: boolean; + }, + private accountService: AccountService + ) {} + + copyAccountNumber() { + window.navigator.clipboard + .writeText(this.data.accountNumber.toString()) + .then(() => { + this.copySuccess = true; + setTimeout(() => { + this.copySuccess = false; + }, 1500); + }); + } + + cancel() { + this.dialogRef.close(false); + } + + confirm() { + this.dialogRef.close(true); + } +} From c01ab878d6720ce41ef0b2d01c704e2d49025fc8 Mon Sep 17 00:00:00 2001 From: Jackson Dean Date: Mon, 25 Sep 2023 07:24:07 -0700 Subject: [PATCH 2/2] update button styles (#277) * update button styles * review feedback and bug fixes (#279) --- src/app/account.service.ts | 219 ++++++++++-------- src/app/app.component.ts | 4 - src/app/approve/approve.component.ts | 3 +- src/app/auth/google/google.component.ts | 8 +- src/app/backend-api.service.ts | 6 +- src/app/crypto.service.ts | 29 +-- src/app/derive/derive.component.html | 14 +- src/app/derive/derive.component.ts | 5 + src/app/global-vars.service.ts | 6 - .../backup-seed-dialog.component.html | 16 +- .../grouped-account-select.component.html | 64 +++-- .../grouped-account-select.component.ts | 142 ++++++++---- .../recovery-secret.component.html | 10 +- .../recovery-secret.component.ts | 9 +- .../remove-account-dialog.component.html | 4 +- .../remove-account-dialog.component.spec.ts | 21 -- .../remove-account-dialog.component.ts | 2 +- src/app/identity.service.ts | 51 +--- src/app/log-in-seed/log-in-seed.component.ts | 46 ++-- src/app/log-in/log-in.component.html | 14 +- .../sign-up-metamask.component.ts | 3 +- src/app/sign-up/sign-up.component.html | 8 +- src/app/sign-up/sign-up.component.ts | 4 +- src/app/signing.service.ts | 37 +-- src/styles.scss | 9 +- 25 files changed, 367 insertions(+), 367 deletions(-) delete mode 100644 src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.spec.ts diff --git a/src/app/account.service.ts b/src/app/account.service.ts index 0f95d29f..02545134 100644 --- a/src/app/account.service.ts +++ b/src/app/account.service.ts @@ -39,6 +39,29 @@ import { MetamaskService } from './metamask.service'; import { SigningService } from './signing.service'; export const ERROR_USER_NOT_FOUND = 'User not found'; + +/** + * The key used to store the sub-account reverse lookup map in local storage. + * This map is used to look up the account number for a sub-account given the + * public key. Application developers provide the "owner" public key in certain + * scenarios (generating derived keys, for example), and we need to be able to + * look up the account number for that public key in order to generate the + * private key for signing. The structure of the map is: + * + * ```json + * { + * "subAccountPublicKey": { + * "lookupKey": "rootPublicKey", + * "accountNumber": 1 + * } + * } + * ``` + * + * For historical reasons, the "lookupKey" is the root public key, which is the + * sub-account generated for account number 0. This is the "root" account, and + * is used to store the common data for all accounts in a particular account + * group, including its mnemonic and all its sub-account account numbers. + */ const SUB_ACCOUNT_REVERSE_LOOKUP_KEY = 'subAccountReverseLookup'; export interface SubAccountReversLookupEntry { @@ -62,13 +85,35 @@ export class AccountService { private signingService: SigningService, private metamaskService: MetamaskService ) { + /** + * We rebuild the sub-account reverse lookup map on every page load. This is + * to ensure there are no stale or missing entries in the map. The number of + * users in local storage is generally small, so this should not be a + * performance issue. If it does become a performance issue, we can consider + * a more sophisticated approach, but the number of users would need to be + * on the order of hundreds or thousands (very unlikely, and maybe literally + * impossible) before this would be a problem. + */ this.initializeSubAccountReverseLookup(); } // Public Getters - getPublicKeys(): any { - return Object.keys(this.getRootLevelUsers()); + getPublicKeys(): string[] { + const publicKeys: string[] = []; + const rootUsers = this.getRootLevelUsers(); + + Object.keys(rootUsers).forEach((publicKey) => { + publicKeys.push(publicKey); + const subAccounts = rootUsers[publicKey].subAccounts || []; + subAccounts.forEach((subAccount) => { + publicKeys.push( + this.getAccountPublicKeyBase58(publicKey, subAccount.accountNumber) + ); + }); + }); + + return publicKeys; } getAccountInfo(publicKey: string): PrivateUserInfo & SubAccountMetadata { @@ -96,9 +141,16 @@ export class AccountService { ); if (foundAccount) { + const keychain = this.cryptoService.getSubAccountKeychain( + rootUser.seedHex, + foundAccount.accountNumber + ); + const subAccountSeedHex = + this.cryptoService.keychainToSeedHex(keychain); info = { ...rootUser, ...foundAccount, + seedHex: subAccountSeedHex, }; } } @@ -140,12 +192,13 @@ export class AccountService { getEncryptedUsers(): { [key: string]: PublicUserInfo } { const hostname = this.globalVars.hostname; - const privateUsers = this.getRootLevelUsers(); + const rootUsers = this.getRootLevelUsers(); const publicUsers: { [key: string]: PublicUserInfo } = {}; - for (const publicKey of Object.keys(privateUsers)) { - const privateUser = privateUsers[publicKey]; - const accessLevel = this.getAccessLevel(publicKey, hostname); + for (const rootPublicKey of Object.keys(rootUsers)) { + const privateUser = rootUsers[rootPublicKey]; + + const accessLevel = this.getAccessLevel(rootPublicKey, hostname); if (accessLevel === AccessLevel.None) { continue; } @@ -166,19 +219,50 @@ export class AccountService { privateUser.seedHex ); - publicUsers[publicKey] = { + const commonFields = { hasExtraText: privateUser.extraText?.length > 0, btcDepositAddress: privateUser.btcDepositAddress, ethDepositAddress: privateUser.ethDepositAddress, version: privateUser.version, - encryptedSeedHex, network: privateUser.network, loginMethod: privateUser.loginMethod || LoginMethod.DESO, accessLevel, + }; + + publicUsers[rootPublicKey] = { + ...commonFields, + encryptedSeedHex, accessLevelHmac, derivedPublicKeyBase58Check: privateUser.derivedPublicKeyBase58Check, encryptedMessagingKeyRandomness, }; + + // To support sub-accounts for the legacy identity flow, we need to return + // a flat map of all users and their sub-accounts. Each sub-account has a + // unique seed hex that can be used for signing transactions, as well as a + // unique accessLevel hmac. + const subAccounts = privateUser.subAccounts || []; + subAccounts.forEach((subAccount) => { + const subAccountPublicKey = this.getAccountPublicKeyBase58( + rootPublicKey, + subAccount.accountNumber + ); + const accountInfo = this.getAccountInfo(subAccountPublicKey); + const subAccountEncryptedSeedHex = this.cryptoService.encryptSeedHex( + accountInfo.seedHex, + hostname + ); + const subAccountAccessLevelHmac = this.cryptoService.accessLevelHmac( + accessLevel, + accountInfo.seedHex + ); + + publicUsers[subAccountPublicKey] = { + ...commonFields, + encryptedSeedHex: subAccountEncryptedSeedHex, + accessLevelHmac: subAccountAccessLevelHmac, + }; + }); } return publicUsers; @@ -244,12 +328,7 @@ export class AccountService { .encode('array', true); // Derived keys JWT with the same expiration as the derived key. This is needed for some backend endpoints. - derivedJwt = this.signingService.signJWT( - derivedSeedHex, - 0, // NOTE: derived keys are always generated with account number 0. - true, - options - ); + derivedJwt = this.signingService.signJWT(derivedSeedHex, true, options); } else { // If the user has passed in a derived public key, use that instead. // Don't define the derived seed hex (a private key presumably already exists). @@ -261,12 +340,7 @@ export class AccountService { } // Compute the owner-signed JWT with the same expiration as the derived key. This is needed for some backend endpoints. // In case of the metamask log-in, jwt will be signed by a derived key. - jwt = this.signingService.signJWT( - account.seedHex, - account.accountNumber, - isMetamask, - options - ); + jwt = this.signingService.signJWT(account.seedHex, isMetamask, options); // Generate new btc and eth deposit addresses for the derived key. // const btcDepositAddress = this.cryptoService.keychainToBtcAddress(derivedKeychain, network); @@ -362,11 +436,9 @@ export class AccountService { ); } } else { - accessSignature = this.signingService.signHashes( - account.seedHex, - [accessHash], - account.accountNumber - )[0]; + accessSignature = this.signingService.signHashes(account.seedHex, [ + accessHash, + ])[0]; } const { messagingPublicKeyBase58Check, @@ -504,25 +576,16 @@ export class AccountService { mnemonic: string, extraText: string, network: Network, - accountNumber: number, - options: { - google?: boolean; + { + lastLoginTimestamp, + loginMethod = LoginMethod.DESO, + }: { + lastLoginTimestamp?: number; + loginMethod?: LoginMethod; } = {} ): string { - // if the account number is provided, and it is greater than 0, this is a sub account. - if (typeof accountNumber === 'number' && accountNumber > 0) { - // We've already stored the sub account in the root user's subAccounts array, - // so we can just return it's public key directly here. - const seedHex = this.cryptoService.keychainToSeedHex(keychain); - const keyPair = this.cryptoService.seedHexToKeyPair( - seedHex, - accountNumber - ); - return this.cryptoService.publicKeyToDeSoPublicKey(keyPair, network); - } - const seedHex = this.cryptoService.keychainToSeedHex(keychain); - const keyPair = this.cryptoService.seedHexToKeyPair(seedHex, 0); + const keyPair = this.cryptoService.seedHexToKeyPair(seedHex); const btcDepositAddress = this.cryptoService.keychainToBtcAddress( // @ts-ignore TODO: add "identifier" to type definition keychain.identifier, @@ -530,11 +593,6 @@ export class AccountService { ); const ethDepositAddress = this.cryptoService.publicKeyToEthAddress(keyPair); - let loginMethod: LoginMethod = LoginMethod.DESO; - if (options.google) { - loginMethod = LoginMethod.GOOGLE; - } - return this.addPrivateUser({ seedHex, mnemonic, @@ -544,12 +602,12 @@ export class AccountService { network, loginMethod, version: PrivateUserVersion.V2, - lastLoginTimestamp: Date.now(), + ...(lastLoginTimestamp && { lastLoginTimestamp }), }); } addUserWithSeedHex(seedHex: string, network: Network): string { - const keyPair = this.cryptoService.seedHexToKeyPair(seedHex, 0); + const keyPair = this.cryptoService.seedHexToKeyPair(seedHex); const helperKeychain = new HDKey(); helperKeychain.privateKey = Buffer.from(seedHex, 'hex'); // @ts-ignore TODO: add "identifier" to type definition @@ -569,7 +627,6 @@ export class AccountService { network, loginMethod: LoginMethod.DESO, version: PrivateUserVersion.V2, - lastLoginTimestamp: Date.now(), }); } @@ -643,8 +700,7 @@ export class AccountService { if (privateUser.version === PrivateUserVersion.V0) { // Add ethDepositAddress field const keyPair = this.cryptoService.seedHexToKeyPair( - privateUser.seedHex, - 0 + privateUser.seedHex ); privateUser.ethDepositAddress = this.cryptoService.publicKeyToEthAddress(keyPair); @@ -677,10 +733,7 @@ export class AccountService { publicKey: string ): string { const account = this.getAccountInfo(ownerPublicKeyBase58Check); - const privateKey = this.cryptoService.seedHexToKeyPair( - account.seedHex, - account.accountNumber - ); + const privateKey = this.cryptoService.seedHexToKeyPair(account.seedHex); const privateKeyBytes = privateKey.getPrivate().toBuffer(undefined, 32); const publicKeyBytes = this.cryptoService.publicKeyToECBuffer(publicKey); const sharedPx = ecies.derive(privateKeyBytes, publicKeyBytes); @@ -725,11 +778,9 @@ export class AccountService { let messagingKeySignature = ''; if (messagingKeyName === this.globalVars.defaultMessageKeyName) { - messagingKeySignature = this.signingService.signHashes( - account.seedHex, - [messagingKeyHash], - account.accountNumber - )[0]; + messagingKeySignature = this.signingService.signHashes(account.seedHex, [ + messagingKeyHash, + ])[0]; } return { @@ -823,18 +874,9 @@ export class AccountService { senderGroupKeyName: string, recipientPublicKey: string, message: string, - options: { - messagingKeyRandomness?: string; - ownerPublicKeyBase58Check?: string; - } = {} + messagingKeyRandomness?: string ): any { - const { accountNumber = 0 } = options.ownerPublicKeyBase58Check - ? this.getAccountInfo(options.ownerPublicKeyBase58Check) - : {}; - const privateKey = this.cryptoService.seedHexToKeyPair( - seedHex, - accountNumber - ); + const privateKey = this.cryptoService.seedHexToKeyPair(seedHex); const privateKeyBuffer = privateKey.getPrivate().toBuffer(undefined, 32); const publicKeyBuffer = @@ -846,7 +888,7 @@ export class AccountService { privateEncryptionKey = this.getMessagingKeyForSeed( seedHex, senderGroupKeyName, - options.messagingKeyRandomness + messagingKeyRandomness ); } @@ -865,16 +907,9 @@ export class AccountService { // @param encryptedHexes : string[] decryptMessagesLegacy( seedHex: string, - encryptedHexes: any, - options: { ownerPublicKeyBase58Check?: string } = {} + encryptedHexes: any ): { [key: string]: any } { - const { accountNumber = 0 } = options.ownerPublicKeyBase58Check - ? this.getAccountInfo(options.ownerPublicKeyBase58Check) - : {}; - const privateKey = this.cryptoService.seedHexToKeyPair( - seedHex, - accountNumber - ); + const privateKey = this.cryptoService.seedHexToKeyPair(seedHex); const privateKeyBuffer = privateKey.getPrivate().toBuffer(undefined, 32); const decryptedHexes: { [key: string]: any } = {}; @@ -897,21 +932,13 @@ export class AccountService { seedHex: string, encryptedMessages: EncryptedMessage[], messagingGroups: MessagingGroup[], - options: { - messagingKeyRandomness?: string; - ownerPublicKeyBase58Check?: string; - } = {} + messagingKeyRandomness?: string, + ownerPublicKeyBase58Check?: string ): Promise<{ [key: string]: any }> { - const { accountNumber = 0 } = options.ownerPublicKeyBase58Check - ? this.getAccountInfo(options.ownerPublicKeyBase58Check) - : {}; - const privateKey = this.cryptoService.seedHexToKeyPair( - seedHex, - accountNumber - ); + const privateKey = this.cryptoService.seedHexToKeyPair(seedHex); const myPublicKey = - options.ownerPublicKeyBase58Check || + ownerPublicKeyBase58Check || this.cryptoService.privateKeyToDeSoPublicKey( privateKey, this.globalVars.network @@ -1012,7 +1039,7 @@ export class AccountService { this.getMessagingKeyForSeed( seedHex, myMessagingGroupMemberEntry.GroupMemberKeyName, - options.messagingKeyRandomness + messagingKeyRandomness ); privateEncryptionKey = this.signingService .decryptGroupMessagingPrivateKeyToMember( @@ -1035,7 +1062,7 @@ export class AccountService { privateEncryptionKey = this.getMessagingKeyForSeed( seedHex, this.globalVars.defaultMessageKeyName, - options.messagingKeyRandomness + messagingKeyRandomness ); } } catch (e: any) { @@ -1062,7 +1089,7 @@ export class AccountService { addPrivateUser(userInfo: PrivateUserInfo): string { const privateUsers = this.getPrivateUsersRaw(); - const privateKey = this.cryptoService.seedHexToKeyPair(userInfo.seedHex, 0); + const privateKey = this.cryptoService.seedHexToKeyPair(userInfo.seedHex); // Metamask login will be added with the master public key. let publicKey = this.cryptoService.privateKeyToDeSoPublicKey( diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5a11102f..2be3c923 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -73,10 +73,6 @@ export class AppComponent implements OnInit { this.globalVars.authenticatedUsers = authenticatedUsers; } - if (params.get('subAccounts') === 'true') { - this.globalVars.subAccounts = true; - } - // Callback should only be used in mobile applications, where payload is passed through URL parameters. const callback = params.get('callback') || stateParamsFromGoogle.callback; if (callback) { diff --git a/src/app/approve/approve.component.ts b/src/app/approve/approve.component.ts index 9001d3d8..b8ed327d 100644 --- a/src/app/approve/approve.component.ts +++ b/src/app/approve/approve.component.ts @@ -105,8 +105,7 @@ export class ApproveComponent implements OnInit { const signedTransactionHex = this.signingService.signTransaction( account.seedHex, this.transactionHex, - isDerived, - account.accountNumber + isDerived ); this.finishFlow(signedTransactionHex); } diff --git a/src/app/auth/google/google.component.ts b/src/app/auth/google/google.component.ts index 771dbd52..1d2eccdd 100644 --- a/src/app/auth/google/google.component.ts +++ b/src/app/auth/google/google.component.ts @@ -2,7 +2,7 @@ import { Component, NgZone, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Subject } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { GoogleAuthState } from '../../../types/identity'; +import { GoogleAuthState, LoginMethod } from '../../../types/identity'; import { AccountService } from '../../account.service'; import { RouteNames } from '../../app-routing.module'; import { BackendAPIService } from '../../backend-api.service'; @@ -93,9 +93,8 @@ export class GoogleComponent implements OnInit { mnemonic, extraText, network, - 0, { - google: true, + loginMethod: LoginMethod.GOOGLE, } ); } catch (err) { @@ -149,9 +148,8 @@ export class GoogleComponent implements OnInit { mnemonic, extraText, network, - 0, { - google: true, + loginMethod: LoginMethod.GOOGLE, } ); this.loading = false; diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts index ca5ef349..28ebd385 100644 --- a/src/app/backend-api.service.ts +++ b/src/app/backend-api.service.ts @@ -304,11 +304,7 @@ export class BackendAPIService { } const isDerived = this.accountService.isMetamaskAccount(account); - const jwt = this.signingService.signJWT( - account.seedHex, - account.accountNumber, - isDerived - ); + const jwt = this.signingService.signJWT(account.seedHex, isDerived); return this.post(path, { ...body, ...{ JWT: jwt } }); } diff --git a/src/app/crypto.service.ts b/src/app/crypto.service.ts index e1108ed2..07810bc0 100644 --- a/src/app/crypto.service.ts +++ b/src/app/crypto.service.ts @@ -142,49 +142,32 @@ export class CryptoService { nonStandard?: boolean ): HDNode { const seed = bip39.mnemonicToSeedSync(mnemonic, extraText); - return deriveKeys(seed, 0, { + return generateSubAccountKeys(seed, 0, { nonStandard, }); } getSubAccountKeychain(masterSeedHex: string, accountIndex: number): HDNode { const seedBytes = Buffer.from(masterSeedHex, 'hex'); - return deriveKeys(seedBytes, accountIndex); + return generateSubAccountKeys(seedBytes, accountIndex); } keychainToSeedHex(keychain: HDNode): string { return keychain.privateKey.toString('hex'); } - /** - * For a given parent seed hex and account number, return the corresponding private key. Public/private - * key pairs are independent and unique based on a combination of the seed hex and account number. - * @param parentSeedHex This is the seed hex used to generate multiple HD wallets/keys from a single seed. - * @param accountNumber This is the account number used to generate unique keys from the parent seed. - * @returns - */ - seedHexToKeyPair(parentSeedHex: string, accountNumber: number): EC.KeyPair { + seedHexToKeyPair(seedHex: string): EC.KeyPair { const ec = new EC('secp256k1'); - if (accountNumber === 0) { - return ec.keyFromPrivate(parentSeedHex); - } - - const hdKeys = this.getSubAccountKeychain(parentSeedHex, accountNumber); - const seedHex = this.keychainToSeedHex(hdKeys); - return ec.keyFromPrivate(seedHex); } - encryptedSeedHexToPublicKey( - encryptedSeedHex: string, - accountNumber: number - ): string { + encryptedSeedHexToPublicKey(encryptedSeedHex: string): string { const seedHex = this.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); - const privateKey = this.seedHexToKeyPair(seedHex, accountNumber); + const privateKey = this.seedHexToKeyPair(seedHex); return this.privateKeyToDeSoPublicKey(privateKey, this.globalVars.network); } @@ -309,7 +292,7 @@ export class CryptoService { * m / purpose' / coin_type' / account' / change / address_index * See for more details: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account */ -function deriveKeys( +function generateSubAccountKeys( seedBytes: Buffer, accountIndex: number, options?: { nonStandard?: boolean } diff --git a/src/app/derive/derive.component.html b/src/app/derive/derive.component.html index 292a118b..279a0bfa 100644 --- a/src/app/derive/derive.component.html +++ b/src/app/derive/derive.component.html @@ -21,19 +21,7 @@ }} - - - -
- or -
- -
+
Backup DeSo Seed >

-
diff --git a/src/app/grouped-account-select/grouped-account-select.component.html b/src/app/grouped-account-select/grouped-account-select.component.html index 04aa160d..2bd90a92 100644 --- a/src/app/grouped-account-select/grouped-account-select.component.html +++ b/src/app/grouped-account-select/grouped-account-select.component.html @@ -4,7 +4,7 @@ *ngIf="loadingAccounts; else accountsSection" > -
+
Select an account
@@ -17,17 +17,22 @@ >
  • +
    or diff --git a/src/app/grouped-account-select/grouped-account-select.component.ts b/src/app/grouped-account-select/grouped-account-select.component.ts index eafcf4ea..8a9c96bc 100644 --- a/src/app/grouped-account-select/grouped-account-select.component.ts +++ b/src/app/grouped-account-select/grouped-account-select.component.ts @@ -15,7 +15,11 @@ import { BackupSeedDialogComponent } from './backup-seed-dialog/backup-seed-dial import { RemoveAccountDialogComponent } from './remove-account-dialog/remove-account-dialog.component'; type AccountViewModel = SubAccountMetadata & - UserProfile & { publicKey: string } & { lastUsed?: boolean }; + UserProfile & { + rootPublicKey: string; + publicKey: string; + lastUsed?: boolean; + }; function sortAccounts(a: AccountViewModel, b: AccountViewModel) { // sort accounts by last login timestamp DESC, @@ -56,12 +60,7 @@ export class GroupedAccountSelectComponent implements OnInit { */ loadingAccounts: boolean = false; - get hasVisibleAccounts() { - // if any group has at least 1 visible account, return true. - return !!Array.from(this.accountGroups.values()).find( - (group) => group.accounts.length > 0 - ); - } + justAddedPublicKey?: string; constructor( public accountService: AccountService, @@ -76,29 +75,27 @@ export class GroupedAccountSelectComponent implements OnInit { initializeAccountGroups() { this.loadingAccounts = true; - const storedUsers = Object.entries( + const rootUserEntries = Object.entries( this.accountService.getRootLevelUsers() - ).sort( - ( - [kA, { lastLoginTimestamp: timestampA = 0 }], - [kb, { lastLoginTimestamp: timestampB = 0 }] - ) => { - // sort groups by last login timestamp DESC. We don't have balance info here. - return timestampB - timestampA; - } ); const accountGroupsByRootKey = new Map< string, - { publicKey: string; accountNumber: number; lastLoginTimestamp: number }[] + { + rootPublicKey: string; + publicKey: string; + accountNumber: number; + lastLoginTimestamp?: number; + }[] >(); - for (const [rootPublicKey, userInfo] of storedUsers) { + for (const [rootPublicKey, userInfo] of rootUserEntries) { const accounts = !userInfo.isHidden ? [ { + rootPublicKey: rootPublicKey, publicKey: rootPublicKey, accountNumber: 0, - lastLoginTimestamp: userInfo.lastLoginTimestamp ?? 0, + lastLoginTimestamp: userInfo.lastLoginTimestamp, }, ] : []; @@ -116,13 +113,16 @@ export class GroupedAccountSelectComponent implements OnInit { ); accounts.push({ + rootPublicKey: rootPublicKey, publicKey: publicKeyBase58, accountNumber: subAccount.accountNumber, - lastLoginTimestamp: subAccount.lastLoginTimestamp ?? 0, + lastLoginTimestamp: subAccount.lastLoginTimestamp, }); } - accountGroupsByRootKey.set(rootPublicKey, accounts); + if (accounts.length > 0) { + accountGroupsByRootKey.set(rootPublicKey, accounts); + } } const profileKeysToFetch = Array.from(accountGroupsByRootKey.values()) @@ -137,27 +137,48 @@ export class GroupedAccountSelectComponent implements OnInit { finalize(() => (this.loadingAccounts = false)) ) .subscribe((users) => { + const unorderedAccountGroups: typeof this.accountGroups = new Map(); Array.from(accountGroupsByRootKey.entries()).forEach( ([key, accounts]) => { - this.accountGroups.set(key, { - showRecoverSubAccountInput: false, - accounts: accounts - .map((account, j) => ({ - ...account, - ...users[account.publicKey], - })) - .sort(sortAccounts), + unorderedAccountGroups.set(key, { + accounts: accounts.map((account) => ({ + ...account, + ...users[account.publicKey], + })), }); - - // get the first account in the list and set it as the lastUsed. - const firstKey = this.accountGroups.keys().next().value; - const firstGroup = this.accountGroups.get(firstKey); - if (firstGroup) { - firstGroup.accounts[0].lastUsed = true; - this.accountGroups.set(firstKey, firstGroup); - } } ); + + // To sort the accounts holistically across groups, we need to flatten + // the Map values into a single array. Once they're sorted, we can determine + // which account was last used and mark it as such. There can be a case where + // no account is "last used" if the user has never logged in to any account and + // simply loaded or added accounts to the wallet. In this case, we don't mark + // any account as "last used". + const allAccounts = Array.from(unorderedAccountGroups.values()) + .map((a) => a.accounts) + .flat(); + const sortedAccounts = allAccounts.sort(sortAccounts); + const lastUsedAccount = sortedAccounts.find( + (a) => + typeof a.lastLoginTimestamp === 'number' && a.lastLoginTimestamp > 0 + ); + + if (lastUsedAccount) { + lastUsedAccount.lastUsed = true; + } + + sortedAccounts.forEach((account) => { + const group = this.accountGroups.get(account.rootPublicKey); + if (group?.accounts?.length) { + group.accounts.push(account); + } else { + this.accountGroups.set(account.rootPublicKey, { + showRecoverSubAccountInput: false, + accounts: [account], + }); + } + }); }); } @@ -214,6 +235,23 @@ export class GroupedAccountSelectComponent implements OnInit { }); group.accounts = hiddenPreview; this.accountGroups.set(groupKey, group); + + // if removing the last used account, select the next last used account + // in the list, if one exists. + if (account.lastUsed) { + const allAccounts = Array.from(this.accountGroups.values()) + .map((a) => a.accounts) + .flat(); + const sortedAccounts = allAccounts.sort(sortAccounts); + const lastUsedAccount = sortedAccounts.find( + (a) => + typeof a.lastLoginTimestamp === 'number' && + a.lastLoginTimestamp > 0 + ); + if (lastUsedAccount) { + lastUsedAccount.lastUsed = true; + } + } } }); } @@ -237,6 +275,7 @@ export class GroupedAccountSelectComponent implements OnInit { .pipe(take(1)) .subscribe((users) => { const account = { + rootPublicKey: rootPublicKey, publicKey: publicKeyBase58, accountNumber: addedAccountNumber, ...users[publicKeyBase58], @@ -248,12 +287,29 @@ export class GroupedAccountSelectComponent implements OnInit { // if the account is already in the list, don't add it again... if (!group.accounts.find((a) => a.accountNumber === accountNumber)) { - // Insert recovered/added account at the top of the list so - // easy to see that it was added. - group.accounts.unshift(account); + group.accounts.push(account); } this.accountGroups.set(rootPublicKey, group); + + // scroll to, and temporarily highlight the account that was just added/recovered + window.requestAnimationFrame(() => { + const scrollContainer = document.getElementById( + 'account-select-group-' + rootPublicKey + ); + const accountElement = document.getElementById( + 'account-select-' + publicKeyBase58 + ); + + if (scrollContainer && accountElement) { + scrollContainer.scrollTop = accountElement.offsetTop; + } + }); + + this.justAddedPublicKey = publicKeyBase58; + setTimeout(() => { + this.justAddedPublicKey = undefined; + }, 3000); }); } @@ -300,12 +356,8 @@ export class GroupedAccountSelectComponent implements OnInit { } exportSeed(rootPublicKey: string) { - const dialogRef = this.dialog.open(BackupSeedDialogComponent, { + this.dialog.open(BackupSeedDialogComponent, { data: { rootPublicKey }, }); - - dialogRef.afterClosed().subscribe((result) => { - console.log(`Dialog result: ${result}`); - }); } } diff --git a/src/app/grouped-account-select/recovery-secret/recovery-secret.component.html b/src/app/grouped-account-select/recovery-secret/recovery-secret.component.html index 0ba0e165..708f08e1 100644 --- a/src/app/grouped-account-select/recovery-secret/recovery-secret.component.html +++ b/src/app/grouped-account-select/recovery-secret/recovery-secret.component.html @@ -2,7 +2,7 @@ class="input--textarea input--seed margin-bottom--small" style="background-color: black; overflow: hidden" > - {{ this.isRevealed ? secret : maskSecret(secret) }} + {{ this.isRevealed ? secret : maskedSecret }}
    diff --git a/src/app/grouped-account-select/recovery-secret/recovery-secret.component.ts b/src/app/grouped-account-select/recovery-secret/recovery-secret.component.ts index 5d2807a1..f8181fd5 100644 --- a/src/app/grouped-account-select/recovery-secret/recovery-secret.component.ts +++ b/src/app/grouped-account-select/recovery-secret/recovery-secret.component.ts @@ -1,18 +1,19 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; @Component({ selector: 'recovery-secret', templateUrl: './recovery-secret.component.html', styleUrls: ['./recovery-secret.component.scss'], }) -export class RecoverySecretComponent { +export class RecoverySecretComponent implements OnInit { @Input() secret = ''; + maskedSecret = ''; isRevealed = false; copySuccess = false; - maskSecret(secret = '') { - return secret.slice().replace(/\S/g, '*'); + ngOnInit(): void { + this.maskedSecret = this.secret.replace(/\S/g, '*'); } copySecret() { diff --git a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.html b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.html index 96ea615c..337d4fac 100644 --- a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.html +++ b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.html @@ -41,13 +41,13 @@

    Remove Account

diff --git a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.spec.ts b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.spec.ts deleted file mode 100644 index 5b491d60..00000000 --- a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { RemoveAccountDialogComponent } from './remove-account-dialog.component'; - -describe('RemoveAccountDialogComponent', () => { - let component: RemoveAccountDialogComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [RemoveAccountDialogComponent], - }); - fixture = TestBed.createComponent(RemoveAccountDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.ts b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.ts index 6d2046e2..18e88b58 100644 --- a/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.ts +++ b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.ts @@ -8,7 +8,7 @@ import { AccountService } from '../../account.service'; styleUrls: ['./remove-account-dialog.component.scss'], }) export class RemoveAccountDialogComponent { - copySuccess: boolean = false; + copySuccess = false; constructor( public dialogRef: MatDialogRef, diff --git a/src/app/identity.service.ts b/src/app/identity.service.ts index 248fe395..1b3112cc 100644 --- a/src/app/identity.service.ts +++ b/src/app/identity.service.ts @@ -24,9 +24,9 @@ import { TransactionMetadataDeleteUserAssociation, TransactionMetadataFollow, TransactionMetadataLike, - TransactionMetadataNewMessage, TransactionMetadataNFTBid, TransactionMetadataNFTTransfer, + TransactionMetadataNewMessage, TransactionMetadataPrivateMessage, TransactionMetadataSubmitPost, TransactionMetadataSwapIdentity, @@ -222,19 +222,15 @@ export class IdentityService { const { id, - payload: { encryptedSeedHex, unsignedHashes, ownerPublicKeyBase58Check }, + payload: { encryptedSeedHex, unsignedHashes }, } = data; const seedHex = this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); - const { accountNumber = 0 } = ownerPublicKeyBase58Check - ? this.accountService.getAccountInfo(ownerPublicKeyBase58Check) - : {}; const signedHashes = this.signingService.signHashes( seedHex, - unsignedHashes, - accountNumber + unsignedHashes ); this.respond(id, { @@ -249,19 +245,15 @@ export class IdentityService { const { id, - payload: { encryptedSeedHex, unsignedHashes, ownerPublicKeyBase58Check }, + payload: { encryptedSeedHex, unsignedHashes }, } = data; const seedHex = this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); - const { accountNumber = 0 } = ownerPublicKeyBase58Check - ? this.accountService.getAccountInfo(ownerPublicKeyBase58Check) - : {}; const signatures = this.signingService.signHashesETH( seedHex, - unsignedHashes, - accountNumber + unsignedHashes ); this.respond(id, { @@ -276,7 +268,6 @@ export class IdentityService { encryptedSeedHex, transactionHex, derivedPublicKeyBase58Check, - ownerPublicKeyBase58Check, }, } = data; @@ -305,14 +296,10 @@ export class IdentityService { this.globalVars.hostname ); const isDerived = !!derivedPublicKeyBase58Check; - const { accountNumber = 0 } = ownerPublicKeyBase58Check - ? this.accountService.getAccountInfo(ownerPublicKeyBase58Check) - : {}; const signedTransactionHex = this.signingService.signTransaction( seedHex, transactionHex, - isDerived, - accountNumber + isDerived ); this.respond(id, { @@ -372,10 +359,7 @@ export class IdentityService { senderGroupKeyName, recipientPublicKey, message, - { - ownerPublicKeyBase58Check, - messagingKeyRandomness, - } + messagingKeyRandomness ); this.respond(id, { ...encryptedMessage }); @@ -421,8 +405,7 @@ export class IdentityService { try { const decryptedHexes = this.accountService.decryptMessagesLegacy( seedHex, - encryptedHexes, - data.payload.ownerPublicKeyBase58Check + encryptedHexes ); this.respond(id, { decryptedHexes, @@ -440,10 +423,8 @@ export class IdentityService { seedHex, encryptedMessages, data.payload.messagingGroups || [], - { - messagingKeyRandomness, - ownerPublicKeyBase58Check: data.payload.ownerPublicKeyBase58Check, - } + messagingKeyRandomness, + data.payload.ownerPublicKeyBase58Check ) .then( (res) => this.respond(id, { decryptedHexes: res }), @@ -462,21 +443,14 @@ export class IdentityService { const { id, - payload: { - encryptedSeedHex, - derivedPublicKeyBase58Check, - ownerPublicKeyBase58Check, - }, + payload: { encryptedSeedHex, derivedPublicKeyBase58Check }, } = data; const seedHex = this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); - const { accountNumber = 0 } = ownerPublicKeyBase58Check - ? this.accountService.getAccountInfo(ownerPublicKeyBase58Check) - : {}; const isDerived = !!derivedPublicKeyBase58Check; - const jwt = this.signingService.signJWT(seedHex, accountNumber, isDerived); + const jwt = this.signingService.signJWT(seedHex, isDerived); this.respond(id, { jwt, @@ -565,7 +539,6 @@ export class IdentityService { if (accessLevel < requiredAccessLevel) { return false; } - const seedHex = this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname diff --git a/src/app/log-in-seed/log-in-seed.component.ts b/src/app/log-in-seed/log-in-seed.component.ts index cb71a177..f23bd05e 100644 --- a/src/app/log-in-seed/log-in-seed.component.ts +++ b/src/app/log-in-seed/log-in-seed.component.ts @@ -85,41 +85,41 @@ export class LogInSeedComponent implements OnInit { keychain, mnemonic, extraText, - network, - 0 + network ); // NOTE: Temporary support for 1 in 128 legacy users who have non-standard derivations if (keychain.publicKey !== keychainNonStandard.publicKey) { const seedHex = this.cryptoService.keychainToSeedHex(keychainNonStandard); - const privateKey = this.cryptoService.seedHexToKeyPair(seedHex, 0); + const privateKey = this.cryptoService.seedHexToKeyPair(seedHex); const publicKey = this.cryptoService.privateKeyToDeSoPublicKey( privateKey, network ); // We only want to add nonStandard derivations if the account is worth importing - this.backendApi.GetUsersStateless([publicKey]).subscribe((res) => { - if (!res.UserList.length) { - return; - } - const user = res.UserList[0]; - if ( - user.ProfileEntryResponse || - user.BalanceNanos > 0 || - user.UsersYouHODL?.length - ) { - // Add the non-standard key if the user has a profile, a balance, or holdings - userPublicKey = this.accountService.addUser( - keychainNonStandard, - mnemonic, - extraText, - network, - 0 - ); - } - }); + this.backendApi + .GetUsersStateless([publicKey], true, true) + .subscribe((res) => { + if (!res.UserList.length) { + return; + } + const user = res.UserList[0]; + if ( + user.ProfileEntryResponse || + user.BalanceNanos > 0 || + user.UsersYouHODL?.length + ) { + // Add the non-standard key if the user has a profile, a balance, or holdings + userPublicKey = this.accountService.addUser( + keychainNonStandard, + mnemonic, + extraText, + network + ); + } + }); } } diff --git a/src/app/log-in/log-in.component.html b/src/app/log-in/log-in.component.html index 51382feb..5dfcb60c 100644 --- a/src/app/log-in/log-in.component.html +++ b/src/app/log-in/log-in.component.html @@ -119,18 +119,6 @@

- - - -
- or -
- -
+
diff --git a/src/app/sign-up-metamask/sign-up-metamask.component.ts b/src/app/sign-up-metamask/sign-up-metamask.component.ts index f9d33cab..5939bb7d 100644 --- a/src/app/sign-up-metamask/sign-up-metamask.component.ts +++ b/src/app/sign-up-metamask/sign-up-metamask.component.ts @@ -236,8 +236,7 @@ export class SignUpMetamaskComponent implements OnInit { const signedTransactionHex = this.signingService.signTransaction( derivedKeyPair.getPrivate().toString('hex'), authorizeDerivedKeyResponse.TransactionHex, - true, - 0 + true ); this.backendApi diff --git a/src/app/sign-up/sign-up.component.html b/src/app/sign-up/sign-up.component.html index 57fef6eb..810ca6a3 100644 --- a/src/app/sign-up/sign-up.component.html +++ b/src/app/sign-up/sign-up.component.html @@ -314,7 +314,7 @@

Verify your DeSo seed phrase

Never share your DeSo seed phrase with anyone.
-
+

@@ -331,7 +331,11 @@

Verify your DeSo seed phrase

class="section--seed__container margin-bottom--medium" *ngIf="entropyService.temporaryEntropy.extraText.length > 0" > - Enter your passphrase: +

+ Enter your passphrase: +