From 5ea24e330c47b614fbb7afc0e52e5b7f05cc0bc0 Mon Sep 17 00:00:00 2001 From: Jackson Dean Date: Wed, 20 Sep 2023 09:29:54 -0700 Subject: [PATCH 1/6] refactor: update account service to support sub accounts --- src/app/account.service.ts | 439 ++++++++++++++---- src/app/approve/approve.component.ts | 18 +- src/app/auth/google/google.component.ts | 10 +- src/app/backend-api.service.ts | 16 +- src/app/crypto.service.ts | 53 ++- src/app/identity.service.ts | 50 +- src/app/log-in-seed/log-in-seed.component.ts | 46 +- .../sign-up-metamask.component.ts | 3 +- src/app/sign-up/sign-up.component.ts | 3 +- src/app/signing.service.ts | 44 +- src/lib/account-number.ts | 53 +++ src/types/identity.ts | 24 +- 12 files changed, 601 insertions(+), 158 deletions(-) create mode 100644 src/lib/account-number.ts diff --git a/src/app/account.service.ts b/src/app/account.service.ts index c62b4492..0f95d29f 100644 --- a/src/app/account.service.ts +++ b/src/app/account.service.ts @@ -5,8 +5,8 @@ import { ec as EC } from 'elliptic'; import HDKey from 'hdkey'; import * as jsonwebtoken from 'jsonwebtoken'; import KeyEncoder from 'key-encoder'; -import { CookieService } from 'ngx-cookie'; import sha256 from 'sha256'; +import { generateAccountNumber } from '../lib/account-number'; import { uint64ToBufBigEndian } from '../lib/bindata/util'; import { Transaction, @@ -24,6 +24,7 @@ import { PrivateUserInfo, PrivateUserVersion, PublicUserInfo, + SubAccountMetadata, } from '../types/identity'; import { BackendAPIService, @@ -38,6 +39,12 @@ import { MetamaskService } from './metamask.service'; import { SigningService } from './signing.service'; export const ERROR_USER_NOT_FOUND = 'User not found'; +const SUB_ACCOUNT_REVERSE_LOOKUP_KEY = 'subAccountReverseLookup'; + +export interface SubAccountReversLookupEntry { + lookupKey: string; + accountNumber: number; +} @Injectable({ providedIn: 'root', @@ -51,21 +58,89 @@ export class AccountService { constructor( private cryptoService: CryptoService, private globalVars: GlobalVarsService, - private cookieService: CookieService, private entropyService: EntropyService, private signingService: SigningService, private metamaskService: MetamaskService - ) {} + ) { + this.initializeSubAccountReverseLookup(); + } // Public Getters getPublicKeys(): any { - return Object.keys(this.getPrivateUsers()); + return Object.keys(this.getRootLevelUsers()); + } + + getAccountInfo(publicKey: string): PrivateUserInfo & SubAccountMetadata { + const privateUsers = this.getRootLevelUsers(); + let info = null; + + if (publicKey in privateUsers) { + info = { + ...privateUsers[publicKey], + // If the user is in the top level users map, their keys were generated + // with account number 0. This is the "root/parent" account. + accountNumber: 0, + }; + } + + // If the user is not found at the top level, it should be a sub account public key. + const lookup = this.getSubAccountReverseLookupMap(); + const mapping = lookup[publicKey]; + + if (mapping) { + const rootUser = privateUsers[mapping.lookupKey]; + + const foundAccount = rootUser.subAccounts?.find( + (a) => a.accountNumber === mapping.accountNumber + ); + + if (foundAccount) { + info = { + ...rootUser, + ...foundAccount, + }; + } + } + + if (info === null) { + throw new Error(`No user found for public key ${publicKey}`); + } + + return info; + } + + getSubAccountReverseLookupMap(): { + [subAccountKey: string]: SubAccountReversLookupEntry | undefined; + } { + const json = window.localStorage.getItem(SUB_ACCOUNT_REVERSE_LOOKUP_KEY); + return json ? JSON.parse(json) : {}; + } + + /** + * Add the sub-account public key to a reverse lookup map. We'll need + * this to look up the account number and the seed from the public key. + */ + private updateSubAccountReverseLookupMap({ + lookupKey, + accountNumber, + }: SubAccountReversLookupEntry) { + const keyMap = this.getSubAccountReverseLookupMap(); + const subAccountPublicKey = this.getAccountPublicKeyBase58( + lookupKey, + accountNumber + ); + keyMap[subAccountPublicKey] = { lookupKey, accountNumber }; + + window.localStorage.setItem( + SUB_ACCOUNT_REVERSE_LOOKUP_KEY, + JSON.stringify(keyMap) + ); } getEncryptedUsers(): { [key: string]: PublicUserInfo } { const hostname = this.globalVars.hostname; - const privateUsers = this.getPrivateUsers(); + const privateUsers = this.getRootLevelUsers(); const publicUsers: { [key: string]: PublicUserInfo } = {}; for (const publicKey of Object.keys(privateUsers)) { @@ -115,14 +190,8 @@ export class AccountService { } requiresMessagingKeyRandomness(publicKey: string): boolean { - const privateUser = this.getPrivateUsers()[publicKey]; - if (!privateUser) { - console.error('private user not found'); - throw new Error('private user not found'); - } - return ( - this.isMetamaskAccount(privateUser) && !privateUser.messagingKeyRandomness - ); + const account = this.getAccountInfo(publicKey); + return this.isMetamaskAccount(account) && !account.messagingKeyRandomness; } getAccessLevel(publicKey: string, hostname: string): AccessLevel { @@ -153,13 +222,9 @@ export class AccountService { derivedPublicKeyBase58CheckInput?: string, expirationDays?: number ): Promise { - if (!(publicKeyBase58Check in this.getPrivateUsers())) { - return undefined; - } - - const privateUser = this.getPrivateUsers()[publicKeyBase58Check]; - const network = privateUser.network; - const isMetamask = this.isMetamaskAccount(privateUser); + const account = this.getAccountInfo(publicKeyBase58Check); + const network = account.network; + const isMetamask = this.isMetamaskAccount(account); let derivedSeedHex = ''; let derivedPublicKeyBuffer: number[]; @@ -167,7 +232,7 @@ export class AccountService { let jwt = ''; let derivedJwt = ''; const numDaysBeforeExpiration = expirationDays || 30; - + const options = { expiration: `${numDaysBeforeExpiration} days` }; if (!derivedPublicKeyBase58CheckInput) { const derivedKeyData = this.generateDerivedKey(network); derivedPublicKeyBase58Check = derivedKeyData.derivedPublicKeyBase58Check; @@ -181,8 +246,9 @@ export class AccountService { // 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, - `${numDaysBeforeExpiration} days` + options ); } else { // If the user has passed in a derived public key, use that instead. @@ -196,9 +262,10 @@ 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( - privateUser.seedHex, + account.seedHex, + account.accountNumber, isMetamask, - `${numDaysBeforeExpiration} days` + options ); // Generate new btc and eth deposit addresses for the derived key. @@ -295,9 +362,11 @@ export class AccountService { ); } } else { - accessSignature = this.signingService.signHashes(privateUser.seedHex, [ - accessHash, - ])[0]; + accessSignature = this.signingService.signHashes( + account.seedHex, + [accessHash], + account.accountNumber + )[0]; } const { messagingPublicKeyBase58Check, @@ -329,7 +398,7 @@ export class AccountService { } getDefaultKeyPrivateUser(publicKey: string, appPublicKey: string): any { - const privateUser = this.getPrivateUsers()[publicKey]; + const privateUser = this.getRootLevelUsers()[publicKey]; const network = privateUser.network; // create jwt with private key and app public key const keyEncoder = new KeyEncoder('secp256k1'); @@ -435,10 +504,25 @@ export class AccountService { mnemonic: string, extraText: string, network: Network, - google?: boolean + accountNumber: number, + options: { + google?: boolean; + } = {} ): 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.seedHexToPrivateKey(seedHex); + const keyPair = this.cryptoService.seedHexToKeyPair(seedHex, 0); const btcDepositAddress = this.cryptoService.keychainToBtcAddress( // @ts-ignore TODO: add "identifier" to type definition keychain.identifier, @@ -447,7 +531,7 @@ export class AccountService { const ethDepositAddress = this.cryptoService.publicKeyToEthAddress(keyPair); let loginMethod: LoginMethod = LoginMethod.DESO; - if (google) { + if (options.google) { loginMethod = LoginMethod.GOOGLE; } @@ -460,11 +544,12 @@ export class AccountService { network, loginMethod, version: PrivateUserVersion.V2, + lastLoginTimestamp: Date.now(), }); } addUserWithSeedHex(seedHex: string, network: Network): string { - const keyPair = this.cryptoService.seedHexToPrivateKey(seedHex); + const keyPair = this.cryptoService.seedHexToKeyPair(seedHex, 0); const helperKeychain = new HDKey(); helperKeychain.privateKey = Buffer.from(seedHex, 'hex'); // @ts-ignore TODO: add "identifier" to type definition @@ -484,6 +569,7 @@ export class AccountService { network, loginMethod: LoginMethod.DESO, version: PrivateUserVersion.V2, + lastLoginTimestamp: Date.now(), }); } @@ -556,8 +642,9 @@ export class AccountService { // Migrate from V0 -> V1 if (privateUser.version === PrivateUserVersion.V0) { // Add ethDepositAddress field - const keyPair = this.cryptoService.seedHexToPrivateKey( - privateUser.seedHex + const keyPair = this.cryptoService.seedHexToKeyPair( + privateUser.seedHex, + 0 ); privateUser.ethDepositAddress = this.cryptoService.publicKeyToEthAddress(keyPair); @@ -589,12 +676,11 @@ export class AccountService { ownerPublicKeyBase58Check: string, publicKey: string ): string { - const privateUsers = this.getPrivateUsers(); - if (!(ownerPublicKeyBase58Check in privateUsers)) { - return ''; - } - const seedHex = privateUsers[ownerPublicKeyBase58Check].seedHex; - const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); + const account = this.getAccountInfo(ownerPublicKeyBase58Check); + const privateKey = this.cryptoService.seedHexToKeyPair( + account.seedHex, + account.accountNumber + ); const privateKeyBytes = privateKey.getPrivate().toBuffer(undefined, 32); const publicKeyBytes = this.cryptoService.publicKeyToECBuffer(publicKey); const sharedPx = ecies.derive(privateKeyBytes, publicKeyBytes); @@ -606,17 +692,12 @@ export class AccountService { ownerPublicKeyBase58Check: string, messagingKeyName: string ): Promise { - const privateUsers = this.getPrivateUsers(); - if (!(ownerPublicKeyBase58Check in privateUsers)) { - throw new Error(ERROR_USER_NOT_FOUND); - } - const privateUser = privateUsers[ownerPublicKeyBase58Check]; - const seedHex = privateUser.seedHex; + const account = this.getAccountInfo(ownerPublicKeyBase58Check); // Compute messaging private key as sha256x2( sha256x2(secret key) || sha256x2(messageKeyname) ) let messagingPrivateKeyBuff; try { messagingPrivateKeyBuff = await this.getMessagingKey( - privateUser, + account, messagingKeyName ); } catch (e) { @@ -644,9 +725,11 @@ export class AccountService { let messagingKeySignature = ''; if (messagingKeyName === this.globalVars.defaultMessageKeyName) { - messagingKeySignature = this.signingService.signHashes(seedHex, [ - messagingKeyHash, - ])[0]; + messagingKeySignature = this.signingService.signHashes( + account.seedHex, + [messagingKeyHash], + account.accountNumber + )[0]; } return { @@ -740,9 +823,18 @@ export class AccountService { senderGroupKeyName: string, recipientPublicKey: string, message: string, - messagingKeyRandomness: string | undefined + options: { + messagingKeyRandomness?: string; + ownerPublicKeyBase58Check?: string; + } = {} ): any { - const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); + const { accountNumber = 0 } = options.ownerPublicKeyBase58Check + ? this.getAccountInfo(options.ownerPublicKeyBase58Check) + : {}; + const privateKey = this.cryptoService.seedHexToKeyPair( + seedHex, + accountNumber + ); const privateKeyBuffer = privateKey.getPrivate().toBuffer(undefined, 32); const publicKeyBuffer = @@ -754,7 +846,7 @@ export class AccountService { privateEncryptionKey = this.getMessagingKeyForSeed( seedHex, senderGroupKeyName, - messagingKeyRandomness + options.messagingKeyRandomness ); } @@ -773,9 +865,16 @@ export class AccountService { // @param encryptedHexes : string[] decryptMessagesLegacy( seedHex: string, - encryptedHexes: any + encryptedHexes: any, + options: { ownerPublicKeyBase58Check?: string } = {} ): { [key: string]: any } { - const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); + const { accountNumber = 0 } = options.ownerPublicKeyBase58Check + ? this.getAccountInfo(options.ownerPublicKeyBase58Check) + : {}; + const privateKey = this.cryptoService.seedHexToKeyPair( + seedHex, + accountNumber + ); const privateKeyBuffer = privateKey.getPrivate().toBuffer(undefined, 32); const decryptedHexes: { [key: string]: any } = {}; @@ -798,13 +897,21 @@ export class AccountService { seedHex: string, encryptedMessages: EncryptedMessage[], messagingGroups: MessagingGroup[], - messagingKeyRandomness: string | undefined, - ownerPublicKeyBase58Check: string | undefined + options: { + messagingKeyRandomness?: string; + ownerPublicKeyBase58Check?: string; + } = {} ): Promise<{ [key: string]: any }> { - const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); + const { accountNumber = 0 } = options.ownerPublicKeyBase58Check + ? this.getAccountInfo(options.ownerPublicKeyBase58Check) + : {}; + const privateKey = this.cryptoService.seedHexToKeyPair( + seedHex, + accountNumber + ); const myPublicKey = - ownerPublicKeyBase58Check || + options.ownerPublicKeyBase58Check || this.cryptoService.privateKeyToDeSoPublicKey( privateKey, this.globalVars.network @@ -905,7 +1012,7 @@ export class AccountService { this.getMessagingKeyForSeed( seedHex, myMessagingGroupMemberEntry.GroupMemberKeyName, - messagingKeyRandomness + options.messagingKeyRandomness ); privateEncryptionKey = this.signingService .decryptGroupMessagingPrivateKeyToMember( @@ -928,7 +1035,7 @@ export class AccountService { privateEncryptionKey = this.getMessagingKeyForSeed( seedHex, this.globalVars.defaultMessageKeyName, - messagingKeyRandomness + options.messagingKeyRandomness ); } } catch (e: any) { @@ -953,12 +1060,9 @@ export class AccountService { return decryptedHexes; } - // Private Getters and Modifiers - - // TEMP: public for import flow - public addPrivateUser(userInfo: PrivateUserInfo): string { + addPrivateUser(userInfo: PrivateUserInfo): string { const privateUsers = this.getPrivateUsersRaw(); - const privateKey = this.cryptoService.seedHexToPrivateKey(userInfo.seedHex); + const privateKey = this.cryptoService.seedHexToKeyPair(userInfo.seedHex, 0); // Metamask login will be added with the master public key. let publicKey = this.cryptoService.privateKeyToDeSoPublicKey( @@ -1020,11 +1124,11 @@ export class AccountService { getLoginMethodWithPublicKeyBase58Check( publicKeyBase58Check: string ): LoginMethod { - const account = this.getPrivateUsers()[publicKeyBase58Check]; + const account = this.getRootLevelUsers()[publicKeyBase58Check]; return account.loginMethod || LoginMethod.DESO; } - private getPrivateUsers(): { [key: string]: PrivateUserInfo } { + getRootLevelUsers(): { [key: string]: PrivateUserInfo } { const privateUsers = this.getPrivateUsersRaw(); const filteredPrivateUsers: { [key: string]: PrivateUserInfo } = {}; @@ -1048,26 +1152,168 @@ export class AccountService { return filteredPrivateUsers; } - private getPrivateUsersRaw(): { [key: string]: PrivateUserInfo } { - return JSON.parse( - localStorage.getItem(AccountService.USERS_STORAGE_KEY) || '{}' + updateAccountInfo(publicKey: string, attrs: Partial): void { + const privateUsers = this.getPrivateUsersRaw(); + + if (!privateUsers[publicKey]) { + // we could be dealing with a sub account. + const lookupMap = this.getSubAccountReverseLookupMap(); + const mapping = lookupMap[publicKey]; + + if (!mapping) { + throw new Error(`User not found for public key: ${publicKey}`); + } + + const rootUser = privateUsers[mapping.lookupKey]; + + if (!rootUser) { + throw new Error(`Root user not found for public key: ${publicKey}`); + } + + const subAccounts = rootUser.subAccounts ?? []; + const subAccountIndex = subAccounts.findIndex( + (a) => a.accountNumber === mapping.accountNumber + ); + + if (subAccountIndex < 0) { + throw new Error( + `Sub account not found for root user public key: ${publicKey} with account number: ${mapping.accountNumber}}` + ); + } + + subAccounts[subAccountIndex] = { + ...subAccounts[subAccountIndex], + ...attrs, + }; + + privateUsers[mapping.lookupKey] = { + ...rootUser, + subAccounts, + }; + } else { + privateUsers[publicKey] = { + ...privateUsers[publicKey], + ...attrs, + }; + } + + this.setPrivateUsersRaw(privateUsers); + } + + /** + * Adds a new sub account entry to the root user's subAccounts array. If the + * account number is provided, we will use it. Otherwise we will generate a + * new account number that is not already in use. If the account number + * provided matches an existing account, we will just make sure it appears in + * the UI again if it had been hidden before. If it matches and the account is + * NOT hidden, then nothing happens. + */ + addSubAccount( + rootPublicKey: string, + options: { accountNumber?: number } = {} + ): number { + // The zeroth account represents the "root" account key so we don't allow it + // for sub-accounts. There is nothing particularly special about the root + // account, but for historical reasons its public key is used to index the + // main users map in local storage. + if (options.accountNumber === 0) { + this.updateAccountInfo(rootPublicKey, { isHidden: false }); + return 0; + } + + const privateUsers = this.getPrivateUsersRaw(); + const parentAccount = privateUsers[rootPublicKey]; + + if (!parentAccount) { + throw new Error( + `Parent account not found for public key: ${rootPublicKey}` + ); + } + + const subAccounts = parentAccount.subAccounts ?? []; + const foundAccountIndex = + typeof options.accountNumber === 'number' + ? subAccounts.findIndex( + (a) => a.accountNumber === options.accountNumber + ) + : -1; + const accountNumbers = new Set(subAccounts.map((a) => a.accountNumber)); + const accountNumber = + options.accountNumber ?? generateAccountNumber(accountNumbers); + + let newSubAccounts: SubAccountMetadata[] = []; + + if (foundAccountIndex !== -1) { + // If accountNumber is provided and we already have it, we just make sure + // the existing account is not hidden. + subAccounts[foundAccountIndex].isHidden = false; + newSubAccounts = subAccounts; + } else { + // otherwise we create a new sub account + newSubAccounts = subAccounts.concat({ + accountNumber, + isHidden: false, + }); + + this.updateSubAccountReverseLookupMap({ + lookupKey: rootPublicKey, + accountNumber, + }); + } + + // sanity check that we're not adding a duplicate account number before we save. + const accountNumbersSet = new Set( + newSubAccounts.map((a) => a.accountNumber) ); + if (accountNumbersSet.size !== newSubAccounts.length) { + throw new Error( + `Duplicate account number ${accountNumber} found for root user public key: ${rootPublicKey}` + ); + } + + this.updateAccountInfo(rootPublicKey, { subAccounts: newSubAccounts }); + + return accountNumber; } - encryptedSeedHexToPublicKeyBase58Check(encryptedSeedHex: string): string { - return this.seedHexToPublicKeyBase58Check( - this.cryptoService.decryptSeedHex( - encryptedSeedHex, - this.globalVars.hostname - ) + getAccountPublicKeyBase58( + rootPublicKeyBase58: string, + accountNumber: number = 0 + ) { + // Account number 0 is reserved for the parent account, so we can just + // return the parent key directly in this case. + if (accountNumber === 0) { + return rootPublicKeyBase58; + } + + const users = this.getRootLevelUsers(); + const parentAccount = users[rootPublicKeyBase58]; + + if (!parentAccount) { + throw new Error( + `Account not found for public key: ${rootPublicKeyBase58}` + ); + } + + const parentSeedHex = parentAccount.seedHex; + const childKey = this.cryptoService.getSubAccountKeychain( + parentSeedHex, + accountNumber + ); + const ec = new EC('secp256k1'); + const keyPair = ec.keyFromPrivate(childKey.privateKey); + + return this.cryptoService.publicKeyToDeSoPublicKey( + keyPair, + parentAccount.network ); } - seedHexToPublicKeyBase58Check(seedHex: string): string { - const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); - return this.cryptoService.privateKeyToDeSoPublicKey( - privateKey, - this.globalVars.network + // Private Getters and Modifiers + + private getPrivateUsersRaw(): { [key: string]: PrivateUserInfo } { + return JSON.parse( + localStorage.getItem(AccountService.USERS_STORAGE_KEY) || '{}' ); } @@ -1079,4 +1325,33 @@ export class AccountService { JSON.stringify(privateUsers) ); } + + /** + * It's possible for the reverse lookup to get out of sync, especially during + * development or testing. This method will fix any discrepancies by iterating + * through all the accounts and adding any missing entries. + */ + private initializeSubAccountReverseLookup() { + const lookupMap = this.getSubAccountReverseLookupMap(); + const users = this.getRootLevelUsers(); + + Object.keys(users).forEach((lookupKey) => { + const subAccounts = users[lookupKey].subAccounts ?? []; + subAccounts.forEach((subAccount) => { + const publicKey = this.getAccountPublicKeyBase58( + lookupKey, + subAccount.accountNumber + ); + lookupMap[publicKey] = { + lookupKey, + accountNumber: subAccount.accountNumber, + }; + }); + }); + + window.localStorage.setItem( + SUB_ACCOUNT_REVERSE_LOOKUP_KEY, + JSON.stringify(lookupMap) + ); + } } diff --git a/src/app/approve/approve.component.ts b/src/app/approve/approve.component.ts index 51846e43..9001d3d8 100644 --- a/src/app/approve/approve.component.ts +++ b/src/app/approve/approve.component.ts @@ -100,12 +100,13 @@ export class ApproveComponent implements OnInit { } onSubmit(): void { - const user = this.accountService.getEncryptedUsers()[this.publicKey]; - const isDerived = this.accountService.isMetamaskAccount(user); + const account = this.accountService.getAccountInfo(this.publicKey); + const isDerived = this.accountService.isMetamaskAccount(account); const signedTransactionHex = this.signingService.signTransaction( - this.seedHex(), + account.seedHex, this.transactionHex, - isDerived + isDerived, + account.accountNumber ); this.finishFlow(signedTransactionHex); } @@ -117,15 +118,6 @@ export class ApproveComponent implements OnInit { }); } - seedHex(): string { - const encryptedSeedHex = - this.accountService.getEncryptedUsers()[this.publicKey].encryptedSeedHex; - return this.cryptoService.decryptSeedHex( - encryptedSeedHex, - this.globalVars.hostname - ); - } - generateTransactionDescription(): void { let description = 'sign an unknown transaction'; let publicKeys: string[] = []; diff --git a/src/app/auth/google/google.component.ts b/src/app/auth/google/google.component.ts index a1f7bdea..771dbd52 100644 --- a/src/app/auth/google/google.component.ts +++ b/src/app/auth/google/google.component.ts @@ -93,7 +93,10 @@ export class GoogleComponent implements OnInit { mnemonic, extraText, network, - true + 0, + { + google: true, + } ); } catch (err) { console.error(err); @@ -146,7 +149,10 @@ export class GoogleComponent implements OnInit { mnemonic, extraText, network, - true + 0, + { + google: true, + } ); this.loading = false; }); diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts index ca4bc41b..56fc4827 100644 --- a/src/app/backend-api.service.ts +++ b/src/app/backend-api.service.ts @@ -291,24 +291,24 @@ export class BackendAPIService { } jwtPost(path: string, publicKey: string, body: any): Observable { - const publicUserInfo = this.accountService.getEncryptedUsers()[publicKey]; + const account = this.accountService.getAccountInfo(publicKey); // NOTE: there are some cases where derived user's were not being sent phone number // verification texts due to missing public user info. This is to log how often // this is happening. logInteractionEvent('backend-api', 'jwt-post', { - hasPublicUserInfo: !!publicUserInfo, + hasPublicUserInfo: !!account, }); - if (!publicUserInfo) { + if (!account) { return of(null); } - const isDerived = this.accountService.isMetamaskAccount(publicUserInfo); + const isDerived = this.accountService.isMetamaskAccount(account); - const seedHex = this.cryptoService.decryptSeedHex( - publicUserInfo.encryptedSeedHex, - this.globalVars.hostname + const jwt = this.signingService.signJWT( + account.seedHex, + account.accountNumber, + isDerived ); - const jwt = this.signingService.signJWT(seedHex, isDerived); return this.post(path, { ...body, ...{ JWT: jwt } }); } diff --git a/src/app/crypto.service.ts b/src/app/crypto.service.ts index 4196272c..e1108ed2 100644 --- a/src/app/crypto.service.ts +++ b/src/app/crypto.service.ts @@ -142,25 +142,49 @@ export class CryptoService { nonStandard?: boolean ): HDNode { const seed = bip39.mnemonicToSeedSync(mnemonic, extraText); - // @ts-ignore - return HDKey.fromMasterSeed(seed).derive("m/44'/0'/0'/0/0", nonStandard); + return deriveKeys(seed, 0, { + nonStandard, + }); + } + + getSubAccountKeychain(masterSeedHex: string, accountIndex: number): HDNode { + const seedBytes = Buffer.from(masterSeedHex, 'hex'); + return deriveKeys(seedBytes, accountIndex); } keychainToSeedHex(keychain: HDNode): string { return keychain.privateKey.toString('hex'); } - seedHexToPrivateKey(seedHex: string): EC.KeyPair { + /** + * 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 { 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): string { + encryptedSeedHexToPublicKey( + encryptedSeedHex: string, + accountNumber: number + ): string { const seedHex = this.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); - const privateKey = this.seedHexToPrivateKey(seedHex); + const privateKey = this.seedHexToKeyPair(seedHex, accountNumber); return this.privateKeyToDeSoPublicKey(privateKey, this.globalVars.network); } @@ -279,3 +303,22 @@ export class CryptoService { return ethAddressChecksum; } } + +/** + * We set the account according to the following derivation path scheme: + * 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( + seedBytes: Buffer, + accountIndex: number, + options?: { nonStandard?: boolean } +) { + // We are using a customized version of hdkey and the derive signature types + // are not compatible with the "nonStandard" flag. Hence the ts-ignore. + return HDKey.fromMasterSeed(seedBytes).derive( + `m/44'/0'/${accountIndex}'/0/0`, + // @ts-ignore + !!options?.nonStandard + ); +} diff --git a/src/app/identity.service.ts b/src/app/identity.service.ts index 3710be5c..248fe395 100644 --- a/src/app/identity.service.ts +++ b/src/app/identity.service.ts @@ -222,15 +222,19 @@ export class IdentityService { const { id, - payload: { encryptedSeedHex, unsignedHashes }, + payload: { encryptedSeedHex, unsignedHashes, ownerPublicKeyBase58Check }, } = 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 + unsignedHashes, + accountNumber ); this.respond(id, { @@ -245,15 +249,19 @@ export class IdentityService { const { id, - payload: { encryptedSeedHex, unsignedHashes }, + payload: { encryptedSeedHex, unsignedHashes, ownerPublicKeyBase58Check }, } = 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 + unsignedHashes, + accountNumber ); this.respond(id, { @@ -268,6 +276,7 @@ export class IdentityService { encryptedSeedHex, transactionHex, derivedPublicKeyBase58Check, + ownerPublicKeyBase58Check, }, } = data; @@ -295,13 +304,15 @@ export class IdentityService { encryptedSeedHex, this.globalVars.hostname ); - const isDerived = !!derivedPublicKeyBase58Check; - + const { accountNumber = 0 } = ownerPublicKeyBase58Check + ? this.accountService.getAccountInfo(ownerPublicKeyBase58Check) + : {}; const signedTransactionHex = this.signingService.signTransaction( seedHex, transactionHex, - isDerived + isDerived, + accountNumber ); this.respond(id, { @@ -361,7 +372,10 @@ export class IdentityService { senderGroupKeyName, recipientPublicKey, message, - messagingKeyRandomness + { + ownerPublicKeyBase58Check, + messagingKeyRandomness, + } ); this.respond(id, { ...encryptedMessage }); @@ -407,7 +421,8 @@ export class IdentityService { try { const decryptedHexes = this.accountService.decryptMessagesLegacy( seedHex, - encryptedHexes + encryptedHexes, + data.payload.ownerPublicKeyBase58Check ); this.respond(id, { decryptedHexes, @@ -425,8 +440,10 @@ export class IdentityService { seedHex, encryptedMessages, data.payload.messagingGroups || [], - messagingKeyRandomness, - data.payload.ownerPublicKeyBase58Check + { + messagingKeyRandomness, + ownerPublicKeyBase58Check: data.payload.ownerPublicKeyBase58Check, + } ) .then( (res) => this.respond(id, { decryptedHexes: res }), @@ -445,14 +462,21 @@ export class IdentityService { const { id, - payload: { encryptedSeedHex, derivedPublicKeyBase58Check }, + payload: { + encryptedSeedHex, + derivedPublicKeyBase58Check, + ownerPublicKeyBase58Check, + }, } = 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, isDerived); + const jwt = this.signingService.signJWT(seedHex, accountNumber, isDerived); this.respond(id, { jwt, 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 1e0bc3bc..cb71a177 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 + network, + 0 ); // 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.seedHexToPrivateKey(seedHex); + const privateKey = this.cryptoService.seedHexToKeyPair(seedHex, 0); const publicKey = this.cryptoService.privateKeyToDeSoPublicKey( privateKey, network ); // We only want to add nonStandard derivations if the account is worth importing - 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 - ); - } - }); + 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 + ); + } + }); } } 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 5939bb7d..f9d33cab 100644 --- a/src/app/sign-up-metamask/sign-up-metamask.component.ts +++ b/src/app/sign-up-metamask/sign-up-metamask.component.ts @@ -236,7 +236,8 @@ export class SignUpMetamaskComponent implements OnInit { const signedTransactionHex = this.signingService.signTransaction( derivedKeyPair.getPrivate().toString('hex'), authorizeDerivedKeyResponse.TransactionHex, - true + true, + 0 ); this.backendApi diff --git a/src/app/sign-up/sign-up.component.ts b/src/app/sign-up/sign-up.component.ts index 947d970e..74c18f96 100644 --- a/src/app/sign-up/sign-up.component.ts +++ b/src/app/sign-up/sign-up.component.ts @@ -117,7 +117,8 @@ export class SignUpComponent implements OnInit, OnDestroy { keychain, mnemonic, extraText, - network + network, + 0 ); this.accountService.setAccessLevel( diff --git a/src/app/signing.service.ts b/src/app/signing.service.ts index 063d02be..1dfb28e3 100644 --- a/src/app/signing.service.ts +++ b/src/app/signing.service.ts @@ -20,13 +20,24 @@ export class SigningService { signJWT( seedHex: string, + accountNumber: number, isDerived: boolean, - expiration: string | number = 60 * 10 + { expiration = 60 * 10 }: { expiration?: string | number } = {} ): string { const keyEncoder = new KeyEncoder('secp256k1'); - const encodedPrivateKey = keyEncoder.encodePrivate(seedHex, 'raw', 'pem'); + // TODO: make sure the account number stuff works here... + const acctNumber = isDerived ? 0 : accountNumber; + const keys = this.cryptoService.seedHexToKeyPair(seedHex, acctNumber); + const encodedPrivateKey = keyEncoder.encodePrivate( + keys.getPrivate('hex'), + 'raw', + 'pem' + ); if (isDerived) { - const derivedPrivateKey = this.cryptoService.seedHexToPrivateKey(seedHex); + const derivedPrivateKey = this.cryptoService.seedHexToKeyPair( + seedHex, + accountNumber + ); const derivedPublicKeyBase58Check = this.cryptoService.privateKeyToDeSoPublicKey( derivedPrivateKey, @@ -52,9 +63,13 @@ export class SigningService { signTransaction( seedHex: string, transactionHex: string, - isDerivedKey: boolean + isDerivedKey: boolean, + accountNumber: number ): string { - const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); + const privateKey = this.cryptoService.seedHexToKeyPair( + seedHex, + accountNumber + ); const transactionBytes = new Buffer(transactionHex, 'hex'); const [_, v1FieldsBuffer] = TransactionV0.fromBytes(transactionBytes) as [ @@ -83,8 +98,15 @@ export class SigningService { ]).toString('hex'); } - signHashes(seedHex: string, unsignedHashes: string[]): string[] { - const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); + signHashes( + seedHex: string, + unsignedHashes: string[], + accountNumber: number + ): string[] { + const privateKey = this.cryptoService.seedHexToKeyPair( + seedHex, + accountNumber + ); const signedHashes = []; for (const unsignedHash of unsignedHashes) { @@ -98,9 +120,13 @@ export class SigningService { signHashesETH( seedHex: string, - unsignedHashes: string[] + unsignedHashes: string[], + accountNumber: number ): { s: any; r: any; v: number | null }[] { - const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); + const privateKey = this.cryptoService.seedHexToKeyPair( + seedHex, + accountNumber + ); const signedHashes = []; for (const unsignedHash of unsignedHashes) { diff --git a/src/lib/account-number.ts b/src/lib/account-number.ts new file mode 100644 index 00000000..c14bf0a4 --- /dev/null +++ b/src/lib/account-number.ts @@ -0,0 +1,53 @@ +/** + * https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys + * The max value that should be used for an hd key account number is 2^31 -1. In + * practice, we would never reach this via pure incremental account number + * generation, but we allow entering arbitrary account numbers in the UI, so we + * need to explicitly validate them. For whatever reason, the hdkey library + * we use throws for anything over 2^30. + */ +const MAX_UNSIGNED_INT_VALUE = 1073741824; // 2^30 + +export function isValid32BitUnsignedInt(value: number) { + return ( + Number.isInteger(value) && value >= 0 && value <= MAX_UNSIGNED_INT_VALUE + ); +} + +/** + * - The lowest possible sub-account number is 1 (0 is reserved for the "root" account). + * - If the set is empty, we just return 1. + * - Otherwise, we look for the highest number in the set and increment it by 1. + * - If the next incremented number is too big, we look back for the first gap in the numbers and use that instead. + * - If no gap is found (very unlikely), we throw an error. + */ +export function generateAccountNumber(accountNumbers: Set): number { + if (accountNumbers.size === 0) { + return 1; + } + + const sorted = Array.from(accountNumbers).sort((a, b) => a - b); + const currentHighestAccountNumber = sorted[sorted.length - 1]; + const candidate = currentHighestAccountNumber + 1; + + if (candidate <= MAX_UNSIGNED_INT_VALUE) { + return candidate; + } + + // At most we look back 500 numbers. This is a bit arbitrary... but the + // number of values could *technically* be 2^32 - 1, so we just limit the + // number of iterations to some reasonable value. The reason we look back for + // the highest available number instead of picking the lowest number is that + // the lowest number is more likely to have been used in the past and we're + // aiming to get a fresh wallet. + const maxLookBack = Math.max(sorted.length - 500, 0); + let nextExpectedValueInSequence = currentHighestAccountNumber - 1; + for (let i = sorted.length - 2; i >= maxLookBack; i--) { + if (nextExpectedValueInSequence !== sorted[i]) { + return nextExpectedValueInSequence; + } + nextExpectedValueInSequence--; + } + + throw new Error('Cannot generate account number.'); +} diff --git a/src/types/identity.ts b/src/types/identity.ts index b8fb5657..06f71ba0 100644 --- a/src/types/identity.ts +++ b/src/types/identity.ts @@ -1,6 +1,15 @@ import { TransactionSpendingLimit } from 'src/lib/deso/transaction'; -export interface PrivateUserInfo { +export interface AccountMetadata { + isHidden?: boolean; + lastLoginTimestamp?: number; +} + +export interface SubAccountMetadata extends AccountMetadata { + accountNumber: number; +} + +export interface PrivateUserInfo extends AccountMetadata { seedHex: string; mnemonic: string; publicKeyHex?: string; @@ -12,6 +21,12 @@ export interface PrivateUserInfo { version: PrivateUserVersion; messagingKeyRandomness?: string; derivedPublicKeyBase58Check?: string; + /** + * This is a list of the account numbers that this user has used to generate + * sub-accounts. An account number plus the parent account seed is enough to + * generate or recover a unique sub-account. + */ + subAccounts?: SubAccountMetadata[]; /** DEPRECATED in favor of loginMethod */ google?: boolean; @@ -82,8 +97,15 @@ export interface DefaultKeyPrivateInfo { export interface UserProfile { username: string; profilePic: any; + balanceNanos: number; } +export type Account = UserProfile & { + publicKey: string; + accountNumber: number; +}; +export type ParentAccount = Account & { subAccounts: Account[] }; + export interface DerivedKey { derivedPublicKeyBase58Check: string; ownerPublicKeyBase58Check: string; From 7027d240187199dd7de0ec812a28090d78dbd558 Mon Sep 17 00:00:00 2001 From: Jackson Dean Date: Mon, 25 Sep 2023 07:27:19 -0700 Subject: [PATCH 2/6] feature: new component for sub account UI (#266) * feature: new component for sub account UI * feature: add feature flag guard for new account select component (#268) * feature: add feature flag guard for new account select component * feature: backup seed component (#269) * feature: backup seed component * feature: add balance to account selector UI (#270) * feature: add balance to account selector UI * migrate swal to mat dialog (#273) * migrate swal to mat dialog * 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/app.module.ts | 10 + src/app/approve/approve.component.ts | 3 +- src/app/auth/google/google.component.ts | 8 +- src/app/backend-api.service.ts | 9 +- src/app/crypto.service.ts | 29 +- src/app/derive/derive.component.html | 9 +- src/app/derive/derive.component.ts | 10 +- src/app/global-vars.service.ts | 38 +- .../backup-seed-dialog.component.html | 94 +++++ .../backup-seed-dialog.component.scss | 0 .../backup-seed-dialog.component.ts | 50 +++ .../grouped-account-select.component.html | 196 ++++++++++ .../grouped-account-select.component.scss | 0 .../grouped-account-select.component.ts | 362 ++++++++++++++++++ .../recovery-secret.component.html | 26 ++ .../recovery-secret.component.scss | 0 .../recovery-secret.component.ts | 31 ++ .../remove-account-dialog.component.html | 53 +++ .../remove-account-dialog.component.scss | 0 .../remove-account-dialog.component.ts | 43 +++ src/app/icons/icons.module.ts | 6 + src/app/identity.service.ts | 51 +-- .../log-in-options.component.html | 2 +- src/app/log-in-seed/log-in-seed.component.ts | 46 +-- src/app/log-in/log-in.component.html | 6 +- .../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/lib/account-number.ts | 16 +- src/styles.scss | 66 +++- src/types/identity.ts | 7 + 34 files changed, 1182 insertions(+), 264 deletions(-) create mode 100644 src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html create mode 100644 src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.scss create mode 100644 src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.ts create mode 100644 src/app/grouped-account-select/grouped-account-select.component.html create mode 100644 src/app/grouped-account-select/grouped-account-select.component.scss create mode 100644 src/app/grouped-account-select/grouped-account-select.component.ts create mode 100644 src/app/grouped-account-select/recovery-secret/recovery-secret.component.html create mode 100644 src/app/grouped-account-select/recovery-secret/recovery-secret.component.scss create mode 100644 src/app/grouped-account-select/recovery-secret/recovery-secret.component.ts 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.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/app.module.ts b/src/app/app.module.ts index e59a2157..23933e92 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,7 @@ import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatTooltipModule } from '@angular/material/tooltip'; import { BrowserModule } from '@angular/platform-browser'; @@ -31,6 +32,10 @@ import { ErrorCallbackComponent } from './error-callback/error-callback.componen import { FreeDeSoDisclaimerComponent } from './free-deso-message/free-deso-disclaimer/free-deso-disclaimer.component'; import { FreeDesoMessageComponent } from './free-deso-message/free-deso-message.component'; 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'; @@ -98,6 +103,10 @@ import { TransactionSpendingLimitComponent } from './transaction-spending-limit/ TransactionSpendingLimitAssociationComponent, TransactionSpendingLimitAccessGroupComponent, TransactionSpendingLimitAccessGroupMemberComponent, + GroupedAccountSelectComponent, + RecoverySecretComponent, + BackupSeedDialogComponent, + RemoveAccountDialogComponent, ], imports: [ BrowserModule, @@ -114,6 +123,7 @@ import { TransactionSpendingLimitComponent } from './transaction-spending-limit/ }), BuyDeSoComponentWrapper, CookieModule.forRoot(), + MatDialogModule, ], providers: [ IdentityService, 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 56fc4827..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 } }); } @@ -349,7 +345,7 @@ export class BackendAPIService { publicKeys: string[] ): Observable<{ [key: string]: UserProfile }> { const userProfiles: { [key: string]: any } = {}; - const req = this.GetUsersStateless(publicKeys, true); + const req = this.GetUsersStateless(publicKeys, true, true); if (publicKeys.length > 0) { return req .pipe( @@ -358,6 +354,7 @@ export class BackendAPIService { userProfiles[user.PublicKeyBase58Check] = { username: user.ProfileEntryResponse?.Username, profilePic: user.ProfileEntryResponse?.ProfilePic, + balanceNanos: user.BalanceNanos, }; } return userProfiles; 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 44ec34cd..279a0bfa 100644 --- a/src/app/derive/derive.component.html +++ b/src/app/derive/derive.component.html @@ -21,14 +21,7 @@ }} - -
- or -
- +
0; - this.backendApi.GetAppState().subscribe((res) => { this.blockHeight = res.BlockHeight; }); @@ -69,6 +64,11 @@ export class DeriveComponent implements OnInit { throw Error('invalid query parameter permutation'); } if (params.publicKey) { + if (!this.publicKeyBase58Check) { + this.accountService.updateAccountInfo(params.publicKey, { + lastLoginTimestamp: Date.now(), + }); + } this.publicKeyBase58Check = params.publicKey; this.isSingleAccount = true; } diff --git a/src/app/global-vars.service.ts b/src/app/global-vars.service.ts index 050ff9de..e8fa4127 100644 --- a/src/app/global-vars.service.ts +++ b/src/app/global-vars.service.ts @@ -62,12 +62,6 @@ export class GlobalVarsService { */ showSkip: boolean = false; - /** - * Flag used to gate the new subAccounts functionality. After some sunset - * period (TBD), we can remove this flag and make this the default behavior. - */ - subAccounts: boolean = false; - /** * Set of public keys that have been authenticated by the calling application. * This is used as a hint to decide whether to show the derived key approval @@ -180,4 +174,36 @@ export class GlobalVarsService { formatTxCountLimit(count: number = 0): string { return count >= 1e9 ? 'UNLIMITED' : count.toLocaleString(); } + + abbreviateNumber(value: number) { + if (value === 0) { + return '0'; + } + + if (value < 0) { + return value.toString(); + } + if (value < 0.01) { + return value.toFixed(5); + } + if (value < 0.1) { + return value.toFixed(4); + } + + let shortValue; + const suffixes = ['', 'K', 'M', 'B', 'e12', 'e15', 'e18', 'e21']; + const suffixNum = Math.floor((('' + value.toFixed(0)).length - 1) / 3); + shortValue = value / Math.pow(1000, suffixNum); + if ( + Math.floor(shortValue / 100) > 0 || + shortValue / 1 === 0 || + suffixNum > 3 + ) { + return shortValue.toFixed(0) + suffixes[suffixNum]; + } + if (Math.floor(shortValue / 10) > 0 || Math.floor(shortValue) > 0) { + return shortValue.toFixed(2) + suffixes[suffixNum]; + } + return shortValue.toFixed(3) + suffixes[suffixNum]; + } } diff --git a/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html b/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html new file mode 100644 index 00000000..c5ddc562 --- /dev/null +++ b/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html @@ -0,0 +1,94 @@ +
+
+ +

Backup DeSo Seed

+
+
+
+

+ Your seed phrase is the only way to recover your DeSo account. If you + lose your seed phrase, you will lose access to your DeSo account. Store + it in a safe and secure place. +

+

+ DO NOT share your seed phrase with anyone! Support agents will never + request this. +

+
+ + +
+
+ + +
+

Seed Phrase

+ +
+
+

Pass Phrase

+ +
+
+

Seed Hex

+

+ Provides an alternative means of logging in if you don't have a seed + phrase. +

+ +
+ +

+ Disabling backup makes your account more secure by preventing anyone + from revealing your seed in the future, even if they've gained access to + your device. +

+
+ +
+

+ Disabling backup means you will not be able to access your seed phrase + anymore. + Make sure that you've copied your seed phrase and stored it in a safe + place before you proceed. +

+
+ + +
+
+
+
diff --git a/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.scss b/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.ts b/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.ts new file mode 100644 index 00000000..9433879c --- /dev/null +++ b/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.ts @@ -0,0 +1,50 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { AccountService } from '../../account.service'; + +@Component({ + selector: 'backup-seed-dialog', + templateUrl: './backup-seed-dialog.component.html', + styleUrls: ['./backup-seed-dialog.component.scss'], +}) +export class BackupSeedDialogComponent { + step = 1; + mnemonic?: string; + extraText?: string; + seedHex?: string; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { rootPublicKey: string }, + private accountService: AccountService + ) {} + + cancel(): void { + this.dialogRef.close(); + } + + showSecrets() { + if (!this.data.rootPublicKey) { + throw new Error('Root public key is required'); + } + + const { mnemonic, extraText, seedHex } = this.accountService.getAccountInfo( + this.data.rootPublicKey + ); + this.mnemonic = mnemonic; + this.extraText = extraText; + this.seedHex = seedHex; + this.step = 2; + } + + showDisableBackupConfirmation() { + this.step = 3; + } + + disableBackup() { + this.accountService.updateAccountInfo(this.data.rootPublicKey, { + exportDisabled: true, + }); + this.dialogRef.close(); + } +} diff --git a/src/app/grouped-account-select/grouped-account-select.component.html b/src/app/grouped-account-select/grouped-account-select.component.html new file mode 100644 index 00000000..2bd90a92 --- /dev/null +++ b/src/app/grouped-account-select/grouped-account-select.component.html @@ -0,0 +1,196 @@ +
+ +
+
+ Select an account +
+ + +
+
    +
  • +
    + + +
    +
  • +
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+ or +
+ diff --git a/src/app/grouped-account-select/grouped-account-select.component.scss b/src/app/grouped-account-select/grouped-account-select.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/grouped-account-select/grouped-account-select.component.ts b/src/app/grouped-account-select/grouped-account-select.component.ts new file mode 100644 index 00000000..8d79b4a4 --- /dev/null +++ b/src/app/grouped-account-select/grouped-account-select.component.ts @@ -0,0 +1,362 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { finalize, take } from 'rxjs/operators'; +import { + LoginMethod, + SubAccountMetadata, + UserProfile, +} from 'src/types/identity'; +import Swal from 'sweetalert2'; +import { isValid32BitUnsignedInt } from '../../lib/account-number'; +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 & { + rootPublicKey: string; + publicKey: string; + lastUsed?: boolean; + }; + +function sortAccounts(a: AccountViewModel, b: AccountViewModel) { + // sort accounts by last login timestamp DESC, + // secondarily by balance DESC + return ( + (b.lastLoginTimestamp ?? 0) - (a.lastLoginTimestamp ?? 0) || + b.balanceNanos - a.balanceNanos + ); +} + +@Component({ + selector: 'grouped-account-select', + templateUrl: './grouped-account-select.component.html', + styleUrls: ['./grouped-account-select.component.scss'], +}) +export class GroupedAccountSelectComponent implements OnInit { + @Output() onAccountSelect: EventEmitter = new EventEmitter(); + + /** + * Accounts are grouped by root public key. The root public key is the public + * key derived at account index 0 for a given seed phrase. + */ + accountGroups: Map< + string, + { + showRecoverSubAccountInput?: boolean; + accounts: AccountViewModel[]; + } + > = new Map(); + + /** + * Bound to a UI text input and used to recover a sub account. + */ + accountNumberToRecover = 0; + + /** + * UI loading state flag. + */ + loadingAccounts: boolean = false; + + justAddedPublicKey?: string; + + constructor( + public accountService: AccountService, + public globalVars: GlobalVarsService, + private backendApi: BackendAPIService, + public dialog: MatDialog + ) {} + + ngOnInit(): void { + this.initializeAccountGroups(); + } + + initializeAccountGroups() { + this.loadingAccounts = true; + const rootUserEntries = Object.entries( + this.accountService.getRootLevelUsers() + ); + const accountGroupsByRootKey = new Map< + string, + { + rootPublicKey: string; + publicKey: string; + accountNumber: number; + lastLoginTimestamp?: number; + }[] + >(); + + for (const [rootPublicKey, userInfo] of rootUserEntries) { + const accounts = !userInfo.isHidden + ? [ + { + rootPublicKey: rootPublicKey, + publicKey: rootPublicKey, + accountNumber: 0, + lastLoginTimestamp: userInfo.lastLoginTimestamp, + }, + ] + : []; + + const subAccounts = userInfo?.subAccounts ?? []; + + for (const subAccount of subAccounts) { + if (subAccount.isHidden) { + continue; + } + + const publicKeyBase58 = this.accountService.getAccountPublicKeyBase58( + rootPublicKey, + subAccount.accountNumber + ); + + accounts.push({ + rootPublicKey: rootPublicKey, + publicKey: publicKeyBase58, + accountNumber: subAccount.accountNumber, + lastLoginTimestamp: subAccount.lastLoginTimestamp, + }); + } + + if (accounts.length > 0) { + accountGroupsByRootKey.set(rootPublicKey, accounts); + } + } + + const profileKeysToFetch = Array.from(accountGroupsByRootKey.values()) + .flat() + .map((a) => a.publicKey); + + // Fetch profiles and balances so we can show usernames in the UI (if we have them) + this.backendApi + .GetUserProfiles(profileKeysToFetch) + .pipe( + take(1), + finalize(() => (this.loadingAccounts = false)) + ) + .subscribe((users) => { + const unorderedAccountGroups: typeof this.accountGroups = new Map(); + Array.from(accountGroupsByRootKey.entries()).forEach( + ([key, accounts]) => { + unorderedAccountGroups.set(key, { + accounts: accounts.map((account) => ({ + ...account, + ...users[account.publicKey], + })), + }); + } + ); + + // 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], + }); + } + }); + }); + } + + /** + * We need this to address angular's weird default sorting of Maps by key when + * iterating in the template. See this issue for details. We just want to + * preserve the natural order of the Map entries: + * https://github.com/angular/angular/issues/31420 + */ + keyValueSort() { + return 1; + } + + getLoginMethodIcon(loginMethod: LoginMethod = LoginMethod.DESO): string { + return { + [LoginMethod.DESO]: 'assets/logo-deso-mark.svg', + [LoginMethod.GOOGLE]: 'assets/google_logo.svg', + [LoginMethod.METAMASK]: 'assets/metamask.png', + }[loginMethod]; + } + + selectAccount(publicKey: string) { + this.accountService.updateAccountInfo(publicKey, { + lastLoginTimestamp: Date.now(), + }); + this.onAccountSelect.emit(publicKey); + } + + hideAccount(groupKey: string, account: AccountViewModel) { + // NOTE: if there is at least 1 sub account left in the group after hiding this account, + // the user only needs the account number to recover it. If there are no sub accounts left, + // the user needs the seed phrase + the account number to recover it. + const group = this.accountGroups.get(groupKey) ?? { + accounts: [], + }; + // get a copy of the underlying array so we can preview what it looks like when hiding this account + const hiddenPreview = group.accounts + .slice() + .filter((a) => a.accountNumber !== account.accountNumber); + + 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, + }); + 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; + } + } + } + }); + } + + addSubAccount( + rootPublicKey: string, + { accountNumber }: { accountNumber?: number } = {} + ) { + const addedAccountNumber = this.accountService.addSubAccount( + rootPublicKey, + { accountNumber } + ); + const publicKeyBase58 = this.accountService.getAccountPublicKeyBase58( + rootPublicKey, + addedAccountNumber + ); + // Check if this account has profile, balance, etc, and add it to the list. + this.backendApi + .GetUserProfiles([publicKeyBase58]) + .pipe(take(1)) + .subscribe((users) => { + const account = { + rootPublicKey: rootPublicKey, + publicKey: publicKeyBase58, + accountNumber: addedAccountNumber, + ...users[publicKeyBase58], + }; + + const group = this.accountGroups.get(rootPublicKey) ?? { + accounts: [], + }; + + // if the account is already in the list, don't add it again... + if (!group.accounts.find((a) => a.accountNumber === accountNumber)) { + 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; + }, 2000); + }); + } + + /** + * Shows and hides the "recover sub account" text input. + */ + toggleRecoverSubAccountForm(rootPublicKey: string) { + const group = this.accountGroups.get(rootPublicKey); + if (!group) { + return; + } + group.showRecoverSubAccountInput = !group.showRecoverSubAccountInput; + this.accountGroups.set(rootPublicKey, group); + } + + recoverSubAccount(event: SubmitEvent, rootPublicKey: string) { + event.preventDefault(); + + if (!isValid32BitUnsignedInt(this.accountNumberToRecover)) { + Swal.fire({ + title: 'Invalid Account Number', + html: `Please enter a valid account number.`, + }); + return; + } + + this.addSubAccount(rootPublicKey, { + accountNumber: this.accountNumberToRecover, + }); + } + + getAccountDisplayName(account: { username?: string; publicKey: string }) { + return account.username ?? account.publicKey; + } + + isMetaMaskAccountGroup(rootPublicKey: string) { + const rootAccount = this.accountService.getAccountInfo(rootPublicKey); + return this.accountService.isMetamaskAccount(rootAccount); + } + + shouldShowExportSeedButton(rootPublicKey: string) { + const rootAccount = this.accountService.getAccountInfo(rootPublicKey); + return !rootAccount.exportDisabled; + } + + exportSeed(rootPublicKey: string) { + this.dialog.open(BackupSeedDialogComponent, { + data: { rootPublicKey }, + }); + } +} 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 new file mode 100644 index 00000000..708f08e1 --- /dev/null +++ b/src/app/grouped-account-select/recovery-secret/recovery-secret.component.html @@ -0,0 +1,26 @@ +
+ {{ this.isRevealed ? secret : maskedSecret }} +
+
+ + +
diff --git a/src/app/grouped-account-select/recovery-secret/recovery-secret.component.scss b/src/app/grouped-account-select/recovery-secret/recovery-secret.component.scss new file mode 100644 index 00000000..e69de29b 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 new file mode 100644 index 00000000..f8181fd5 --- /dev/null +++ b/src/app/grouped-account-select/recovery-secret/recovery-secret.component.ts @@ -0,0 +1,31 @@ +import { Component, Input, OnInit } from '@angular/core'; + +@Component({ + selector: 'recovery-secret', + templateUrl: './recovery-secret.component.html', + styleUrls: ['./recovery-secret.component.scss'], +}) +export class RecoverySecretComponent implements OnInit { + @Input() secret = ''; + + maskedSecret = ''; + isRevealed = false; + copySuccess = false; + + ngOnInit(): void { + this.maskedSecret = this.secret.replace(/\S/g, '*'); + } + + copySecret() { + window.navigator.clipboard.writeText(this.secret).then(() => { + this.copySuccess = true; + setTimeout(() => { + this.copySuccess = false; + }, 1500); + }); + } + + toggleRevealSecret() { + this.isRevealed = !this.isRevealed; + } +} 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..337d4fac --- /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.ts b/src/app/grouped-account-select/remove-account-dialog/remove-account-dialog.component.ts new file mode 100644 index 00000000..18e88b58 --- /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 = 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); + } +} diff --git a/src/app/icons/icons.module.ts b/src/app/icons/icons.module.ts index 3d3b5aa0..0dfb9723 100644 --- a/src/app/icons/icons.module.ts +++ b/src/app/icons/icons.module.ts @@ -23,6 +23,8 @@ import { CreditCard, DollarSign, ExternalLink, + Eye, + EyeOff, Feather, Flag, FolderMinus, @@ -48,6 +50,7 @@ import { RefreshCw, Repeat, RotateCw, + Save, Search, Send, Settings, @@ -117,6 +120,8 @@ const icons = { DollarSign, Diamond, ExternalLink, + Eye, + EyeOff, Feather, Flag, FolderMinus, @@ -152,6 +157,7 @@ const icons = { RefreshCw, Repeat, RotateCw, + Save, Search, Send, Settings, 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-options/log-in-options.component.html b/src/app/log-in-options/log-in-options.component.html index 0bd186ed..a36c34f6 100644 --- a/src/app/log-in-options/log-in-options.component.html +++ b/src/app/log-in-options/log-in-options.component.html @@ -1,4 +1,4 @@ - 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: +

): number { return candidate; } - // At most we look back 500 numbers. This is a bit arbitrary... but the - // number of values could *technically* be 2^32 - 1, so we just limit the + // If we get here, it means a user must have manually entered the maximum + // allowed account number into the recover account field, and subsequently + // tried to add a new account. We'll get an error if we try to use the next + // incremented value (which is too big). We cannot use a static default + // fallback value because it could conflict with an account number we already + // have stored in local storage, so we look back for the first gap in the + // numbers. At most we look back 500 numbers. This is a bit arbitrary... but + // the number of values could *technically* be 2^32 - 1, so we just limit the // number of iterations to some reasonable value. The reason we look back for - // the highest available number instead of picking the lowest number is that - // the lowest number is more likely to have been used in the past and we're - // aiming to get a fresh wallet. + // the highest available number instead of picking the lowest available number + // is that the lowest number is more likely to have been used in the past and + // we're aiming to get a fresh/unused set of keys. const maxLookBack = Math.max(sorted.length - 500, 0); let nextExpectedValueInSequence = currentHighestAccountNumber - 1; for (let i = sorted.length - 2; i >= maxLookBack; i--) { diff --git a/src/styles.scss b/src/styles.scss index 4082bdab..cd4eadb1 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -164,6 +164,7 @@ $colors: ( // SPACING UNITS //------------------------------- $spacing-units: ( + 'auto': auto, 'none': 0, 'xsmall': 4px, 'small': 8px, @@ -470,7 +471,7 @@ a, @include color('blue', 'base', 'background-color'); @include color('neutral', 'white', 'color'); &:hover { - @include color('blue', 'darker', 'background-color'); + @include color('blue', 'dark', 'background-color'); } } .button--primary--outline { @@ -480,6 +481,7 @@ a, @include color('neutral', 'transparent', 'background-color'); @include color('text', 'lighter', 'color'); &:hover { + @include color('neutral', 'white', 'color'); @include color('blue', 'darker', 'background-color'); } } @@ -617,6 +619,17 @@ a, flex-direction: row-reverse; } } + +//------------------------------- +// POSITION +//------------------------------- +.relative { + position: relative; +} +.absolute { + position: absolute; +} + //------------------------------- // LAYOUT //------------------------------- @@ -790,6 +803,7 @@ a, max-height: 245px; overflow: scroll; border: 1px solid; + scroll-behavior: smooth; @include spacing('padding', 'medium'); @include spacing('padding-top', 'small'); @include color('blue', 'dark', 'border-color'); @@ -806,7 +820,6 @@ a, @include spacing('padding-right', 'medium'); @include spacing('padding-top', 'small'); @include spacing('padding-bottom', 'small'); - @include spacing('margin-top', 'small'); @include color('text', 'lighter', 'color'); @include color('blue', 'darker', 'background-color'); @include color('blue', 'dark', 'border-color'); @@ -817,6 +830,11 @@ a, @include color('blue', 'light', 'border-color'); cursor: pointer; } + + &--just-added { + @include color('blue', 'light', 'border-color'); + @include color('blue', 'dark', 'background-color'); + } } //------------------------------- // SECTION = Create Seed Phrase @@ -1162,3 +1180,47 @@ a, background-color: rgba(0, 0, 0, 0.8) !important; } } + +.styleless-button { + border: none; + background: none; + padding: 0; + margin: 0; + cursor: pointer; +} + +//------------------------- +// Material Dialog +//------------------------- +.cdk-overlay-dark-backdrop { + background-color: rgba(0, 0, 0, 0.8) !important; +} +.dialog { + background-color: lighten(#040609, 10%); + border: 1px solid darkgray; + border-radius: 4px; + padding: 8px; +} +.dialog__header { + position: relative; + @include color('neutral', 'white', 'color'); +} +.dialog__x { + border: none; + background: none; + position: absolute; + top: 0; + right: 0; + margin: 0; + padding: 0; + cursor: pointer; + @include color('neutral', 'white', 'color'); +} +.dialog__title { + @include font-size('large'); + @include color('neutral', 'white', 'color'); + @include font-weight('bold'); +} +.dialog__body { + padding: 12px 0; +} diff --git a/src/types/identity.ts b/src/types/identity.ts index 06f71ba0..d39cd0a5 100644 --- a/src/types/identity.ts +++ b/src/types/identity.ts @@ -28,6 +28,13 @@ export interface PrivateUserInfo extends AccountMetadata { */ subAccounts?: SubAccountMetadata[]; + /** + * Determines whether we display the "Back up your seed" button in the UI. We + * show it by default for all users, but we hide it for users who have + * explicitly disabled it. + */ + exportDisabled?: boolean; + /** DEPRECATED in favor of loginMethod */ google?: boolean; } From 1c71117d6b209e294eaf877559a7003761ce8feb Mon Sep 17 00:00:00 2001 From: Ed Moss Date: Tue, 26 Sep 2023 22:49:01 -0600 Subject: [PATCH 3/6] Cleanup UI for multiple account mgmt (#280) * cleanup UI for multiple accout mgmt * run prettier * fix margin on metamask account * formatting * fix mobile * fix scrolling on overlay --------- Co-authored-by: Lazy Nina <> --- src/app/get-deso/get-deso.component.html | 18 +- .../backup-seed-dialog.component.html | 98 +++++----- .../grouped-account-select.component.html | 168 ++++++++++-------- .../recovery-secret.component.html | 26 +-- .../remove-account-dialog.component.html | 36 ++-- .../sign-up-get-starter-deso.component.html | 4 +- src/app/sign-up/sign-up.component.html | 14 +- src/styles.scss | 135 +++++++++++--- 8 files changed, 310 insertions(+), 189 deletions(-) diff --git a/src/app/get-deso/get-deso.component.html b/src/app/get-deso/get-deso.component.html index 5f794ec4..e18ed02b 100644 --- a/src/app/get-deso/get-deso.component.html +++ b/src/app/get-deso/get-deso.component.html @@ -24,7 +24,7 @@

Get starter $DESO

*ngIf="!alternativeOptionsEnabled && captchaAvailable" class="padding-bottom--2xlarge text--center" > -
+
Get starter $DESO
Complete a captcha to get free $DESO

- Prove you're not a robot 🤖 and we'll
send you a small amount - of $DESO that will last
you up to thousands of on-chain - transactions. + Prove you're not a robot 🤖 and we'll
+ send you a small amount of free $DESO that will last
+ you up to thousands of on-chain transactions.

Get starter $DESO 1. Get DESO for free by verifying your phone number

- We'll send you a small amount of $DESO that will last
you up to - thousands of on-chain transactions. + We'll send you a small amount of $DESO that will last
+ you up to thousands of on-chain transactions.

diff --git a/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html b/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html index c5ddc562..daac2105 100644 --- a/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html +++ b/src/app/grouped-account-select/backup-seed-dialog/backup-seed-dialog.component.html @@ -3,90 +3,98 @@ -

Backup DeSo Seed

+

+ Backup DeSo Seed +

+

Disable Backup

-
-

+

+

Your seed phrase is the only way to recover your DeSo account. If you lose your seed phrase, you will lose access to your DeSo account. Store it in a safe and secure place.

-

- DO NOT share your seed phrase with anyone! Support agents will never - request this. +

+ DO NOT share your seed phrase with anyone! Developers and support agents + will never request this.

-
+
-

Seed Phrase

+

DeSo Seed Phrase:

-

Pass Phrase

+

DeSo Pass Phrase:

-
-

Seed Hex

-

+

+

Seed Hex:

+

Provides an alternative means of logging in if you don't have a seed phrase.

- -

- Disabling backup makes your account more secure by preventing anyone - from revealing your seed in the future, even if they've gained access to - your device. -

+

+ Disable Backup +

+

+ Disabling backup makes your account more secure by preventing anyone + from revealing your seed in the future, even if they've gained access + to your device. +

+ +
-
-

+

+

Disabling backup means you will not be able to access your seed phrase anymore. - Make sure that you've copied your seed phrase and stored it in a safe - place before you proceed.

-
+
+ Make sure that you've copied your seed phrase and stored it in a safe + place before you proceed. +
+
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 2bd90a92..bcaf43e6 100644 --- a/src/app/grouped-account-select/grouped-account-select.component.html +++ b/src/app/grouped-account-select/grouped-account-select.component.html @@ -1,8 +1,4 @@ -
+
@@ -15,7 +11,8 @@ let i = index " > -
+
+

Account Group

    -
    +
    +
    + +
    +
+ + +
+
+ +
- - -
-
- - - + +
-
- - - -
-
+
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 708f08e1..3324136d 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 @@ -1,26 +1,28 @@
{{ this.isRevealed ? secret : maskedSecret }}
-
+
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 337d4fac..03f43252 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 @@ -5,49 +5,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!

+

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

-

+

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

- Account number: {{ this.data.accountNumber }}Account number:  {{ + this.data.accountNumber + }}
-
+
+ -
diff --git a/src/app/sign-up-get-starter-deso/sign-up-get-starter-deso.component.html b/src/app/sign-up-get-starter-deso/sign-up-get-starter-deso.component.html index 821de89f..c7460a7f 100644 --- a/src/app/sign-up-get-starter-deso/sign-up-get-starter-deso.component.html +++ b/src/app/sign-up-get-starter-deso/sign-up-get-starter-deso.component.html @@ -79,8 +79,8 @@ />

Get free $DESO

- We'll send you a small amount of $DESO that will last
you up to - thousands of on-chain transactions. + We'll send you a small amount of $DESO that will last  
+ you up to thousands of on-chain transactions.

diff --git a/src/app/sign-up/sign-up.component.html b/src/app/sign-up/sign-up.component.html index 810ca6a3..fb5e4f53 100644 --- a/src/app/sign-up/sign-up.component.html +++ b/src/app/sign-up/sign-up.component.html @@ -24,8 +24,8 @@

Safely store your DeSo seed phrase

- Write, download, print, or copy it somewhere
safe and secure that - only you have access to. + Write, download, print, or copy it somewhere
+ safe and secure that only you have access to.

@@ -275,8 +275,9 @@

Safely store your DeSo seed phrase

- If you lose your DeSo seed phrase your account will be lost forever.
Never - enter it anywhere outside of https://identity.deso.org + If you lose your DeSo seed phrase your account will be lost forever. +
+ Never enter it anywhere outside of https://identity.deso.org
- If you lose your DeSo seed phrase your account will be lost forever.
Never - enter it anywhere outside of https://identity.deso.org + If you lose your DeSo seed phrase your account will be lost forever. +
+ Never enter it anywhere outside of https://identity.deso.org
diff --git a/src/styles.scss b/src/styles.scss index cd4eadb1..93defc6b 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -354,7 +354,8 @@ h6 { } } -p { +p, +label { @include spacing('margin', 'none'); font-weight: normal; line-height: 1.5; @@ -466,6 +467,15 @@ a, text-decoration: none; } } + +.button--icon { + border: none; + background: none; + padding: 0; + margin: 0; + cursor: pointer; +} + .button--primary { @extend %button; @include color('blue', 'base', 'background-color'); @@ -538,6 +548,13 @@ a, //------------------------------- // BUTTONS = Sizes //------------------------------- +.button--xsmall { + @include font-size('xsmall'); + @include spacing('padding-top', 'xsmall'); + @include spacing('padding-bottom', 'xsmall'); + @include spacing('padding-left', 'medium'); + @include spacing('padding-right', 'medium'); +} .button--small { @include font-size('small'); @include spacing('padding-top', 'small'); @@ -801,10 +818,8 @@ a, flex-direction: column; gap: 8px; max-height: 245px; - overflow: scroll; border: 1px solid; scroll-behavior: smooth; - @include spacing('padding', 'medium'); @include spacing('padding-top', 'small'); @include color('blue', 'dark', 'border-color'); @include border-radius('base'); @@ -816,6 +831,8 @@ a, display: flex; align-items: center; @include border-radius('pill'); + @include spacing('margin-left', 'medium'); + @include spacing('margin-right', 'base'); @include spacing('padding-left', 'small'); @include spacing('padding-right', 'medium'); @include spacing('padding-top', 'small'); @@ -827,7 +844,7 @@ a, @include font-size('small'); &:hover { - @include color('blue', 'light', 'border-color'); + @include color('text', 'lighter', 'border-color'); cursor: pointer; } @@ -836,6 +853,51 @@ a, @include color('blue', 'dark', 'background-color'); } } +.section--accounts__remove { + border-radius: 99px; + position: relative; + width: 26px; + height: 26px; + @include color('blue', 'darker', 'background-color'); + @include spacing('right', 'medium'); + @include spacing('padding', 'xsmall'); + i-feather[name='power'] { + @include color('text', 'base', 'color'); + width: 16px; + height: 16px; + } + &:hover { + @include color('red', 'dark', 'background-color'); + cursor: pointer; + i-feather[name='power'] { + @include color('red', 'lighter', 'color'); + } + } +} +.section--accounts__recover { + border: 1px solid; + border-radius: 20px; + @include color('blue', 'dark', 'border-color'); + @include spacing('padding', 'medium'); +} +.section--accounts__recover--actions { + display: flex; + flex-direction: row; + @include spacing('gap', 'small'); + @include media(mobile) { + flex-direction: column; + } +} +//------------------------------- +// SECTION = Sub Actions +//------------------------------- +.section--subactions { + border-top: 1px solid; + @include spacing('padding', 'medium'); + @include color('blue', 'dark', 'border-color'); + @include spacing('padding-top', 'medium'); + @include spacing('margin-top', 'small'); +} //------------------------------- // SECTION = Create Seed Phrase //------------------------------- @@ -862,6 +924,11 @@ a, display: flex; align-items: center; @include spacing('gap', 'small'); + @include media(mobile) { + display: flex; + flex-direction: column; + width: 100%; + } } .section--seed__advanced { border-top: 1px solid; @@ -881,6 +948,17 @@ a, @include color('blue', 'dark', 'border-color'); @include border-radius('base'); } +.section--starter__option--captcha { + border: 1px solid; + @include spacing('padding', 'large'); + @include color('blue', 'dark', 'border-color'); + @include border-radius('base'); + @include media(mobile) { + padding: 0; + border-radius: none; + border: none; + } +} //------------------------------- // SECTION = Phone Page //------------------------------- @@ -930,6 +1008,10 @@ a, @include spacing('padding', 'medium'); @include color('blue', 'dark', 'border-color'); } +.input--textarea--small { + @include spacing('padding', 'small'); + @include font-size('small'); +} .input--phone { width: 100%; border: 1px solid; @@ -1048,6 +1130,8 @@ a, .warning--error { text-align: center; line-height: 1.5; + border: 1px solid; + @include color('red', 'dark', 'border-color'); @include border-radius('base'); @include font-size('small'); @include spacing('padding', 'small'); @@ -1137,14 +1221,16 @@ a, } .spinner-border { display: inline-block; - width: 2rem; - height: 2rem; + width: 30px; + height: 30px; vertical-align: text-bottom; border: 0.25em solid currentColor; border-right-color: transparent; border-radius: 50%; -webkit-animation: 0.75s linear infinite spinner-border; animation: 0.75s linear infinite spinner-border; + padding: 40px; + margin: 40px; } //------------------------- @@ -1180,26 +1266,25 @@ a, background-color: rgba(0, 0, 0, 0.8) !important; } } - -.styleless-button { - border: none; - background: none; - padding: 0; - margin: 0; - cursor: pointer; -} - //------------------------- // Material Dialog //------------------------- +.mat-mdc-dialog-container .mdc-dialog__surface { + background-color: transparent !important; +} +.cdk-overlay-pane { + overflow-y: scroll; +} .cdk-overlay-dark-backdrop { - background-color: rgba(0, 0, 0, 0.8) !important; + background-color: rgba(0, 0, 0, 0.5) !important; + backdrop-filter: blur(5px); } .dialog { - background-color: lighten(#040609, 10%); - border: 1px solid darkgray; - border-radius: 4px; - padding: 8px; + border-radius: 20px; + max-width: 600px; + box-shadow: 0 0 10px black; + @include color('blue', 'darker', 'background-color'); + @include spacing('padding', 'base'); } .dialog__header { position: relative; @@ -1214,13 +1299,19 @@ a, margin: 0; padding: 0; cursor: pointer; - @include color('neutral', 'white', 'color'); + @include color('text', 'base', 'color'); + &:hover { + @include color('neutral', 'white', 'color'); + } } .dialog__title { @include font-size('large'); @include color('neutral', 'white', 'color'); - @include font-weight('bold'); + @include font-weight('medium'); } .dialog__body { padding: 12px 0; } +//------------------------- +// Material Dialog +//------------------------- From c252107070a05cd14f70c55aefedc07b88e8e1fd Mon Sep 17 00:00:00 2001 From: Lazy Nina <> Date: Wed, 27 Sep 2023 00:54:19 -0400 Subject: [PATCH 4/6] auto, not scroll for overflow-y on overlay --- src/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles.scss b/src/styles.scss index 93defc6b..c070a89b 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1273,7 +1273,7 @@ a, background-color: transparent !important; } .cdk-overlay-pane { - overflow-y: scroll; + overflow-y: auto; } .cdk-overlay-dark-backdrop { background-color: rgba(0, 0, 0, 0.5) !important; From 00763db737dac606938fc2d78e19b0785d009ecc Mon Sep 17 00:00:00 2001 From: Jackson Dean Date: Thu, 28 Sep 2023 21:59:19 -0700 Subject: [PATCH 5/6] fix sub account key generation (#281) --- src/app/account.service.ts | 19 ++++++++++++------- src/app/auth/google/google.component.ts | 14 ++++++-------- src/app/crypto.service.ts | 18 ++++++++++-------- src/app/log-in-seed/log-in-seed.component.ts | 13 +++++++------ src/app/sign-up/sign-up.component.ts | 4 +++- 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/app/account.service.ts b/src/app/account.service.ts index 02545134..84d5b8b8 100644 --- a/src/app/account.service.ts +++ b/src/app/account.service.ts @@ -141,9 +141,12 @@ export class AccountService { ); if (foundAccount) { - const keychain = this.cryptoService.getSubAccountKeychain( - rootUser.seedHex, - foundAccount.accountNumber + const keychain = this.cryptoService.mnemonicToKeychain( + rootUser.mnemonic, + { + extraText: rootUser.extraText, + accountNumber: foundAccount.accountNumber, + } ); const subAccountSeedHex = this.cryptoService.keychainToSeedHex(keychain); @@ -1322,10 +1325,12 @@ export class AccountService { ); } - const parentSeedHex = parentAccount.seedHex; - const childKey = this.cryptoService.getSubAccountKeychain( - parentSeedHex, - accountNumber + const childKey = this.cryptoService.mnemonicToKeychain( + parentAccount.mnemonic, + { + accountNumber, + extraText: parentAccount.extraText, + } ); const ec = new EC('secp256k1'); const keyPair = ec.keyFromPrivate(childKey.privateKey); diff --git a/src/app/auth/google/google.component.ts b/src/app/auth/google/google.component.ts index 1d2eccdd..0ce22676 100644 --- a/src/app/auth/google/google.component.ts +++ b/src/app/auth/google/google.component.ts @@ -83,10 +83,9 @@ export class GoogleComponent implements OnInit { const mnemonic = fileContents.mnemonic; const extraText = fileContents.extraText; const network = fileContents.network; - const keychain = this.cryptoService.mnemonicToKeychain( - mnemonic, - extraText - ); + const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, { + extraText, + }); this.publicKey = this.accountService.addUser( keychain, @@ -139,10 +138,9 @@ export class GoogleComponent implements OnInit { this.googleDrive .uploadFile(this.fileName(), JSON.stringify(userInfo)) .subscribe(() => { - const keychain = this.cryptoService.mnemonicToKeychain( - mnemonic, - extraText - ); + const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, { + extraText, + }); this.publicKey = this.accountService.addUser( keychain, mnemonic, diff --git a/src/app/crypto.service.ts b/src/app/crypto.service.ts index 07810bc0..1c451751 100644 --- a/src/app/crypto.service.ts +++ b/src/app/crypto.service.ts @@ -138,20 +138,22 @@ export class CryptoService { mnemonicToKeychain( mnemonic: string, - extraText?: string, - nonStandard?: boolean + { + extraText, + nonStandard, + accountNumber = 0, + }: { + extraText?: string; + nonStandard?: boolean; + accountNumber?: number; + } = {} ): HDNode { const seed = bip39.mnemonicToSeedSync(mnemonic, extraText); - return generateSubAccountKeys(seed, 0, { + return generateSubAccountKeys(seed, accountNumber, { nonStandard, }); } - getSubAccountKeychain(masterSeedHex: string, accountIndex: number): HDNode { - const seedBytes = Buffer.from(masterSeedHex, 'hex'); - return generateSubAccountKeys(seedBytes, accountIndex); - } - keychainToSeedHex(keychain: HDNode): string { return keychain.privateKey.toString('hex'); } 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 f23bd05e..0a2ff971 100644 --- a/src/app/log-in-seed/log-in-seed.component.ts +++ b/src/app/log-in-seed/log-in-seed.component.ts @@ -72,14 +72,15 @@ export class LogInSeedComponent implements OnInit { return; } - const keychain = this.cryptoService.mnemonicToKeychain( - mnemonic, - extraText - ); + const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, { + extraText, + }); const keychainNonStandard = this.cryptoService.mnemonicToKeychain( mnemonic, - extraText, - true + { + extraText, + nonStandard: true, + } ); userPublicKey = this.accountService.addUser( keychain, diff --git a/src/app/sign-up/sign-up.component.ts b/src/app/sign-up/sign-up.component.ts index f808c446..4d9cdae8 100644 --- a/src/app/sign-up/sign-up.component.ts +++ b/src/app/sign-up/sign-up.component.ts @@ -111,7 +111,9 @@ export class SignUpComponent implements OnInit, OnDestroy { const network = this.globalVars.network; const mnemonic = this.mnemonicCheck; const extraText = this.extraTextCheck; - const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, extraText); + const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, { + extraText, + }); this.seedHex = this.cryptoService.keychainToSeedHex(keychain); this.publicKeyAdded = this.accountService.addUser( keychain, From a3e248b5e8979ba8956b9141aeedb5cfc0617764 Mon Sep 17 00:00:00 2001 From: Lazy Nina <> Date: Thu, 5 Oct 2023 22:14:31 -0700 Subject: [PATCH 6/6] try using noble, not working --- package-lock.json | 31 +++++++ package.json | 2 + src/app/account.service.ts | 44 +++++----- src/app/approve/approve.component.ts | 31 +++---- src/app/auth/google/google.component.ts | 33 +++---- src/app/backend-api.service.ts | 46 ++-------- .../buy-deso-complete.component.ts | 7 +- .../buy-deso-heroswap.component.ts | 15 ++-- .../buy-deso/buy-deso/buy-deso.component.ts | 15 ++-- .../buy-or-send-deso.component.ts | 19 +++-- src/app/crypto.service.ts | 33 +++++-- src/app/derive/derive.component.ts | 15 ++-- src/app/get-deso/get-deso.component.ts | 20 ++--- src/app/identity.service.ts | 85 ++++++++++--------- .../jumio-error/jumio-error.component.ts | 11 ++- .../jumio-success/jumio-success.component.ts | 10 ++- src/app/log-in/log-in.component.ts | 17 ++-- src/app/logout/logout.component.ts | 15 ++-- .../messaging-group.component.ts | 15 ++-- .../sign-up-get-starter-deso.component.ts | 13 +-- .../sign-up-metamask.component.ts | 18 ++-- src/app/sign-up/sign-up.component.ts | 15 ++-- 22 files changed, 277 insertions(+), 233 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8c297fe4..1582c3b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "@ethereumjs/vm": "^5.9.0", "@metamask/post-message-stream": "^6.1.2", "@metamask/providers": "^11.1.1", + "@noble/ciphers": "^0.3.0", + "@noble/secp256k1": "^1.7.1", "@types/lodash": "^4.14.182", "@types/sha256": "^0.2.0", "@types/sprintf-js": "^1.1.2", @@ -5412,6 +5414,14 @@ "semver": "bin/semver.js" } }, + "node_modules/@noble/ciphers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.3.0.tgz", + "integrity": "sha512-ldbrnOjmNRwFdXcTM6uXDcxpMIFrbzAWNnpBPp4oTJTFF0XByGD6vf45WrehZGXRQTRVV+Zm8YP+EgEf+e4cWA==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/curves": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.0.0.tgz", @@ -5448,6 +5458,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -23220,6 +23241,11 @@ "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", "dev": true }, + "@noble/ciphers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.3.0.tgz", + "integrity": "sha512-ldbrnOjmNRwFdXcTM6uXDcxpMIFrbzAWNnpBPp4oTJTFF0XByGD6vf45WrehZGXRQTRVV+Zm8YP+EgEf+e4cWA==" + }, "@noble/curves": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.0.0.tgz", @@ -23240,6 +23266,11 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" }, + "@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index b7980733..6f4d7260 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "@ethereumjs/vm": "^5.9.0", "@metamask/post-message-stream": "^6.1.2", "@metamask/providers": "^11.1.1", + "@noble/ciphers": "^0.3.0", + "@noble/secp256k1": "^1.7.1", "@types/lodash": "^4.14.182", "@types/sha256": "^0.2.0", "@types/sprintf-js": "^1.1.2", diff --git a/src/app/account.service.ts b/src/app/account.service.ts index 84d5b8b8..150516f8 100644 --- a/src/app/account.service.ts +++ b/src/app/account.service.ts @@ -177,15 +177,15 @@ export class AccountService { * this to look up the account number and the seed from the public key. */ private updateSubAccountReverseLookupMap({ - lookupKey, - accountNumber, - }: SubAccountReversLookupEntry) { + lookupKey, + accountNumber, + }: SubAccountReversLookupEntry) { const keyMap = this.getSubAccountReverseLookupMap(); const subAccountPublicKey = this.getAccountPublicKeyBase58( lookupKey, accountNumber ); - keyMap[subAccountPublicKey] = { lookupKey, accountNumber }; + keyMap[subAccountPublicKey] = {lookupKey, accountNumber}; window.localStorage.setItem( SUB_ACCOUNT_REVERSE_LOOKUP_KEY, @@ -193,7 +193,7 @@ export class AccountService { ); } - getEncryptedUsers(): { [key: string]: PublicUserInfo } { + async getEncryptedUsers(): Promise<{ [key: string]: PublicUserInfo }> { const hostname = this.globalVars.hostname; const rootUsers = this.getRootLevelUsers(); const publicUsers: { [key: string]: PublicUserInfo } = {}; @@ -206,13 +206,13 @@ export class AccountService { continue; } - const encryptedSeedHex = this.cryptoService.encryptSeedHex( + const encryptedSeedHex = await this.cryptoService.encryptSeedHex( privateUser.seedHex, hostname ); let encryptedMessagingKeyRandomness: string | undefined; if (privateUser.messagingKeyRandomness) { - encryptedMessagingKeyRandomness = this.cryptoService.encryptSeedHex( + encryptedMessagingKeyRandomness = await this.cryptoService.encryptSeedHex( privateUser.messagingKeyRandomness, hostname ); @@ -245,13 +245,13 @@ export class AccountService { // unique seed hex that can be used for signing transactions, as well as a // unique accessLevel hmac. const subAccounts = privateUser.subAccounts || []; - subAccounts.forEach((subAccount) => { + for (const subAccount of subAccounts) { const subAccountPublicKey = this.getAccountPublicKeyBase58( rootPublicKey, subAccount.accountNumber ); const accountInfo = this.getAccountInfo(subAccountPublicKey); - const subAccountEncryptedSeedHex = this.cryptoService.encryptSeedHex( + const subAccountEncryptedSeedHex = await this.cryptoService.encryptSeedHex( accountInfo.seedHex, hostname ); @@ -265,7 +265,7 @@ export class AccountService { encryptedSeedHex: subAccountEncryptedSeedHex, accessLevelHmac: subAccountAccessLevelHmac, }; - }); + } } return publicUsers; @@ -319,7 +319,7 @@ export class AccountService { let jwt = ''; let derivedJwt = ''; const numDaysBeforeExpiration = expirationDays || 30; - const options = { expiration: `${numDaysBeforeExpiration} days` }; + const options = {expiration: `${numDaysBeforeExpiration} days`}; if (!derivedPublicKeyBase58CheckInput) { const derivedKeyData = this.generateDerivedKey(network); derivedPublicKeyBase58Check = derivedKeyData.derivedPublicKeyBase58Check; @@ -426,7 +426,7 @@ export class AccountService { } await this.metamaskService.connectWallet(); } - const { signature } = + const {signature} = await this.metamaskService.signMessageWithMetamaskAndGetEthAddress( accessBytesHex ); @@ -482,7 +482,7 @@ export class AccountService { 'raw', 'pem' ); - const jwt = jsonwebtoken.sign({ appPublicKey }, encodedPrivateKey, { + const jwt = jsonwebtoken.sign({appPublicKey}, encodedPrivateKey, { algorithm: 'ES256', expiresIn: '30 minutes', }); @@ -605,7 +605,7 @@ export class AccountService { network, loginMethod, version: PrivateUserVersion.V2, - ...(lastLoginTimestamp && { lastLoginTimestamp }), + ...(lastLoginTimestamp && {lastLoginTimestamp}), }); } @@ -830,7 +830,7 @@ export class AccountService { ); } try { - const { message, signature, publicEthAddress } = + const {message, signature, publicEthAddress} = await this.metamaskService.signMessageWithMetamaskAndGetEthAddress( randomnessString ); @@ -918,7 +918,7 @@ export class AccountService { const decryptedHexes: { [key: string]: any } = {}; for (const encryptedHex of encryptedHexes) { const encryptedBytes = new Buffer(encryptedHex, 'hex'); - const opts = { legacy: true }; + const opts = {legacy: true}; try { decryptedHexes[encryptedHex] = ecies .decrypt(privateKeyBuffer, encryptedBytes, opts) @@ -957,7 +957,7 @@ export class AccountService { // If message was encrypted using public key, check the sender to determine if message is decryptable. try { if (!encryptedMessage.IsSender) { - const opts = { legacy: true }; + const opts = {legacy: true}; decryptedHexes[encryptedMessage.EncryptedHex] = ecies .decrypt(privateKeyBuffer, encryptedBytes, opts) .toString(); @@ -1115,7 +1115,7 @@ export class AccountService { if ( privateUsers[publicKey]?.derivedPublicKeyBase58Check && privateUsers[publicKey]?.derivedPublicKeyBase58Check !== - userInfo.derivedPublicKeyBase58Check + userInfo.derivedPublicKeyBase58Check ) { const previousUserInfo = privateUsers[publicKey]; const archivedUserData = JSON.parse( @@ -1247,7 +1247,7 @@ export class AccountService { // account, but for historical reasons its public key is used to index the // main users map in local storage. if (options.accountNumber === 0) { - this.updateAccountInfo(rootPublicKey, { isHidden: false }); + this.updateAccountInfo(rootPublicKey, {isHidden: false}); return 0; } @@ -1264,8 +1264,8 @@ export class AccountService { const foundAccountIndex = typeof options.accountNumber === 'number' ? subAccounts.findIndex( - (a) => a.accountNumber === options.accountNumber - ) + (a) => a.accountNumber === options.accountNumber + ) : -1; const accountNumbers = new Set(subAccounts.map((a) => a.accountNumber)); const accountNumber = @@ -1301,7 +1301,7 @@ export class AccountService { ); } - this.updateAccountInfo(rootPublicKey, { subAccounts: newSubAccounts }); + this.updateAccountInfo(rootPublicKey, {subAccounts: newSubAccounts}); return accountNumber; } diff --git a/src/app/approve/approve.component.ts b/src/app/approve/approve.component.ts index b8ed327d..9b6911d8 100644 --- a/src/app/approve/approve.component.ts +++ b/src/app/approve/approve.component.ts @@ -77,7 +77,8 @@ export class ApproveComponent implements OnInit { public globalVars: GlobalVarsService, private signingService: SigningService, private backendApi: BackendAPIService - ) {} + ) { + } ngOnInit(): void { this.activatedRoute.queryParams.subscribe((params) => { @@ -95,11 +96,11 @@ export class ApproveComponent implements OnInit { }); } - onCancel(): void { - this.finishFlow(); + async onCancel(): Promise { + await this.finishFlow(); } - onSubmit(): void { + async onSubmit(): Promise { const account = this.accountService.getAccountInfo(this.publicKey); const isDerived = this.accountService.isMetamaskAccount(account); const signedTransactionHex = this.signingService.signTransaction( @@ -107,12 +108,12 @@ export class ApproveComponent implements OnInit { this.transactionHex, isDerived ); - this.finishFlow(signedTransactionHex); + await this.finishFlow(signedTransactionHex); } - finishFlow(signedTransactionHex?: string): void { + async finishFlow(signedTransactionHex?: string): Promise { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), signedTransactionHex, }); } @@ -400,23 +401,23 @@ export class ApproveComponent implements OnInit { exchangeRateCoinsToSellPerCoinToBuy === 0 ? `using ${sellingCoin} at any exchange rate` : `at an exchange rate of ${this.toFixedLengthDecimalString( - exchangeRateCoinsToSellPerCoinToBuy - )} ` + `${sellingCoin} per coin bought`; + exchangeRateCoinsToSellPerCoinToBuy + )} ` + `${sellingCoin} per coin bought`; const exchangeRateCoinsToBuyPerCoinsToSellPhrase = exchangeRateCoinsToSellPerCoinToBuy === 0 ? `for ${buyingCoin} at any exchange rate` : `at an exchange rate of ${this.toFixedLengthDecimalString( - 1 / exchangeRateCoinsToSellPerCoinToBuy - )} ` + `${buyingCoin} per coin sold`; + 1 / exchangeRateCoinsToSellPerCoinToBuy + )} ` + `${buyingCoin} per coin sold`; const daoCoinLimitOrderFillTypePhrase = daoCoinLimitOrderFillType === '1' ? 'a Good-Till-Cancelled' : daoCoinLimitOrderFillType === '2' - ? 'an Immediate-Or-Cancel' - : daoCoinLimitOrderFillType === '3' - ? 'a Fill-Or-Kill' - : `an unknown fill type (${daoCoinLimitOrderFillType})`; + ? 'an Immediate-Or-Cancel' + : daoCoinLimitOrderFillType === '3' + ? 'a Fill-Or-Kill' + : `an unknown fill type (${daoCoinLimitOrderFillType})`; if (daoCoinLimitOrderOperationType === '1') { // -- ASK Order -- diff --git a/src/app/auth/google/google.component.ts b/src/app/auth/google/google.component.ts index 0ce22676..c264bc45 100644 --- a/src/app/auth/google/google.component.ts +++ b/src/app/auth/google/google.component.ts @@ -38,7 +38,8 @@ export class GoogleComponent implements OnInit { private zone: NgZone, private route: ActivatedRoute, private backendApi: BackendAPIService - ) {} + ) { + } copySeed(): void { this.textService.copyText(this.mnemonic); @@ -63,7 +64,7 @@ export class GoogleComponent implements OnInit { this.googleDrive.setAccessToken(accessToken); - this.googleDrive.listFiles(this.fileName()).subscribe((res) => { + this.googleDrive.listFiles(this.fileName()).subscribe(async (res) => { if (res.files.length > 0) { this.loadAccounts(res.files); } else { @@ -108,9 +109,9 @@ export class GoogleComponent implements OnInit { }); } - filesLoaded.subscribe(() => { + filesLoaded.subscribe(async () => { if (numLoaded === 1) { - this.finishFlow(false); + await this.finishFlow(false); } else { this.zone.run(() => { this.router.navigate(['/', RouteNames.LOG_IN], { @@ -154,7 +155,7 @@ export class GoogleComponent implements OnInit { }); } - finishFlow(signedUp: boolean): void { + async finishFlow(signedUp: boolean): Promise { this.globalVars.signedUp = signedUp; this.accountService.setAccessLevel( this.publicKey, @@ -163,11 +164,11 @@ export class GoogleComponent implements OnInit { ); if (this.globalVars.derive) { - this.router.navigate(['/', RouteNames.DERIVE], { + await this.router.navigate(['/', RouteNames.DERIVE], { queryParams: { publicKey: this.publicKey, transactionSpendingLimitResponse: - this.globalVars.transactionSpendingLimitResponse, + this.globalVars.transactionSpendingLimitResponse, deleteKey: this.globalVars.deleteKey || undefined, derivedPublicKey: this.globalVars.derivedPublicKey || undefined, expirationDays: this.globalVars.expirationDays || undefined, @@ -176,35 +177,35 @@ export class GoogleComponent implements OnInit { }); } else { if (!this.globalVars.getFreeDeso) { - this.login(signedUp); + await this.login(signedUp); } if (!signedUp) { this.backendApi .GetUsersStateless([this.publicKey], true, true) - .subscribe((res) => { + .subscribe(async (res) => { if (res?.UserList?.length) { if (res.UserList[0].BalanceNanos !== 0) { - this.login(signedUp); + await this.login(signedUp); return; } } - this.router.navigate(['/', RouteNames.GET_DESO], { - queryParams: { publicKey: this.publicKey, signedUp }, + await this.router.navigate(['/', RouteNames.GET_DESO], { + queryParams: {publicKey: this.publicKey, signedUp}, queryParamsHandling: 'merge', }); }); } else { - this.router.navigate(['/', RouteNames.GET_DESO], { - queryParams: { publicKey: this.publicKey, signedUp }, + await this.router.navigate(['/', RouteNames.GET_DESO], { + queryParams: {publicKey: this.publicKey, signedUp}, queryParamsHandling: 'merge', }); } } } - login(signedUp: boolean): void { + async login(signedUp: boolean): Promise { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKey, signedUp, }); diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts index 28ebd385..98d915a6 100644 --- a/src/app/backend-api.service.ts +++ b/src/app/backend-api.service.ts @@ -269,7 +269,8 @@ export class BackendAPIService { private signingService: SigningService, private accountService: AccountService, private globalVars: GlobalVarsService - ) {} + ) { + } getRoute(path: string): string { let endpoint = this.endpoint; @@ -305,7 +306,7 @@ export class BackendAPIService { const isDerived = this.accountService.isMetamaskAccount(account); const jwt = this.signingService.signJWT(account.seedHex, isDerived); - return this.post(path, { ...body, ...{ JWT: jwt } }); + return this.post(path, {...body, ...{JWT: jwt}}); } // Error parsing @@ -434,15 +435,15 @@ export class BackendAPIService { if (res.DerivedKeys.hasOwnProperty(derivedKey)) { derivedKeys[ res.DerivedKeys[derivedKey]?.DerivedPublicKeyBase58Check - ] = { + ] = { derivedPublicKeyBase58Check: - res.DerivedKeys[derivedKey]?.DerivedPublicKeyBase58Check, + res.DerivedKeys[derivedKey]?.DerivedPublicKeyBase58Check, ownerPublicKeyBase58Check: - res.DerivedKeys[derivedKey]?.OwnerPublicKeyBase58Check, + res.DerivedKeys[derivedKey]?.OwnerPublicKeyBase58Check, expirationBlock: res.DerivedKeys[derivedKey]?.ExpirationBlock, isValid: res.DerivedKeys[derivedKey]?.IsValid, transactionSpendingLimit: - res.DerivedKeys[derivedKey]?.TransactionSpendingLimit, + res.DerivedKeys[derivedKey]?.TransactionSpendingLimit, }; } } @@ -691,39 +692,6 @@ export class BackendAPIService { ); } - ExchangeBitcoin( - LatestBitcionAPIResponse: any, - BTCDepositAddress: string, - PublicKeyBase58Check: string, - BurnAmountSatoshis: number, - FeeRateSatoshisPerKB: number, - SignedHashes: string[], - Broadcast: boolean - ): Observable { - // Check if the user is logged in with a derived key and operating as the owner key. - const DerivedPublicKeyBase58Check = - this.accountService.getEncryptedUsers()[PublicKeyBase58Check] - ?.derivedPublicKeyBase58Check; - - const req = this.post('exchange-bitcoin', { - PublicKeyBase58Check, - DerivedPublicKeyBase58Check, - BurnAmountSatoshis, - LatestBitcionAPIResponse, - BTCDepositAddress, - FeeRateSatoshisPerKB, - SignedHashes, - Broadcast, - }); - - return req.pipe( - catchError((err) => { - console.error(JSON.stringify(err)); - return throwError(err); - }) - ); - } - SubmitETHTx( PublicKeyBase58Check: string, Tx: any, diff --git a/src/app/buy-deso/buy-deso-complete/buy-deso-complete.component.ts b/src/app/buy-deso/buy-deso-complete/buy-deso-complete.component.ts index 9a440fd0..7239062d 100644 --- a/src/app/buy-deso/buy-deso-complete/buy-deso-complete.component.ts +++ b/src/app/buy-deso/buy-deso-complete/buy-deso-complete.component.ts @@ -21,16 +21,17 @@ export class BuyDeSoCompleteComponent implements OnInit { private backendApi: BackendAPIService, private identityService: IdentityService, private accountService: AccountService - ) {} + ) { + } triggerBuyMoreDeSo(): void { this.buyMoreDeSoClicked.emit(); } - close(): void { + async close(): Promise { this.closeModal.emit(); this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKey, signedUp: this.globalVars.signedUp, }); diff --git a/src/app/buy-deso/buy-deso-heroswap/buy-deso-heroswap.component.ts b/src/app/buy-deso/buy-deso-heroswap/buy-deso-heroswap.component.ts index 1b260cee..9fde0ec4 100644 --- a/src/app/buy-deso/buy-deso-heroswap/buy-deso-heroswap.component.ts +++ b/src/app/buy-deso/buy-deso-heroswap/buy-deso-heroswap.component.ts @@ -25,7 +25,8 @@ export class BuyDeSoHeroSwapComponent implements OnInit, OnDestroy { private router: Router, private identityService: IdentityService, private accountService: AccountService - ) {} + ) { + } ngOnInit(): void { window.scroll(0, 0); @@ -53,20 +54,20 @@ export class BuyDeSoHeroSwapComponent implements OnInit, OnDestroy { window.removeEventListener('message', this.#heroswapMessageListener); } - finishFlow(): void { + async finishFlow(): Promise { if (this.globalVars.derive) { - this.router.navigate(['/', RouteNames.DERIVE], { - queryParams: { publicKey: this.publicKey }, + await this.router.navigate(['/', RouteNames.DERIVE], { + queryParams: {publicKey: this.publicKey}, queryParamsHandling: 'merge', }); } else { - this.login(); + await this.login(); } } - login(): void { + async login(): Promise { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKey, signedUp: this.globalVars.signedUp, }); diff --git a/src/app/buy-deso/buy-deso/buy-deso.component.ts b/src/app/buy-deso/buy-deso/buy-deso.component.ts index 44fff935..8cefcec0 100644 --- a/src/app/buy-deso/buy-deso/buy-deso.component.ts +++ b/src/app/buy-deso/buy-deso/buy-deso.component.ts @@ -76,7 +76,7 @@ export class BuyDeSoComponent implements OnInit { ]; buyTabs = this.defaultBuyTabs; activeTab = BuyDeSoComponent.BUY_WITH_HEROSWAP; - linkTabs = { [BuyDeSoComponent.BUY_ON_CB]: BuyDeSoComponent.CB_LINK }; + linkTabs = {[BuyDeSoComponent.BUY_ON_CB]: BuyDeSoComponent.CB_LINK}; satoshisPerDeSoExchangeRate = 0; ProtocolUSDCentsPerBitcoinExchangeRate = 0; @@ -149,8 +149,8 @@ export class BuyDeSoComponent implements OnInit { confirmButtonText: showBuyDeSo ? 'Buy DeSo' : showBuyCreatorCoin - ? 'Buy Creator Coin' - : 'Ok', + ? 'Buy Creator Coin' + : 'Ok', reverseButtons: true, }); } @@ -194,16 +194,16 @@ export class BuyDeSoComponent implements OnInit { ); } - ngOnInit(): void { + async ngOnInit(): Promise { const encryptedUser = - this.accountService.getEncryptedUsers()[this.publicKey]; + (await this.accountService.getEncryptedUsers())[this.publicKey]; // TODO: need some sort of UI for when we can't get encrypted user. if (!encryptedUser) { console.error('Encrypted User not found: Buying DESO will not work.'); this.publicKeyNotInIdentity = true; return; } else { - this.seedHex = this.cryptoService.decryptSeedHex( + this.seedHex = await this.cryptoService.decryptSeedHex( encryptedUser.encryptedSeedHex, this.globalVars.hostname ); @@ -253,4 +253,5 @@ export class BuyDeSoComponent implements OnInit { exports: [BuyDeSoComponent, BuyDeSoCompleteComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) -export class BuyDeSoComponentWrapper {} +export class BuyDeSoComponentWrapper { +} diff --git a/src/app/buy-or-send-deso/buy-or-send-deso.component.ts b/src/app/buy-or-send-deso/buy-or-send-deso.component.ts index 745093d5..2bb88333 100644 --- a/src/app/buy-or-send-deso/buy-or-send-deso.component.ts +++ b/src/app/buy-or-send-deso/buy-or-send-deso.component.ts @@ -50,7 +50,8 @@ export class BuyOrSendDesoComponent implements OnInit { }); } - ngOnInit(): void {} + ngOnInit(): void { + } ////// STEP FIVE BUTTONS | STEP_OBTAIN_DESO /////// @@ -97,27 +98,27 @@ export class BuyOrSendDesoComponent implements OnInit { }, 1000); } - finishFlowTransferDeSo(): void { - this.finishFlow(); + async finishFlowTransferDeSo(): Promise { + await this.finishFlow(); } ////// STEP SIX BUTTONS | STEP_BUY_DESO /////// ////// FINISH FLOW /////// - finishFlow(): void { + async finishFlow(): Promise { if (this.globalVars.derive) { - this.router.navigate(['/', RouteNames.DERIVE], { - queryParams: { publicKey: this.publicKeyAdded }, + await this.router.navigate(['/', RouteNames.DERIVE], { + queryParams: {publicKey: this.publicKeyAdded}, queryParamsHandling: 'merge', }); } else { - this.login(); + await this.login(); } } - login(): void { + async login(): Promise { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKeyAdded, signedUp: this.globalVars.signedUp, }); diff --git a/src/app/crypto.service.ts b/src/app/crypto.service.ts index 1c451751..f155fa4d 100644 --- a/src/app/crypto.service.ts +++ b/src/app/crypto.service.ts @@ -15,6 +15,10 @@ import * as sha256 from 'sha256'; import { Keccak } from 'sha3'; import { AccessLevel, Network } from '../types/identity'; import { GlobalVarsService } from './global-vars.service'; +import { aes_256_gcm } from "@noble/ciphers/webcrypto/aes"; +import { + utils as ecUtils +} from '@noble/secp256k1'; @Injectable({ providedIn: 'root', @@ -23,7 +27,8 @@ export class CryptoService { constructor( private cookieService: CookieService, private globalVars: GlobalVarsService - ) {} + ) { + } static PUBLIC_KEY_PREFIXES = { mainnet: { @@ -107,16 +112,26 @@ export class CryptoService { return encryptionKey; } - encryptSeedHex(seedHex: string, hostname: string): string { + async encryptSeedHex(seedHex: string, hostname: string): Promise { const encryptionKey = this.seedHexEncryptionKey(hostname, false); - const cipher = createCipher('aes-256-gcm', encryptionKey); - return cipher.update(seedHex).toString('hex'); + const cipher = aes_256_gcm(ecUtils.hexToBytes(encryptionKey), new Uint8Array(12)); + debugger; + const x = await cipher.encrypt(ecUtils.hexToBytes(seedHex)); + return ecUtils.bytesToHex(x); + // const cipher = createCipher('aes-256-gcm', encryptionKey); + // return cipher.update(seedHex).toString('hex'); } - decryptSeedHex(encryptedSeedHex: string, hostname: string): string { + async decryptSeedHex(encryptedSeedHex: string, hostname: string): Promise { const encryptionKey = this.seedHexEncryptionKey(hostname, false); - const decipher = createDecipher('aes-256-gcm', encryptionKey); - return decipher.update(Buffer.from(encryptedSeedHex, 'hex')).toString(); + const decipher = aes_256_gcm(ecUtils.hexToBytes(encryptionKey), new Uint8Array(12)); + const x = await decipher.decrypt(ecUtils.hexToBytes(encryptedSeedHex)); + return ecUtils.bytesToHex(x); + // const decipher = createDecipher('aes-256-gcm', encryptionKey); + // const buff = decipher.update(Buffer.from(encryptedSeedHex, 'hex')); + // debugger; + // console.log(buff); + // return buff.toString(); } accessLevelHmac(accessLevel: AccessLevel, seedHex: string): string { @@ -164,8 +179,8 @@ export class CryptoService { return ec.keyFromPrivate(seedHex); } - encryptedSeedHexToPublicKey(encryptedSeedHex: string): string { - const seedHex = this.decryptSeedHex( + async encryptedSeedHexToPublicKey(encryptedSeedHex: string): Promise { + const seedHex = await this.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); diff --git a/src/app/derive/derive.component.ts b/src/app/derive/derive.component.ts index 174229fd..580dfc10 100644 --- a/src/app/derive/derive.component.ts +++ b/src/app/derive/derive.component.ts @@ -12,6 +12,7 @@ import { } from '../backend-api.service'; import { GlobalVarsService } from '../global-vars.service'; import { IdentityService } from '../identity.service'; + type Accounts = { [key: string]: UserProfile } | {}; type DeriveParams = { publicKey?: string; @@ -20,6 +21,7 @@ type DeriveParams = { transactionSpendingLimitResponse?: any; expirationDays?: string; }; + @Component({ selector: 'app-derive', templateUrl: './derive.component.html', @@ -50,7 +52,8 @@ export class DeriveComponent implements OnInit { private backendApi: BackendAPIService, private activatedRoute: ActivatedRoute, private router: Router - ) {} + ) { + } ngOnInit(): void { this.backendApi.GetAppState().subscribe((res) => { @@ -84,7 +87,7 @@ export class DeriveComponent implements OnInit { this.deleteKey = params.deleteKey === 'true'; // We don't want or need to parse transaction spending limit when revoking derived key, // so we initialize a spending limit object with no permissions. - this.transactionSpendingLimitResponse = { GlobalDESOLimit: 0 }; + this.transactionSpendingLimitResponse = {GlobalDESOLimit: 0}; // Setting expiration days to 0 forces us to have a minimum transaction size that is still valid. this.expirationDays = 0; } @@ -185,10 +188,10 @@ export class DeriveComponent implements OnInit { true /*IncludeBalance*/ ) .pipe(take(1)) - .subscribe((res) => { + .subscribe(async (res) => { if (res.UserList?.[0]?.BalanceNanos === 0) { - this.router.navigate(['/', RouteNames.GET_DESO], { - queryParams: { publicKey }, + await this.router.navigate(['/', RouteNames.GET_DESO], { + queryParams: {publicKey}, queryParamsHandling: 'merge', }); return; @@ -198,7 +201,7 @@ export class DeriveComponent implements OnInit { // without approval. if (this.globalVars.authenticatedUsers.has(publicKey)) { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: publicKey, }); diff --git a/src/app/get-deso/get-deso.component.ts b/src/app/get-deso/get-deso.component.ts index 98388649..a17717ad 100644 --- a/src/app/get-deso/get-deso.component.ts +++ b/src/app/get-deso/get-deso.component.ts @@ -51,7 +51,7 @@ export class GetDesoComponent implements OnInit { publicKeyCopied = false; - @ViewChild('captchaElem', { static: false }) captchaElem: any; + @ViewChild('captchaElem', {static: false}) captchaElem: any; constructor( public entropyService: EntropyService, @@ -132,7 +132,7 @@ export class GetDesoComponent implements OnInit { stepThreeNextPhone(): void { this.router.navigate(['/', RouteNames.VERIFY_PHONE_NUMBER], { - queryParams: { public_key: this.publicKeyAdded }, + queryParams: {public_key: this.publicKeyAdded}, queryParamsHandling: 'merge', }); } @@ -161,10 +161,10 @@ export class GetDesoComponent implements OnInit { onCaptchaVerify(token: string): void { this.backendAPIService.VerifyHCaptcha(token, this.publicKeyAdded).subscribe( - (res) => { + async (res) => { if (res.Success) { this.isFinishFlowDisabled = false; - this.finishFlow(); + await this.finishFlow(); } else { this.captchaFailed = true; } @@ -221,23 +221,23 @@ export class GetDesoComponent implements OnInit { ? this.userBalanceNanos < 1e4 : false; - finishFlow(): void { + async finishFlow(): Promise { if (this.isFinishFlowDisabled) { return; } if (this.globalVars.derive) { - this.router.navigate(['/', RouteNames.DERIVE], { - queryParams: { publicKey: this.publicKeyAdded }, + await this.router.navigate(['/', RouteNames.DERIVE], { + queryParams: {publicKey: this.publicKeyAdded}, queryParamsHandling: 'merge', }); } else { - this.login(); + await this.login(); } } - login(): void { + async login(): Promise { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKeyAdded, signedUp: this.globalVars.signedUp, }); diff --git a/src/app/identity.service.ts b/src/app/identity.service.ts index 1b3112cc..db6c0268 100644 --- a/src/app/identity.service.ts +++ b/src/app/identity.service.ts @@ -215,16 +215,16 @@ export class IdentityService { // Incoming Messages - private handleBurn(data: any): void { - if (!this.approve(data, AccessLevel.Full)) { - return; + private async handleBurn(data: any): Promise { + if (!(await this.approve(data, AccessLevel.Full))) { + return Promise.resolve(); } const { id, - payload: { encryptedSeedHex, unsignedHashes }, + payload: {encryptedSeedHex, unsignedHashes}, } = data; - const seedHex = this.cryptoService.decryptSeedHex( + const seedHex = await this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); @@ -238,16 +238,16 @@ export class IdentityService { }); } - private handleSignETH(data: any): void { - if (!this.approve(data, AccessLevel.Full)) { + private async handleSignETH(data: any): Promise { + if (!(await this.approve(data, AccessLevel.Full))) { return; } const { id, - payload: { encryptedSeedHex, unsignedHashes }, + payload: {encryptedSeedHex, unsignedHashes}, } = data; - const seedHex = this.cryptoService.decryptSeedHex( + const seedHex = await this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); @@ -261,7 +261,7 @@ export class IdentityService { }); } - private handleSign(data: any): void { + private async handleSign(data: any): Promise { const { id, payload: { @@ -278,7 +278,7 @@ export class IdentityService { // In the case that approve() fails, it responds with a message indicating // that approvalRequired = true, which the caller can then uses to trigger // the /approve UI. - if (!this.approve(data, requiredAccessLevel)) { + if (!(await this.approve(data, requiredAccessLevel))) { return; } @@ -291,7 +291,7 @@ export class IdentityService { return; } - const seedHex = this.cryptoService.decryptSeedHex( + const seedHex = await this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); @@ -306,9 +306,10 @@ export class IdentityService { signedTransactionHex, }); } + // Encrypt with shared secret - private handleEncrypt(data: any): void { - if (!this.approve(data, AccessLevel.ApproveAll)) { + private async handleEncrypt(data: any): Promise { + if (!(await this.approve(data, AccessLevel.ApproveAll))) { return; } @@ -337,13 +338,13 @@ export class IdentityService { }); return; } - const seedHex = this.cryptoService.decryptSeedHex( + const seedHex = await this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); let messagingKeyRandomness: string | undefined; if (encryptedMessagingKeyRandomness) { - messagingKeyRandomness = this.cryptoService.decryptSeedHex( + messagingKeyRandomness = await this.cryptoService.decryptSeedHex( encryptedMessagingKeyRandomness, this.globalVars.hostname ); @@ -362,11 +363,11 @@ export class IdentityService { messagingKeyRandomness ); - this.respond(id, { ...encryptedMessage }); + this.respond(id, {...encryptedMessage}); } - private handleDecrypt(data: any): void { - if (!this.approve(data, AccessLevel.ApproveAll)) { + private async handleDecrypt(data: any): Promise { + if (!(await this.approve(data, AccessLevel.ApproveAll))) { return; } @@ -386,13 +387,13 @@ export class IdentityService { } const encryptedSeedHex = data.payload.encryptedSeedHex; - const seedHex = this.cryptoService.decryptSeedHex( + const seedHex = await this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); let messagingKeyRandomness: string | undefined; if (data.payload.encryptedMessagingKeyRandomness) { - messagingKeyRandomness = this.cryptoService.decryptSeedHex( + messagingKeyRandomness = await this.cryptoService.decryptSeedHex( data.payload.encryptedMessagingKeyRandomness, this.globalVars.hostname ); @@ -413,7 +414,7 @@ export class IdentityService { } catch (e: any) { console.error(e); // We include an empty decryptedHexes response to be backward compatible - this.respond(id, { error: e.message, decryptedHexes: {} }); // no suggestion just throw + this.respond(id, {error: e.message, decryptedHexes: {}}); // no suggestion just throw } } else { // Messages can be V1, V2, or V3. The message entries will indicate version. @@ -427,25 +428,25 @@ export class IdentityService { data.payload.ownerPublicKeyBase58Check ) .then( - (res) => this.respond(id, { decryptedHexes: res }), + (res) => this.respond(id, {decryptedHexes: res}), (err) => { console.error(err); - this.respond(id, { decryptedHexes: {}, error: err }); + this.respond(id, {decryptedHexes: {}, error: err}); } ); } } - private handleJwt(data: any): void { - if (!this.approve(data, AccessLevel.ApproveAll)) { + private async handleJwt(data: any): Promise { + if (!(await this.approve(data, AccessLevel.ApproveAll))) { return; } const { id, - payload: { encryptedSeedHex, derivedPublicKeyBase58Check }, + payload: {encryptedSeedHex, derivedPublicKeyBase58Check}, } = data; - const seedHex = this.cryptoService.decryptSeedHex( + const seedHex = await this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); @@ -532,14 +533,14 @@ export class IdentityService { return AccessLevel.Full; } - private hasAccessLevel(data: any, requiredAccessLevel: AccessLevel): boolean { + private async hasAccessLevel(data: any, requiredAccessLevel: AccessLevel): Promise { const { - payload: { encryptedSeedHex, accessLevel, accessLevelHmac }, + payload: {encryptedSeedHex, accessLevel, accessLevelHmac}, } = data; if (accessLevel < requiredAccessLevel) { return false; } - const seedHex = this.cryptoService.decryptSeedHex( + const seedHex = await this.cryptoService.decryptSeedHex( encryptedSeedHex, this.globalVars.hostname ); @@ -553,7 +554,7 @@ export class IdentityService { // This method checks if transaction in the payload has correct outputs for requested AccessLevel. private approveSpending(data: any): boolean { const { - payload: { accessLevel, transactionHex }, + payload: {accessLevel, transactionHex}, } = data; // If the requested access level is ApproveLarge, we want to confirm that transaction doesn't @@ -566,7 +567,7 @@ export class IdentityService { output.publicKey.toString('hex') !== transaction.publicKey.toString('hex') ) { - this.respond(data.id, { approvalRequired: true }); + this.respond(data.id, {approvalRequired: true}); return false; } } @@ -574,14 +575,14 @@ export class IdentityService { return true; } - private approve(data: any, accessLevel: AccessLevel): boolean { - const hasAccess = this.hasAccessLevel(data, accessLevel); + private async approve(data: any, accessLevel: AccessLevel): Promise { + const hasAccess = await this.hasAccessLevel(data, accessLevel); const hasEncryptionKey = this.cryptoService.hasSeedHexEncryptionKey( this.globalVars.hostname ); if (!hasAccess || !hasEncryptionKey) { - this.respond(data.id, { approvalRequired: true }); + this.respond(data.id, {approvalRequired: true}); return false; } @@ -591,8 +592,8 @@ export class IdentityService { // Message handling private handleMessage(event: MessageEvent): void { - const { data } = event; - const { service, method } = data; + const {data} = event; + const {service, method} = data; if (service !== 'identity') { return; @@ -606,12 +607,12 @@ export class IdentityService { } } - private handleRequest(event: MessageEvent): void { + private async handleRequest(event: MessageEvent): Promise { const data = event.data; const method = data.method; if (method === 'burn') { - this.handleBurn(data); + await this.handleBurn(data); } else if (method === 'encrypt') { this.handleEncrypt(data); } else if (method === 'decrypt') { @@ -623,7 +624,7 @@ export class IdentityService { } else if (method === 'jwt') { this.handleJwt(data); } else if (method === 'info') { - this.handleInfo(event); + await this.handleInfo(event); } else { console.error('Unhandled identity request'); console.error(event); @@ -632,7 +633,7 @@ export class IdentityService { private handleResponse(event: MessageEvent): void { const { - data: { id, payload }, + data: {id, payload}, origin, } = event; const hostname = new URL(origin).hostname; diff --git a/src/app/jumio/jumio-error/jumio-error.component.ts b/src/app/jumio/jumio-error/jumio-error.component.ts index 9a2364e9..3b2b1b4f 100644 --- a/src/app/jumio/jumio-error/jumio-error.component.ts +++ b/src/app/jumio/jumio-error/jumio-error.component.ts @@ -13,6 +13,7 @@ import { ActivatedRoute } from '@angular/router'; export class JumioErrorComponent implements OnInit, OnDestroy { publicKey = ''; hostname = ''; + constructor( public globalVars: GlobalVarsService, private activatedRoute: ActivatedRoute, @@ -25,13 +26,15 @@ export class JumioErrorComponent implements OnInit, OnDestroy { }); } - ngOnInit(): void {} + ngOnInit(): void { + } - ngOnDestroy(): void {} + ngOnDestroy(): void { + } - finishFlow(): void { + async finishFlow(): Promise { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKey, signedUp: true, jumioSuccess: false, diff --git a/src/app/jumio/jumio-success/jumio-success.component.ts b/src/app/jumio/jumio-success/jumio-success.component.ts index c5817958..babe78a1 100644 --- a/src/app/jumio/jumio-success/jumio-success.component.ts +++ b/src/app/jumio/jumio-success/jumio-success.component.ts @@ -22,9 +22,9 @@ export class JumioSuccessComponent implements OnInit, OnDestroy { const jumioInternalReference = params.customerInternalReference || ''; this.backendApiService .JumioFlowFinished(publicKey, jumioInternalReference) - .subscribe((res) => { + .subscribe(async (res) => { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: publicKey, signedUp: true, jumioSuccess: true, @@ -33,7 +33,9 @@ export class JumioSuccessComponent implements OnInit, OnDestroy { }); } - ngOnInit(): void {} + ngOnInit(): void { + } - ngOnDestroy(): void {} + ngOnDestroy(): void { + } } diff --git a/src/app/log-in/log-in.component.ts b/src/app/log-in/log-in.component.ts index 37467abd..bd3675ff 100644 --- a/src/app/log-in/log-in.component.ts +++ b/src/app/log-in/log-in.component.ts @@ -21,16 +21,17 @@ export class LogInComponent implements OnInit { private backendApi: BackendAPIService, public globalVars: GlobalVarsService, private router: Router - ) {} + ) { + } ngOnInit(): void { // Set showAccessLevels this.showAccessLevels = !this.globalVars.isFullAccessHostname(); } - login(publicKey: string): void { + async login(publicKey: string): Promise { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: publicKey, signedUp: false, }); @@ -39,25 +40,25 @@ export class LogInComponent implements OnInit { navigateToGetDeso(publicKey: string): void { this.router.navigate(['/', RouteNames.GET_DESO], { queryParamsHandling: 'merge', - queryParams: { publicKey }, + queryParams: {publicKey}, }); } - onAccountSelect(publicKey: string): void { + async onAccountSelect(publicKey: string): Promise { this.accountService.setAccessLevel( publicKey, this.globalVars.hostname, this.globalVars.accessLevelRequest ); if (!this.globalVars.getFreeDeso) { - this.login(publicKey); + await this.login(publicKey); } else { this.backendApi.GetUsersStateless([publicKey], true, true).subscribe( - (res) => { + async (res) => { if (!res?.UserList.length || res.UserList[0].BalanceNanos === 0) { this.navigateToGetDeso(publicKey); } else { - this.login(publicKey); + await this.login(publicKey); } }, (err) => { diff --git a/src/app/logout/logout.component.ts b/src/app/logout/logout.component.ts index 44aca2bd..1dc6efa2 100644 --- a/src/app/logout/logout.component.ts +++ b/src/app/logout/logout.component.ts @@ -24,7 +24,8 @@ export class LogoutComponent implements OnInit { private identityService: IdentityService, private accountService: AccountService, public globalVars: GlobalVarsService - ) {} + ) { + } ngOnInit(): void { this.activatedRoute.queryParams.subscribe((params) => { @@ -32,14 +33,14 @@ export class LogoutComponent implements OnInit { }); } - onCancel(): void { + async onCancel(): Promise { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKey, }); } - onSubmit(): void { + async onSubmit(): Promise { // We set the accessLevel for the logged out user to None. this.accountService.setAccessLevel( this.publicKey, @@ -50,12 +51,12 @@ export class LogoutComponent implements OnInit { // the logged out user, will regenerate their encryptedSeedHex. Without this, // someone could have reused the encryptedSeedHex of an already logged out user. this.cryptoService.seedHexEncryptionKey(this.globalVars.hostname, true); - this.finishFlow(); + await this.finishFlow(); } - finishFlow(): void { + async finishFlow(): Promise { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), }); } } diff --git a/src/app/messaging-group/messaging-group.component.ts b/src/app/messaging-group/messaging-group.component.ts index e0c956a7..a264134b 100644 --- a/src/app/messaging-group/messaging-group.component.ts +++ b/src/app/messaging-group/messaging-group.component.ts @@ -48,7 +48,8 @@ export class MessagingGroupComponent implements OnInit { private activatedRoute: ActivatedRoute, private cryptoService: CryptoService, private signingService: SigningService - ) {} + ) { + } ngOnInit(): void { try { @@ -182,7 +183,7 @@ export class MessagingGroupComponent implements OnInit { const membersSetNonEmpty = this.updatedMembersPublicKeysBase58Check.length > 0 && this.updatedMembersPublicKeysBase58Check.length === - this.updatedMembersKeyNames.length; + this.updatedMembersKeyNames.length; let validityCondition = groupSet; switch (this.operation) { @@ -252,9 +253,9 @@ export class MessagingGroupComponent implements OnInit { this.updatedGroupKeyName ); let encryptedMessagingKeyRandomness: string | undefined; - const publicUser = - this.accountService.getEncryptedUsers()[ - this.updatedGroupOwnerPublicKeyBase58Check + const publicUser = (await + this.accountService.getEncryptedUsers())[ + this.updatedGroupOwnerPublicKeyBase58Check ]; if (publicUser?.encryptedMessagingKeyRandomness) { encryptedMessagingKeyRandomness = @@ -331,6 +332,7 @@ export class MessagingGroupComponent implements OnInit { throw new Error('Error invalid operation'); } } + respondToClient( messagingKeySignature: string, encryptedToApplicationGroupMessagingPrivateKey: string, @@ -347,5 +349,6 @@ export class MessagingGroupComponent implements OnInit { }); } - onAccountSelect(event: any): void {} + onAccountSelect(event: any): void { + } } diff --git a/src/app/sign-up-get-starter-deso/sign-up-get-starter-deso.component.ts b/src/app/sign-up-get-starter-deso/sign-up-get-starter-deso.component.ts index 7657cb9e..c7bde0ab 100644 --- a/src/app/sign-up-get-starter-deso/sign-up-get-starter-deso.component.ts +++ b/src/app/sign-up-get-starter-deso/sign-up-get-starter-deso.component.ts @@ -74,7 +74,8 @@ export class SignUpGetStarterDESOComponent implements OnInit { private identityService: IdentityService, private accountService: AccountService, private router: Router - ) {} + ) { + } ngOnInit(): void { this.activatedRoute.queryParams.subscribe((params) => { @@ -148,7 +149,7 @@ export class SignUpGetStarterDESOComponent implements OnInit { checkIsValidPhoneNumber() { this.phoneForm.controls.phone.setErrors( - !!this.intlPhoneInputInstance?.isValidNumber() ? null : { invalid: true } + !!this.intlPhoneInputInstance?.isValidNumber() ? null : {invalid: true} ); } @@ -313,18 +314,18 @@ export class SignUpGetStarterDESOComponent implements OnInit { .add(() => (this.submittingPhoneNumberVerificationCode = false)); } - finishFlow(): void { + async finishFlow(): Promise { this.finishFlowEvent.emit(); if (this.globalVars.derive) { - this.router.navigate(['/', RouteNames.DERIVE], { - queryParams: { publicKey: this.publicKey }, + await this.router.navigate(['/', RouteNames.DERIVE], { + queryParams: {publicKey: this.publicKey}, queryParamsHandling: 'merge', }); return; } if (!this.finishFlowEventOnly) { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKey, signedUp: this.globalVars.signedUp, phoneNumberSuccess: this.isPhoneNumberSuccess, 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 5939bb7d..28abf178 100644 --- a/src/app/sign-up-metamask/sign-up-metamask.component.ts +++ b/src/app/sign-up-metamask/sign-up-metamask.component.ts @@ -18,16 +18,19 @@ export const getSpendingLimitsForMetamask = () => { IsUnlimited: true, }; }; + enum SCREEN { CREATE_ACCOUNT = 0, LOADING = 1, AUTHORIZE_MESSAGES = 3, } + enum METAMASK { START = 0, CONNECT = 1, ERROR = 2, } + @Component({ selector: 'app-sign-up-metamask', templateUrl: './sign-up-metamask.component.html', @@ -44,6 +47,7 @@ export class SignUpMetamaskComponent implements OnInit { errorMessage = ''; existingConnectedWallet = ''; showAlternative = false; + constructor( private accountService: AccountService, private identityService: IdentityService, @@ -53,7 +57,9 @@ export class SignUpMetamaskComponent implements OnInit { private signingService: SigningService, private metamaskService: MetamaskService, private router: Router - ) {} + ) { + } + async ngOnInit(): Promise { if (this.globalVars.isMobile()) { this.currentScreen = SCREEN.LOADING; @@ -92,7 +98,7 @@ export class SignUpMetamaskComponent implements OnInit { const network = this.globalVars.network; const expirationBlock = SignUpMetamaskComponent.UNLIMITED_DERIVED_KEY_EXPIRATION; - const { keychain, mnemonic, derivedPublicKeyBase58Check, derivedKeyPair } = + const {keychain, mnemonic, derivedPublicKeyBase58Check, derivedKeyPair} = this.accountService.generateDerivedKey(network); this.metamaskState = METAMASK.CONNECT; @@ -271,20 +277,20 @@ export class SignUpMetamaskComponent implements OnInit { }); } - public login(): void { + public async login(): Promise { this.accountService.setAccessLevel( this.publicKey, this.globalVars.hostname, this.globalVars.accessLevelRequest ); if (this.globalVars.derive) { - this.router.navigate(['/', RouteNames.DERIVE], { - queryParams: { publicKey: this.publicKey }, + await this.router.navigate(['/', RouteNames.DERIVE], { + queryParams: {publicKey: this.publicKey}, queryParamsHandling: 'merge', }); } else { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKey, signedUp: true, }); diff --git a/src/app/sign-up/sign-up.component.ts b/src/app/sign-up/sign-up.component.ts index 4d9cdae8..4d312461 100644 --- a/src/app/sign-up/sign-up.component.ts +++ b/src/app/sign-up/sign-up.component.ts @@ -60,7 +60,8 @@ export class SignUpComponent implements OnInit, OnDestroy { } } - ngOnInit(): void {} + ngOnInit(): void { + } ngOnDestroy(): void { // Set new entropy for the next time they go through the flow. @@ -106,7 +107,7 @@ export class SignUpComponent implements OnInit, OnDestroy { ////// STEP TWO BUTTONS | STEP_VERIFY_SEED /////// - stepTwoNext(): void { + async stepTwoNext(): Promise { // Add the new user to the account service registry. const network = this.globalVars.network; const mnemonic = this.mnemonicCheck; @@ -133,19 +134,19 @@ export class SignUpComponent implements OnInit, OnDestroy { if (this.globalVars.getFreeDeso) { this.globalVars.signedUp = true; - this.router.navigate(['/', RouteNames.GET_DESO], { - queryParams: { publicKey: this.publicKeyAdded, signedUp: true }, + await this.router.navigate(['/', RouteNames.GET_DESO], { + queryParams: {publicKey: this.publicKeyAdded, signedUp: true}, queryParamsHandling: 'merge', }); } else if (this.globalVars.derive) { this.globalVars.signedUp = true; - this.router.navigate(['/', RouteNames.DERIVE], { - queryParams: { publicKey: this.publicKeyAdded, signedUp: true }, + await this.router.navigate(['/', RouteNames.DERIVE], { + queryParams: {publicKey: this.publicKeyAdded, signedUp: true}, queryParamsHandling: 'merge', }); } else { this.identityService.login({ - users: this.accountService.getEncryptedUsers(), + users: await this.accountService.getEncryptedUsers(), publicKeyAdded: this.publicKeyAdded, signedUp: true, });