From 3fc7be09f1dbefe2b90a20e7a45dcb0260ca2276 Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Fri, 12 Feb 2021 17:16:44 +0800 Subject: [PATCH] BIP 44 derivation path (#42) --- package.json | 1 + src/components/BalancesList.js | 7 +- src/pages/LoginPage.js | 244 ++++++++++++++++++----- src/utils/wallet-seed.js | 56 ++++-- src/utils/wallet.js | 13 +- src/utils/walletProvider/localStorage.js | 34 +++- yarn.lock | 31 ++- 7 files changed, 307 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index 50cbe93a..71d7a227 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "bn.js": "^5.1.2", "bs58": "^4.0.1", "buffer-layout": "^1.2.0", + "ed25519-hd-key": "^1.2.0", "immutable-tuple": "^0.4.10", "mdi-material-ui": "^6.21.0", "notistack": "^1.0.2", diff --git a/src/components/BalancesList.js b/src/components/BalancesList.js index f559b9bc..5086c688 100644 --- a/src/components/BalancesList.js +++ b/src/components/BalancesList.js @@ -138,10 +138,11 @@ const useStyles = makeStyles((theme) => ({ }, })); -function BalanceListItem({ publicKey }) { +export function BalanceListItem({ publicKey, expandable }) { const balanceInfo = useBalanceInfo(publicKey); const classes = useStyles(); const [open, setOpen] = useState(false); + expandable = expandable === undefined ? true : expandable; if (!balanceInfo) { return ; @@ -151,7 +152,7 @@ function BalanceListItem({ publicKey }) { return ( <> - setOpen((open) => !open)}> + expandable && setOpen((open) => !open)}> @@ -166,7 +167,7 @@ function BalanceListItem({ publicKey }) { secondary={publicKey.toBase58()} secondaryTypographyProps={{ className: classes.address }} /> - {open ? : } + {expandable ? open ? : : <>} - Please write down the following twelve words and keep them in a safe - place: + Please write down the following twenty four words and keep them in a + safe place: {mnemonicAndSeed ? ( + + By default, sollet will use m/44'/501'/0'/0' as the + derivation path for the main wallet. To use an alternative path, try + restoring an existing wallet. + + {next ? ( + setNext(false)} + mnemonic={mnemonic} + password={password} + seed={seed} + /> + ) : ( + + + + Restore Existing Wallet + + + Restore your wallet using your twelve or twenty-four seed words. Note that this + will delete any existing wallet on this device. + + setMnemonic(e.target.value)} + /> + setPassword(e.target.value)} + /> + setPasswordConfirm(e.target.value)} + /> + + + + + + + )} + + ); +} + +function DerivedAccounts({ goBack, mnemonic, seed, password }) { const callAsync = useCallAsync(); + const urlSuffix = useSolanaExplorerUrlSuffix(); + const [dPathMenuItem, setDPathMenuItem] = useState( + DerivationPathMenuItem.Bip44Change, + ); + + const accounts = [...Array(10)].map((_, idx) => { + return getAccountFromSeed( + Buffer.from(seed, 'hex'), + idx, + toDerivationPath(dPathMenuItem), + ); + }); function submit() { callAsync( - mnemonicToSeed(mnemonic).then((seed) => - storeMnemonicAndSeed(mnemonic, seed, password), + storeMnemonicAndSeed( + mnemonic, + seed, + password, + toDerivationPath(dPathMenuItem), ), ); } @@ -242,54 +357,81 @@ function RestoreWalletForm({ goBack }) { return ( - - Restore Existing Wallet - - - Restore your wallet using your twelve seed words. Note that this will - delete any existing wallet on this device. - - setMnemonic(e.target.value)} - /> - setPassword(e.target.value)} - /> - setPasswordConfirm(e.target.value)} - /> +
+ + Derivable Accounts + + + + +
+ {accounts.map((acc) => { + return ( + + + + ); + })}
- - +
); } + +// Material UI's Select doesn't render properly when using an `undefined` value, +// so we define this type and the subsequent `toDerivationPath` translator as a +// workaround. +// +// DERIVATION_PATH.deprecated is always undefined. +const DerivationPathMenuItem = { + Deprecated: 0, + Bip44: 1, + Bip44Change: 2, +}; + +function toDerivationPath(dPathMenuItem) { + switch (dPathMenuItem) { + case DerivationPathMenuItem.Deprecated: + return DERIVATION_PATH.deprecated; + case DerivationPathMenuItem.Bip44: + return DERIVATION_PATH.bip44; + case DerivationPathMenuItem.Bip44Change: + return DERIVATION_PATH.bip44Change; + default: + throw new Error(`invalid derivation path: ${dPathMenuItem}`); + } +} diff --git a/src/utils/wallet-seed.js b/src/utils/wallet-seed.js index 6e69d62d..04b3bfc5 100644 --- a/src/utils/wallet-seed.js +++ b/src/utils/wallet-seed.js @@ -6,7 +6,7 @@ import { EventEmitter } from 'events'; export async function generateMnemonicAndSeed() { const bip39 = await import('bip39'); - const mnemonic = bip39.generateMnemonic(128); + const mnemonic = bip39.generateMnemonic(256); const seed = await bip39.mnemonicToSeed(mnemonic); return { mnemonic, seed: Buffer.from(seed).toString('hex') }; } @@ -27,7 +27,12 @@ let unlockedMnemonicAndSeed = (() => { 'null', ); if (stored === null) { - return { mnemonic: null, seed: null, importsEncryptionKey: null }; + return { + mnemonic: null, + seed: null, + importsEncryptionKey: null, + derivationPath: null, + }; } return { importsEncryptionKey: deriveImportsEncryptionKey(stored.seed), @@ -44,13 +49,28 @@ export function hasLockedMnemonicAndSeed() { return !!localStorage.getItem('locked'); } -function setUnlockedMnemonicAndSeed(mnemonic, seed, importsEncryptionKey) { - unlockedMnemonicAndSeed = { mnemonic, seed, importsEncryptionKey }; +function setUnlockedMnemonicAndSeed( + mnemonic, + seed, + importsEncryptionKey, + derivationPath, +) { + unlockedMnemonicAndSeed = { + mnemonic, + seed, + importsEncryptionKey, + derivationPath, + }; walletSeedChanged.emit('change', unlockedMnemonicAndSeed); } -export async function storeMnemonicAndSeed(mnemonic, seed, password) { - const plaintext = JSON.stringify({ mnemonic, seed }); +export async function storeMnemonicAndSeed( + mnemonic, + seed, + password, + derivationPath, +) { + const plaintext = JSON.stringify({ mnemonic, seed, derivationPath }); if (password) { const salt = randomBytes(16); const kdf = 'pbkdf2'; @@ -77,8 +97,13 @@ export async function storeMnemonicAndSeed(mnemonic, seed, password) { localStorage.removeItem('locked'); sessionStorage.removeItem('unlocked'); } - const privateKey = deriveImportsEncryptionKey(seed); - setUnlockedMnemonicAndSeed(mnemonic, seed, privateKey); + const importsEncryptionKey = deriveImportsEncryptionKey(seed); + setUnlockedMnemonicAndSeed( + mnemonic, + seed, + importsEncryptionKey, + derivationPath, + ); } export async function loadMnemonicAndSeed(password, stayLoggedIn) { @@ -98,13 +123,18 @@ export async function loadMnemonicAndSeed(password, stayLoggedIn) { throw new Error('Incorrect password'); } const decodedPlaintext = Buffer.from(plaintext).toString(); - const { mnemonic, seed } = JSON.parse(decodedPlaintext); + const { mnemonic, seed, derivationPath } = JSON.parse(decodedPlaintext); if (stayLoggedIn) { sessionStorage.setItem('unlocked', decodedPlaintext); } - const privateKey = deriveImportsEncryptionKey(seed); - setUnlockedMnemonicAndSeed(mnemonic, seed, privateKey); - return { mnemonic, seed }; + const importsEncryptionKey = deriveImportsEncryptionKey(seed); + setUnlockedMnemonicAndSeed( + mnemonic, + seed, + importsEncryptionKey, + derivationPath, + ); + return { mnemonic, seed, derivationPath }; } async function deriveEncryptionKey(password, salt, iterations, digest) { @@ -121,7 +151,7 @@ async function deriveEncryptionKey(password, salt, iterations, digest) { } export function lockWallet() { - setUnlockedMnemonicAndSeed(null, null, null); + setUnlockedMnemonicAndSeed(null, null, null, null); } // Returns the 32 byte key used to encrypt imported private keys. diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 30f7b643..44aeb6dd 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -127,7 +127,12 @@ const WalletContext = React.createContext(null); export function WalletProvider({ children }) { useListener(walletSeedChanged, 'change'); - const { mnemonic, seed, importsEncryptionKey } = getUnlockedMnemonicAndSeed(); + const { + mnemonic, + seed, + importsEncryptionKey, + derivationPath, + } = getUnlockedMnemonicAndSeed(); const { enqueueSnackbar } = useSnackbar(); const connection = useConnection(); const [wallet, setWallet] = useState(); @@ -180,6 +185,7 @@ export function WalletProvider({ children }) { ? getAccountFromSeed( Buffer.from(seed, 'hex'), walletSelector.walletIndex, + derivationPath, ) : new Account( (() => { @@ -205,6 +211,7 @@ export function WalletProvider({ children }) { importsEncryptionKey, setWalletSelector, enqueueSnackbar, + derivationPath, ]); function addAccount({ name, importedAccount, ledger }) { @@ -254,7 +261,8 @@ export function WalletProvider({ children }) { const seedBuffer = Buffer.from(seed, 'hex'); const derivedAccounts = [...Array(walletCount).keys()].map((idx) => { - let address = getAccountFromSeed(seedBuffer, idx).publicKey; + let address = getAccountFromSeed(seedBuffer, idx, derivationPath) + .publicKey; let name = localStorage.getItem(`name${idx}`); return { selector: { @@ -320,6 +328,7 @@ export function WalletProvider({ children }) { accounts, addAccount, setAccountName, + derivationPath, }} > {children} diff --git a/src/utils/walletProvider/localStorage.js b/src/utils/walletProvider/localStorage.js index f23ab899..d2a425fc 100644 --- a/src/utils/walletProvider/localStorage.js +++ b/src/utils/walletProvider/localStorage.js @@ -3,14 +3,40 @@ import * as bip32 from 'bip32'; import nacl from 'tweetnacl'; import { Account } from '@solana/web3.js'; import bs58 from 'bs58'; +import { derivePath } from 'ed25519-hd-key'; -export function getAccountFromSeed(seed, walletIndex, accountIndex = 0) { - const derivedSeed = bip32 - .fromSeed(seed) - .derivePath(`m/501'/${walletIndex}'/0/${accountIndex}`).privateKey; +export const DERIVATION_PATH = { + deprecated: undefined, + bip44: 'bip44', + bip44Change: 'bip44Change', +}; + +export function getAccountFromSeed( + seed, + walletIndex, + dPath = undefined, + accountIndex = 0, +) { + const derivedSeed = deriveSeed(seed, walletIndex, dPath, accountIndex); return new Account(nacl.sign.keyPair.fromSeed(derivedSeed).secretKey); } +function deriveSeed(seed, walletIndex, derivationPath, accountIndex) { + switch (derivationPath) { + case DERIVATION_PATH.deprecated: + const path = `m/501'/${walletIndex}'/0/${accountIndex}`; + return bip32.fromSeed(seed).derivePath(path).privateKey; + case DERIVATION_PATH.bip44: + const path44 = `m/44'/501'/${walletIndex}'`; + return derivePath(path44, seed).key; + case DERIVATION_PATH.bip44Change: + const path44Change = `m/44'/501'/${walletIndex}'/0'`; + return derivePath(path44Change, seed).key; + default: + throw new Error(`invalid derivation path: ${derivationPath}`); + } +} + export class LocalStorageWalletProvider { constructor(args) { const { seed } = getUnlockedMnemonicAndSeed(); diff --git a/yarn.lock b/yarn.lock index ecb295fe..ac65f31e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3424,6 +3424,16 @@ bip32@^2.0.5: typeforce "^1.11.5" wif "^2.0.6" +bip39@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.2.tgz#2baf42ff3071fc9ddd5103de92e8f80d9257ee32" + integrity sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ== + dependencies: + "@types/node" "11.11.6" + create-hash "^1.1.0" + pbkdf2 "^3.0.9" + randombytes "^2.0.1" + bip39@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.3.tgz#4a8b79067d6ed2e74f9199ac994a2ab61b176760" @@ -4438,7 +4448,7 @@ create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: +create-hmac@1.1.7, create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -5159,6 +5169,15 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ed25519-hd-key@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ed25519-hd-key/-/ed25519-hd-key-1.2.0.tgz#819d43c6a96477c9385bd121dccc94dbc6c6598c" + integrity sha512-pwES3tQ4Z8g3sfIBZEgtuTwFtHq5AlB9L8k9a48k7qPn74q2OmgrrgkdwyJ+P2GVTOBVCClAC7w21Wpksso3gw== + dependencies: + bip39 "3.0.2" + create-hmac "1.1.7" + tweetnacl "1.0.3" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -12715,16 +12734,16 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tweetnacl@1.0.3, tweetnacl@^1.0.0, tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -tweetnacl@^1.0.0, tweetnacl@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" - integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"