Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor native wallet support with better encryption #398

Merged
merged 8 commits into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
"@shapeshiftoss/asset-service": "^1.7.2",
"@shapeshiftoss/caip": "^1.4.1",
"@shapeshiftoss/chain-adapters": "^1.16.2",
"@shapeshiftoss/hdwallet-core": "^1.18.0",
"@shapeshiftoss/hdwallet-keepkey": "^1.18.0",
"@shapeshiftoss/hdwallet-keepkey-webusb": "^1.18.0",
"@shapeshiftoss/hdwallet-metamask": "^1.18.0",
"@shapeshiftoss/hdwallet-native": "^1.18.0",
"@shapeshiftoss/swapper": "^1.11.7",
"@shapeshiftoss/types": "^1.12.0",
"@shapeshiftoss/hdwallet-portis": "^1.18.0",
"@shapeshiftoss/hdwallet-core": "^1.18.1-alpha.5",
"@shapeshiftoss/hdwallet-keepkey": "^1.18.1-alpha.5",
"@shapeshiftoss/hdwallet-keepkey-webusb": "^1.18.1-alpha.5",
"@shapeshiftoss/hdwallet-metamask": "^1.18.1-alpha.5",
"@shapeshiftoss/hdwallet-native": "^1.18.1-alpha.5",
"@shapeshiftoss/hdwallet-native-vault": "^1.18.1-alpha.5",
"@shapeshiftoss/hdwallet-portis": "^1.18.1-alpha.5",
"@shapeshiftoss/market-service": "^1.3.2",
"@shapeshiftoss/swapper": "^1.11.4",
"@shapeshiftoss/types": "^1.12.0",
"@types/history": "^4.7.9",
"@visx/axis": "^1.17.1",
"@visx/brush": "^1.18.1",
Expand Down
22 changes: 22 additions & 0 deletions patches/jest-environment-jsdom+25.5.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
diff --git a/node_modules/jest-environment-jsdom/build/index.js b/node_modules/jest-environment-jsdom/build/index.js
index cb5278c..48cab70 100644
--- a/node_modules/jest-environment-jsdom/build/index.js
mrnerdhair marked this conversation as resolved.
Show resolved Hide resolved
+++ b/node_modules/jest-environment-jsdom/build/index.js
@@ -116,6 +116,17 @@ class JSDOMEnvironment {
return originalRemoveListener.apply(this, args);
};

+ // open issues to get this patch eliminated
+ // https://github.com/facebook/jest/issues/9983
+ // https://github.com/jsdom/jsdom/issues/2524
+ if (
+ typeof TextEncoder !== 'undefined' &&
+ typeof TextDecoder !== 'undefined'
+ ) {
+ global.TextEncoder = TextEncoder;
+ global.TextDecoder = TextDecoder;
+ }
+
this.moduleMocker = new (_jestMock().ModuleMocker)(global);
const timerConfig = {
idToRef: id => id,
29 changes: 22 additions & 7 deletions src/assets/translations/messages-en.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,25 @@
}
},
"shapeShift": {
"nativeImport": {
"load": {
"error": {
"delete": "Unable to delete you wallet, sorry.",
cjthompson marked this conversation as resolved.
Show resolved Hide resolved
"noWallet": "You have no saved wallets",
"pair": "Unable to pair your wallet"
},
"header": "Load A Saved Wallet",
"body": "Loading your saved wallet... Enter your password when prompted.",
"button": "Continue"
},
"import": {
"header": "Import your wallet",
"body": "Enter your 12 word seed phrase to import an exisiting wallet",
"button": "Next"
},
"nativePasswd": {
"password": {
"error": {
"invalid": "Invalid Password"
},
"header": "Enter your password",
"body": "Enter a password to encrypt your wallet. In order to securely store your keys we will encrypt them, choose a string password you can remember.",
0xdef1cafe marked this conversation as resolved.
Show resolved Hide resolved
"button": "Next"
Expand All @@ -347,13 +360,15 @@
"button": "Next"
},
"nativeStart": {
"header": "Create or import a wallet",
"header": "ShapeShift Native Wallet",
"body": "Would you like to create a new wallet or import an existing one?",
"button": "Import Wallet",
"button2": "Create Wallet"
"import": "Import Wallet",
"create": "Create Wallet",
"load": "Load Saved Wallet"
},
"nativeSuccess": {
"header": "Wallet Connected",
"success": {
"encryptingWallet": "Encrypting your wallet...",
"header": "Wallet Conected",
"success": "Your wallet has been connected",
"error": "There was an error connecting your wallet"
},
Expand Down
119 changes: 119 additions & 0 deletions src/components/Modals/KeyManagement/Native/Password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
Button,
FormControl,
FormErrorMessage,
IconButton,
Input,
InputGroup,
InputRightElement,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay
} from '@chakra-ui/react'
import * as native from '@shapeshiftoss/hdwallet-native'
import { NativeHDWallet } from '@shapeshiftoss/hdwallet-native'
import { Vault } from '@shapeshiftoss/hdwallet-native-vault'
import React, { useState } from 'react'
import { FieldValues, useForm } from 'react-hook-form'
import { FaEye, FaEyeSlash } from 'react-icons/fa'
import { Text } from 'components/Text'
import { useModal } from 'context/ModalProvider/ModalProvider'
import { KeyManager, SUPPORTED_WALLETS } from 'context/WalletProvider/config'
import { useWallet, WalletActions } from 'context/WalletProvider/WalletProvider'

export const PasswordModal = ({ deviceId }: { deviceId: string }) => {
const { nativePassword } = useModal()
const { close, isOpen } = nativePassword
const { state, dispatch } = useWallet()
const wallet = state.keyring.get<NativeHDWallet>(deviceId)

const [showPw, setShowPw] = useState<boolean>(false)

const {
setError,
handleSubmit,
register,
formState: { errors, isSubmitting }
} = useForm()

const handleShowClick = () => setShowPw(!showPw)
const onSubmit = async (values: FieldValues) => {
try {
const vault = await Vault.open(deviceId, values.password)
const mnemonic = (await vault.get('#mnemonic')) as native.crypto.Isolation.Core.BIP39.Mnemonic
mnemonic.addRevoker?.(() => vault.revoke())
await wallet?.loadDevice({
mnemonic,
deviceId
})
const { name, icon } = SUPPORTED_WALLETS[KeyManager.Native]
dispatch({ type: WalletActions.SET_WALLET, payload: { wallet, name, icon, deviceId } })
dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true })
close()
} catch (e) {
setError(
'password',
{
type: 'manual',
message: 'walletProvider.shapeShift.password.error.invalid'
},
{ shouldFocus: true }
)
}
}

return (
<Modal
isOpen={isOpen}
onClose={close}
isCentered
closeOnOverlayClick={false}
closeOnEsc={false}
>
<ModalOverlay />
<ModalContent justifyContent='center' px={3} pt={3} pb={6}>
<ModalCloseButton ml='auto' borderRadius='full' position='static' />
<ModalHeader>
<Text translation={'walletProvider.shapeShift.password.header'} />
</ModalHeader>
<ModalBody>
<Text mb={6} color='gray.500' translation={'walletProvider.shapeShift.password.body'} />
<form onSubmit={handleSubmit(onSubmit)}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here -- maybe we make this revocable too

<FormControl isInvalid={errors.password} mb={6}>
<InputGroup size='lg' variant='filled'>
<Input
{...register('password', {
required: 'This is required',
minLength: { value: 8, message: 'Password must be at least 8 characters' }
})}
pr='4.5rem'
type={showPw ? 'text' : 'password'}
placeholder='Enter password'
id='password'
/>
<InputRightElement>
<IconButton
aria-label={!showPw ? 'Show password' : 'Hide password'}
h='1.75rem'
size='sm'
onClick={handleShowClick}
icon={!showPw ? <FaEye /> : <FaEyeSlash />}
/>
</InputRightElement>
</InputGroup>
<FormErrorMessage>
<Text translation={errors?.password?.message} />
</FormErrorMessage>
</FormControl>
<Button colorScheme='blue' size='lg' isFullWidth type='submit' isLoading={isSubmitting}>
<Text translation={'walletProvider.shapeShift.password.button'} />
</Button>
</form>
</ModalBody>
</ModalContent>
</Modal>
)
}
2 changes: 2 additions & 0 deletions src/context/ModalProvider/ModalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import noop from 'lodash/noop'
import React, { useContext, useMemo, useReducer } from 'react'
import { PassphraseModal } from 'components/Modals/KeyManagement/KeepKey/Passphrase'
import { PinModal } from 'components/Modals/KeyManagement/KeepKey/Pin'
import { PasswordModal } from 'components/Modals/KeyManagement/Native/Password'
import { ReceiveModal } from 'components/Modals/Receive/Receive'
import { SendModal } from 'components/Modals/Send/Send'

// to add new modals, add a new key: value pair below
// the key is the name returned by the hook and the
// component is the modal to be rendered
const MODALS = {
nativePassword: PasswordModal,
keepkeyPin: PinModal,
keepkeyPassphrase: PassphraseModal,
receive: ReceiveModal,
Expand Down
134 changes: 134 additions & 0 deletions src/context/WalletProvider/NativeWallet/components/NativeCreate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
Alert,
AlertDescription,
AlertIcon,
Button,
Code,
ModalBody,
ModalFooter,
ModalHeader,
Tag,
Wrap
} from '@chakra-ui/react'
import * as native from '@shapeshiftoss/hdwallet-native'
import { GENERATE_MNEMONIC, Vault } from '@shapeshiftoss/hdwallet-native-vault'
import { range } from 'lodash'
import { Component, useEffect, useMemo, useRef, useState } from 'react'
import { FaEye } from 'react-icons/fa'
import { Text } from 'components/Text'

import { NativeSetupProps } from '../types'

const Revocable = native.crypto.Isolation.Engines.Default.Revocable
const revocable = native.crypto.Isolation.Engines.Default.revocable

export const NativeCreate = ({ history, location }: NativeSetupProps) => {
const [revealed, setRevealed] = useState<boolean>(false)
const revealedOnce = useRef<boolean>(false)
const handleShow = () => {
revealedOnce.current = true
setRevealed(!revealed)
}
const [vault, setVault] = useState<Vault | null>(null)
const [words, setWords] = useState<Component[] | null>(null)
const [revoker] = useState(new (Revocable(class {}))())

const placeholders = useMemo(() => {
return range(1, 13).map(i => (
<Tag
p={2}
flexBasis='31%'
justifyContent='flex-start'
fontSize='md'
colorScheme='blue'
key={i}
>
<Code mr={2}>{i}</Code>
•••••••
</Tag>
))
}, [])

useEffect(() => {
;(async () => {
try {
const vault = await Vault.create(undefined, false)
vault.set('#mnemonic', GENERATE_MNEMONIC)
setVault(vault)
} catch (e) {
// @TODO
console.error(e)
}
})()
}, [setVault])

useEffect(() => {
if (!vault) return
;(async () => {
try {
setWords(
(await vault.unwrap().get('#mnemonic')).split(' ').map((word: string, index: number) =>
revocable(
<Tag
p={2}
flexBasis='31%'
justifyContent='flex-start'
fontSize='md'
key={word}
colorScheme='blue'
>
<Code mr={2}>{index + 1}</Code>
{word}
</Tag>,
revoker.addRevoker.bind(revocable)
)
)
)
} catch (e) {
console.error('failed to get seed:', e)
setWords(null)
}
})()

return () => {
revoker.revoke()
}
}, [setWords, vault, revoker])

return (
<>
<ModalHeader>
<Text translation={'walletProvider.shapeShift.nativeSeed.header'} />
</ModalHeader>
<ModalBody>
<Text translation={'walletProvider.shapeShift.nativeSeed.body'} />
{location?.state?.error && (
<Alert status='error'>
<AlertIcon />
<AlertDescription>{location.state.error.message}</AlertDescription>
</Alert>
)}
<Wrap mt={12} mb={6}>
{revealed ? words : placeholders}
</Wrap>
</ModalBody>
<ModalFooter justifyContent='space-between'>
<Button colorScheme='blue' onClick={handleShow} size='lg' leftIcon={<FaEye />}>
{`${revealed ? 'Hide' : 'Show'}`} Words
</Button>
<Button
colorScheme='blue'
size='lg'
disabled={!(vault && words && revealedOnce.current)}
onClick={() => {
if (vault) {
history.push('/native/create-test', { vault })
cjthompson marked this conversation as resolved.
Show resolved Hide resolved
}
}}
>
<Text translation={'walletProvider.shapeShift.nativeSeed.button'} />
</Button>
</ModalFooter>
</>
)
}
Loading