Skip to content

Commit

Permalink
feat: persist ledger public key on disk
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Sep 25, 2020
1 parent e0d1905 commit 89a497e
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 57 deletions.
12 changes: 6 additions & 6 deletions app/pages/onboarding/04-connect-ledger/connect-ledger.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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),
})
);
Expand Down
4 changes: 2 additions & 2 deletions app/pages/onboarding/08-set-password/set-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -53,7 +53,7 @@ export const SetPassword: React.FC = () => {
setStrengthResult(result);
if (result.meetsAllStrengthRequirements) {
setBtnDisabled(true);
dispatch(setPasswordAction({ password, history }));
dispatch(setSoftwareWallet({ password, history }));
}
};

Expand Down
36 changes: 16 additions & 20 deletions app/store/keys/keys.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,37 @@ 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<typeof useHistory>;

export const persistMnemonicSafe = createAction<string>('keys/save-mnemonic-safe');

export const persistMnemonic = createAction<string>('keys/save-mnemonic');

export const updateLedgerAddress = createAction<string>('keys/set-ledger-address');
interface PersistLedgerWalletAction {
address: string;
publicKey: string;
}
export const persistLedgerWallet = createAction<PersistLedgerWalletAction>(
'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();
};
}
Expand All @@ -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();
Expand All @@ -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;
Expand Down
28 changes: 13 additions & 15 deletions app/store/keys/keys.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +15,7 @@ export interface KeysState {
decryptionError?: string;
encryptedMnemonic?: string;
stxAddress?: string;
publicKey?: string;
}

const initialState: Readonly<KeysState> = Object.freeze({
Expand All @@ -44,15 +42,10 @@ export const createKeysReducer = (keys: Partial<KeysState> = {}) =>
...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',
}))
);
Expand All @@ -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')
);
46 changes: 32 additions & 14 deletions app/utils/disk-store.ts
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down

0 comments on commit 89a497e

Please sign in to comment.