From 89a497eb70820d78094cd6842ce44c83bdc49a74 Mon Sep 17 00:00:00 2001 From: kyranjamie Date: Fri, 7 Aug 2020 09:26:51 +0200 Subject: [PATCH] feat: persist ledger public key on disk --- .../04-connect-ledger/connect-ledger.tsx | 12 ++--- .../08-set-password/set-password.tsx | 4 +- app/store/keys/keys.actions.ts | 36 +++++++-------- app/store/keys/keys.reducer.ts | 28 ++++++----- app/utils/disk-store.ts | 46 +++++++++++++------ 5 files changed, 69 insertions(+), 57 deletions(-) diff --git a/app/pages/onboarding/04-connect-ledger/connect-ledger.tsx b/app/pages/onboarding/04-connect-ledger/connect-ledger.tsx index 6846c8c04..1c5b5eea4 100644 --- a/app/pages/onboarding/04-connect-ledger/connect-ledger.tsx +++ b/app/pages/onboarding/04-connect-ledger/connect-ledger.tsx @@ -1,8 +1,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import { Box, Flex, Text, CheckmarkCircleIcon } from '@blockstack/ui'; import BlockstackApp from '@zondax/ledger-blockstack'; -import Transport from '@ledgerhq/hw-transport'; +import type Transport from '@ledgerhq/hw-transport'; import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'; import { useDispatch } from 'react-redux'; @@ -14,13 +13,13 @@ import { OnboardingButton, OnboardingBackButton, } from '../../../components/onboarding'; -import { setLedgerAddress } from '../../../store/keys'; +import { setLedgerWallet } from '../../../store/keys'; import { useInterval } from '../../../hooks/use-interval'; import { ERROR_CODE } from '../../../../../ledger-blockstack/js/src/common'; import { delay } from '../../../utils/delay'; import { LedgerConnectInstructions } from '../../../components/ledger/ledger-connect-instructions'; -const STX_DERIVATION_PATH = `m/44'/5757'/0/0/0`; +const STX_DERIVATION_PATH = `m/44'/5757'/0'/0/0`; export enum LedgerConnectStep { Disconnected, @@ -114,7 +113,7 @@ export const ConnectLedger: React.FC = () => { const app = new BlockstackApp(usbTransport); try { - void app.getVersion(); + await app.getVersion(); const confirmedResponse = await app.showAddressAndPubKey(STX_DERIVATION_PATH); if (confirmedResponse.returnCode !== ERROR_CODE.NoError) { @@ -126,8 +125,9 @@ export const ConnectLedger: React.FC = () => { setStep(LedgerConnectStep.HasAddress); await delay(1250); dispatch( - setLedgerAddress({ + setLedgerWallet({ address: confirmedResponse.address, + publicKey: confirmedResponse.publicKey, onSuccess: () => history.push(routes.HOME), }) ); diff --git a/app/pages/onboarding/08-set-password/set-password.tsx b/app/pages/onboarding/08-set-password/set-password.tsx index 144e560b6..4b7fc922c 100644 --- a/app/pages/onboarding/08-set-password/set-password.tsx +++ b/app/pages/onboarding/08-set-password/set-password.tsx @@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; import { Text, Input } from '@blockstack/ui'; -import { setSoftwareWalletPassword as setPasswordAction } from '../../../store/keys'; +import { setSoftwareWallet } from '../../../store/keys'; import { Onboarding, OnboardingTitle, @@ -53,7 +53,7 @@ export const SetPassword: React.FC = () => { setStrengthResult(result); if (result.meetsAllStrengthRequirements) { setBtnDisabled(true); - dispatch(setPasswordAction({ password, history })); + dispatch(setSoftwareWallet({ password, history })); } }; diff --git a/app/store/keys/keys.actions.ts b/app/store/keys/keys.actions.ts index 3d7b179c8..ba38b1f6e 100644 --- a/app/store/keys/keys.actions.ts +++ b/app/store/keys/keys.actions.ts @@ -13,12 +13,11 @@ import { persistStxAddress, persistWalletType, } from '../../utils/disk-store'; -import { safeAwait } from '../../utils/safe-await'; -import { selectKeysSlice, selectMnemonic } from './keys.reducer'; import { generateSalt, deriveKey } from '../../crypto/key-generation'; import { deriveStxAddressKeychain } from '../../crypto/derive-address-keychain'; import { encryptMnemonic, decryptMnemonic } from '../../crypto/key-encryption'; -import { delay } from '../../utils/delay'; +import { persistPublicKey } from '../../utils/disk-store'; +import { selectMnemonic } from './keys.reducer'; type History = ReturnType; @@ -26,18 +25,25 @@ export const persistMnemonicSafe = createAction('keys/save-mnemonic-safe export const persistMnemonic = createAction('keys/save-mnemonic'); -export const updateLedgerAddress = createAction('keys/set-ledger-address'); +interface PersistLedgerWalletAction { + address: string; + publicKey: string; +} +export const persistLedgerWallet = createAction( + 'keys/persist-ledger-wallet' +); interface SetLedgerAddress { address: string; + publicKey: Buffer; onSuccess: () => void; } -export function setLedgerAddress({ address, onSuccess }: SetLedgerAddress) { - return async (dispatch: Dispatch) => { - await delay(1000); +export function setLedgerWallet({ address, publicKey, onSuccess }: SetLedgerAddress) { + return (dispatch: Dispatch) => { persistStxAddress(address); + persistPublicKey(publicKey.toString('hex')); persistWalletType('ledger'); - dispatch(updateLedgerAddress(address)); + dispatch(persistLedgerWallet({ address, publicKey: publicKey.toString('hex') })); onSuccess(); }; } @@ -58,11 +64,11 @@ export function onboardingMnemonicGenerationStep({ stepDelayMs }: { stepDelayMs: }; } -interface SetSoftwareWalletPassword { +interface SetSoftwareWallet { password: string; history: History; } -export function setSoftwareWalletPassword({ password, history }: SetSoftwareWalletPassword) { +export function setSoftwareWallet({ password, history }: SetSoftwareWallet) { return async (dispatch: Dispatch, getState: () => RootState) => { const mnemonic = selectMnemonic(getState()); const salt = generateSalt(); @@ -84,16 +90,6 @@ export function setSoftwareWalletPassword({ password, history }: SetSoftwareWall }; } -export const attemptWalletDecrypt = createAction('keys/attempt-wallet-decrypt'); -export const attemptWalletDecryptSuccess = createAction<{ - salt: string; - mnemonic: string; - address: string; -}>('keys/attempt-wallet-decrypt-success'); -export const attemptWalletDecryptFailed = createAction<{ decryptionError: string }>( - 'keys/attempt-wallet-decrypt-failed' -); - interface DecryptSoftwareWalletArgs { password: string; salt: string; diff --git a/app/store/keys/keys.reducer.ts b/app/store/keys/keys.reducer.ts index e96529165..742e451c8 100644 --- a/app/store/keys/keys.reducer.ts +++ b/app/store/keys/keys.reducer.ts @@ -2,14 +2,11 @@ import { createReducer, createSelector } from '@reduxjs/toolkit'; import log from 'electron-log'; import { RootState } from '..'; -import { setPasswordSuccess, updateLedgerAddress } from './keys.actions'; -import { - persistMnemonicSafe, - persistMnemonic, - attemptWalletDecryptFailed, - attemptWalletDecrypt, -} from './keys.actions'; +import { setPasswordSuccess, persistLedgerWallet } from './keys.actions'; +import { persistMnemonicSafe, persistMnemonic } from './keys.actions'; +// +// TODO: create separate state slices per wallet type export interface KeysState { walletType: 'ledger' | 'software'; mnemonic: string | null; @@ -18,6 +15,7 @@ export interface KeysState { decryptionError?: string; encryptedMnemonic?: string; stxAddress?: string; + publicKey?: string; } const initialState: Readonly = Object.freeze({ @@ -44,15 +42,10 @@ export const createKeysReducer = (keys: Partial = {}) => ...payload, mnemonic: null, })) - .addCase(attemptWalletDecrypt, state => ({ ...state, decrypting: true })) - .addCase(attemptWalletDecryptFailed, (state, action) => ({ + .addCase(persistLedgerWallet, (state, { payload }) => ({ ...state, - decrypting: false, - decryptionError: action.payload.decryptionError, - })) - .addCase(updateLedgerAddress, (state, { payload }) => ({ - ...state, - stxAddress: payload, + stxAddress: payload.address, + publicKey: payload.publicKey, walletType: 'ledger', })) ); @@ -64,9 +57,14 @@ export const selectDecryptionError = createSelector( ); export const selectIsDecrypting = createSelector(selectKeysSlice, state => state.decrypting); export const selectMnemonic = createSelector(selectKeysSlice, state => state.mnemonic); +export const selectWalletType = createSelector(selectKeysSlice, state => state.walletType); export const selectEncryptedMnemonic = createSelector( selectKeysSlice, state => state.encryptedMnemonic ); export const selectAddress = createSelector(selectKeysSlice, state => state.stxAddress); export const selectSalt = createSelector(selectKeysSlice, state => state.salt); +export const selectPublicKey = createSelector( + selectKeysSlice, + state => state.publicKey && Buffer.from(state.publicKey, 'hex') +); diff --git a/app/utils/disk-store.ts b/app/utils/disk-store.ts index 4bdee7846..90e25a68b 100644 --- a/app/utils/disk-store.ts +++ b/app/utils/disk-store.ts @@ -1,52 +1,70 @@ import Store from 'electron-store'; -enum PersistedValues { +enum StoreIndex { Salt = 'salt', EncryptedMnemonic = 'encryptedMnemonic', StxAddress = 'stxAddress', + PublicKey = 'publicKey', WalletType = 'walletType', } -export interface DiskStore { - [PersistedValues.Salt]?: string; - [PersistedValues.EncryptedMnemonic]?: string; - [PersistedValues.WalletType]: 'ledger' | 'software'; - [PersistedValues.StxAddress]: string; +interface SoftwareWallet { + [StoreIndex.WalletType]: 'software'; + [StoreIndex.Salt]: string; + [StoreIndex.EncryptedMnemonic]: string; + [StoreIndex.StxAddress]: string; } +interface LedgerWallet { + [StoreIndex.WalletType]: 'ledger'; + [StoreIndex.StxAddress]: string; + [StoreIndex.PublicKey]: string; +} + +export type DiskStore = LedgerWallet | SoftwareWallet; + const store = new Store({ schema: { - [PersistedValues.Salt]: { + [StoreIndex.Salt]: { type: 'string', minLength: 32, maxLength: 32, }, - [PersistedValues.EncryptedMnemonic]: { + [StoreIndex.EncryptedMnemonic]: { type: 'string', }, - [PersistedValues.StxAddress]: { + [StoreIndex.StxAddress]: { type: 'string', }, - [PersistedValues.WalletType]: { + [StoreIndex.PublicKey]: { + type: 'string', + minLength: 66, + maxLength: 66, + }, + [StoreIndex.WalletType]: { enum: ['ledger', 'software'], }, }, }); export const persistEncryptedMnemonic = (encryptedMnemonic: string) => { - store.set(PersistedValues.EncryptedMnemonic, encryptedMnemonic); + store.set(StoreIndex.EncryptedMnemonic, encryptedMnemonic); }; export const persistStxAddress = (stxAddress: string) => { - store.set(PersistedValues.StxAddress, stxAddress); + store.set(StoreIndex.StxAddress, stxAddress); +}; + +export const persistPublicKey = (publicKey: string) => { + store.set(StoreIndex.PublicKey, publicKey); }; export const persistSalt = (salt: string) => { - store.set(PersistedValues.Salt, salt); + store.set(StoreIndex.Salt, salt); }; export const persistWalletType = (walletType: 'ledger' | 'software') => { - store.set(PersistedValues.WalletType, walletType); + store.set(StoreIndex.WalletType, walletType); }; export const getInitialStateFromDisk = () => {