From d1eec2fc928f8f6db74ea1370eb83cfd995c2952 Mon Sep 17 00:00:00 2001 From: Pierce Trey Date: Fri, 21 May 2021 16:09:25 -0600 Subject: [PATCH 1/3] key provider selection and auth key support --- src/Constant/affiliate.ts | 11 + src/actions/key-actions.ts | 5 +- src/components/Form/InputGroup.tsx | 3 +- src/components/Form/Select/Select.tsx | 193 ++++++++++++++++++ src/components/Form/Select/index.ts | 1 + .../KeyManagement/AddKeyModalContainer.tsx | 39 +++- .../KeyManagement/DisplayPrivateKeysModal.tsx | 26 +-- src/components/KeyManagement/KeyManager.tsx | 28 +-- .../KeyManagement/KeyManagerContainer.tsx | 2 +- src/hooks/key-hooks.ts | 3 +- src/models/keys.ts | 1 + 11 files changed, 260 insertions(+), 52 deletions(-) create mode 100644 src/Constant/affiliate.ts create mode 100644 src/components/Form/Select/Select.tsx create mode 100644 src/components/Form/Select/index.ts diff --git a/src/Constant/affiliate.ts b/src/Constant/affiliate.ts new file mode 100644 index 0000000..c0f3654 --- /dev/null +++ b/src/Constant/affiliate.ts @@ -0,0 +1,11 @@ +export enum KEY_PROVIDERS { + DATABASE = 'DATABASE', + SMART_KEY = 'SMART_KEY', +} + +export const KEY_PROVIDER_META = { + [KEY_PROVIDERS.DATABASE]: { label: 'Database', description: 'Store keys directly in the database. Note: this is a less secure method of key storage' }, + [KEY_PROVIDERS.SMART_KEY]: { label: 'SmartKey', description: 'Use external cloud Hardware Security Module (HSM) provider Equinix SmartKey® to securely store private keys' }, +} as const + +export const KEY_PROVIDERS_ARRAY = Object.entries(KEY_PROVIDERS).map(([key, value]) => ({ ...KEY_PROVIDER_META[key], value })); \ No newline at end of file diff --git a/src/actions/key-actions.ts b/src/actions/key-actions.ts index fe737f9..3fe44be 100644 --- a/src/actions/key-actions.ts +++ b/src/actions/key-actions.ts @@ -4,6 +4,7 @@ import { ContractKey, ServiceKey } from 'models/keys'; import { addError } from './error-actions'; import { handleAndThrow } from 'helpers/general'; import { ajaxDelete, ajaxGet, ajaxPatch, ajaxPost } from './xhr-actions'; +import { KEY_PROVIDERS } from 'Constant/affiliate'; const AFFILIATE_URL = `${P8E_URL}/keys/affiliate`; const SERVICE_URL = `${P8E_URL}/keys/service`; @@ -33,8 +34,8 @@ export const updateContractKey = (hexPublicKey: string, { alias }: { alias: stri export const setCurrentKey = (currentKey?: ContractKey) => async dispatch => dispatch(createAction(SET_CURRENT_KEY)(currentKey)); -export const addContractKey = (signingPrivateKey: string, encryptionPrivateKey: string, useSigningKeyForEncryption: boolean, indexName: string, alias?: string) => async dispatch => - ajaxPost(ADD_CONTRACT_KEY, dispatch, AFFILIATE_URL, { signingPrivateKey, encryptionPrivateKey, useSigningKeyForEncryption, indexName, alias }) +export const addContractKey = (signingPrivateKey: string, encryptionPrivateKey: string, keyProvider: KEY_PROVIDERS, indexName: string, alias?: string) => async dispatch => + ajaxPost(ADD_CONTRACT_KEY, dispatch, AFFILIATE_URL, { signingPrivateKey, encryptionPrivateKey, keyProvider: keyProvider, indexName, alias }) .catch(handleAndThrow(() => dispatch(addError('Error adding contract key')))); export const addServiceKey = (privateKey: string, alias: string) => async dispatch => ajaxPost(ADD_SERVICE_KEY, dispatch, SERVICE_URL, { privateKey, alias }) diff --git a/src/components/Form/InputGroup.tsx b/src/components/Form/InputGroup.tsx index be3e3a4..592b2e1 100644 --- a/src/components/Form/InputGroup.tsx +++ b/src/components/Form/InputGroup.tsx @@ -1,10 +1,11 @@ +import { HTMLProps } from 'react'; import styled from 'styled-components'; type InputGroupProps = { /** Overrides the default margin */ margin?: string; disabled?: boolean; -} +} & HTMLProps export const InputGroup = styled.div` margin: ${({ margin }) => (margin ? margin : '20px 0')}; diff --git a/src/components/Form/Select/Select.tsx b/src/components/Form/Select/Select.tsx new file mode 100644 index 0000000..87ee8f6 --- /dev/null +++ b/src/components/Form/Select/Select.tsx @@ -0,0 +1,193 @@ +import React, { ChangeEventHandler, FunctionComponent, ReactChildren } from 'react'; +import styled, { css } from 'styled-components'; +import theme from 'styled-theming'; +import useWhatInput from 'react-use-what-input'; +import { Color, Width, Icon } from 'Constant'; +import { Sprite } from 'components/Sprite'; +import { InputGroup } from '../InputGroup'; +import { Label } from '../Label'; +import { Error } from '../Error'; + +const backgroundColor = theme('mode', { + dark: Color.BLACK, + light: Color.WHITE, +}); + +const borderColor = theme('mode', { + dark: Color.LIGHT_GREY, + light: Color.LIGHT_GREY, +}); + +const activeBorder = theme('mode', { + dark: Color.BLUE, + light: Color.BLUE, +}); + +const errorBorderColor = theme('mode', { + dark: Color.RED, + light: Color.RED, +}); + +const caretColor = theme('mode', { + dark: Color.WHITE, + light: Color.GREY, +}); + +const focusColor = theme('mode', { + dark: Color.STEEL, + light: Color.SKY, +}); + +const placeholderColor = theme('mode', { + dark: Color.LIGHT_GREY, + light: Color.LIGHT_GREY, +}); + +const arrowDownBackground = theme('mode', { + dark: Color.GREY, + light: Color.WHITE, +}); + +const arrowColor = theme('mode', { + dark: Color.WHITE, + light: Color.BLACK, +}); + +type ElementProps = { + errorText?: string; + $isKeyboard?: boolean; + thin?: boolean; +} + +const Element = styled.select.attrs(({ errorText }) => ({ + 'data-error': errorText && errorText.length > 0, +}))` + position: relative; + padding: 10px 20px; + height: 44px; + width: 100%; + border: ${({ errorText }) => (errorText ? errorBorderColor : borderColor)} 1px solid; + border-radius: 4px; + caret-color: ${caretColor}; /* IE/Edge doesn't honor this but uses the inverse of the background color */ + color: ${caretColor}; + font-size: 1.4rem; + line-height: 1.6; + background: ${(props) => backgroundColor(props)}; + appearance: none; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'initial')}; + outline: none; + + &::placeholder, + &:-ms-input-placeholder, + &::-ms-input-placeholder { + color: ${placeholderColor}; + } + + &:hover:not([disabled]) { + border-color: ${activeBorder}; + } + + &:focus { + ${({ $isKeyboard }) => + $isKeyboard && + css` + border-color: ${activeBorder}; + box-shadow: 0px 0px 0px 6px ${focusColor}; + /* Only visible in Windows High Contrast mode */ + outline: 1px solid transparent; + `}; + } + + @media (min-width: ${Width.SM}px) { + padding: ${({ thin }) => thin && '5px 10px'}; + height: ${({ thin }) => thin && '34px'}; + } +`; + +type ArrowDownProps = { + thin?: boolean; +} + +const ArrowDownContainer = styled.div` + position: absolute; + display: flex; + justify-content: center; + align-items: center; + right: 1px; + bottom: 1px; + height: 42px; + width: 42px; + border-left: none; + border-radius: 0 4px 4px 0; + background: ${(props) => arrowDownBackground(props)}; + pointer-events: none; + color: ${arrowColor}; + + @media (min-width: ${Width.SM}px) { + width: ${({ thin }) => thin && '34px'}; + height: ${({ thin }) => thin && '32px'}; + } +`; + +const SelectWrapper = styled.div` + position: relative; +`; + +type SelectProps = { + /** The className */ + className?: string; + /** How many columns to span */ + colSpan?: number; + /** Whether or not the input is disabled */ + disabled?: boolean; + /** The id for the input */ + id: string; + /** The label for the input */ + label: string; + /** Value that controls the input */ + value: string; + /** Used for validation */ + errorText?: string; + /** Function used to update the value prop */ + onChange: ChangeEventHandler; + /** Thin is used for smaller height selects in tighter designs */ + thin?: boolean; +}; + +const Select: FunctionComponent = ({ className, colSpan, disabled, id, label, value, errorText, onChange, children, thin, ...rest }) => { + const currentInput = useWhatInput('input'); + + const inputProps = { + ...rest, + disabled, + id, + value, + onChange, + errorText, + thin, + $isKeyboard: currentInput === 'keyboard', + }; + + return ( + + + + {children} + + + + + {errorText} + + ); +}; + +Select.defaultProps = { + className: '', + colSpan: 1, + disabled: false, + errorText: '', + thin: false, +}; + +export default Select; \ No newline at end of file diff --git a/src/components/Form/Select/index.ts b/src/components/Form/Select/index.ts new file mode 100644 index 0000000..5ac74ef --- /dev/null +++ b/src/components/Form/Select/index.ts @@ -0,0 +1 @@ +export * from './Select'; \ No newline at end of file diff --git a/src/components/KeyManagement/AddKeyModalContainer.tsx b/src/components/KeyManagement/AddKeyModalContainer.tsx index 623e9e7..c458b54 100644 --- a/src/components/KeyManagement/AddKeyModalContainer.tsx +++ b/src/components/KeyManagement/AddKeyModalContainer.tsx @@ -10,6 +10,10 @@ import { H4 } from 'components/Text'; import { FlexContainer } from 'components/Layout/Flex'; import { Checkbox, FormWrapper, TextInput } from 'components/Form'; import { Modal } from 'components/Modal'; +import { KEY_PROVIDERS, KEY_PROVIDERS_ARRAY, KEY_PROVIDER_META } from 'Constant/affiliate'; +import { Dropdown } from 'components/Dropdown'; +import Select from 'components/Form/Select/Select'; +import styled from 'styled-components'; export const AddServiceKeyModal: FunctionComponent = () => { const { serviceAccountKey, addServiceKey, serviceAccountKeyFetched } = usePublicKeys(); @@ -23,6 +27,10 @@ export const AddServiceKeyModal: FunctionComponent = () => { return } +const SubText = styled.p` + margin-top: -10px; +` + const AddKeyModal: FunctionComponent> = ({ keyType = 'CONTRACT', isOpen, onClose = () => {}, isSubmitting, handleSubmit, setFieldValue, values, errors, touched }) => { const closable = keyType === 'CONTRACT'; @@ -35,9 +43,16 @@ const AddKeyModal: FunctionComponent setFieldValue('alias', e.target.value)} /> {keyType === 'CONTRACT' && setFieldValue('indexName', e.target.value)}/>} - setFieldValue('signingPrivateKey', e.target.value)} /> - {keyType === 'CONTRACT' && setFieldValue('useSigningKeyForEncryption', !values.useSigningKeyForEncryption)} inline={false} />} - {keyType === 'CONTRACT' && setFieldValue('encryptionPrivateKey', e.target.value)} />} + {keyType === 'CONTRACT' && <> + + {KEY_PROVIDER_META[values.keyProvider]?.description} + } + {values.keyProvider === KEY_PROVIDERS.DATABASE && <> + setFieldValue('signingPrivateKey', e.target.value)} /> + {keyType === 'CONTRACT' && setFieldValue('encryptionPrivateKey', e.target.value)} />} + } @@ -56,9 +71,9 @@ interface AddKeyModalContainerProps { export interface AddKeyFields { alias: string; indexName: string; + keyProvider: KEY_PROVIDERS; signingPrivateKey: string; encryptionPrivateKey: string; - useSigningKeyForEncryption: boolean; } export const AddKeyModalContainer = withFormik({ @@ -67,18 +82,24 @@ export const AddKeyModalContainer = withFormik ({ alias: '', indexName: '', + keyProvider: KEY_PROVIDERS.SMART_KEY, signingPrivateKey: '', - useSigningKeyForEncryption: true, encryptionPrivateKey: '', }), validationSchema: (props) => yup.object().shape({ alias: yup.string(), indexName: props.keyType === 'CONTRACT' ? yup.string().required("Index Name is required").matches(/^[^\\/*?"<>| ,#:-_+.]+[^\\/*?"<>| ,#:]*$/, "Index Name cannot contain spaces or any of the following characters: \\/*?\"<>|,#: and can not start with any of the following characters: -_+") : yup.string(), - useSigningKeyForEncryption: yup.boolean(), - signingPrivateKey: yup.string().matches(/^[0-9a-fA-F]*$/, "Key must be in hex format"), - encryptionPrivateKey: yup.string().matches(/^[0-9a-fA-F]*$/, "Key must be in hex format"), - }), + keyProvider: yup.string().oneOf(Object.values(KEY_PROVIDERS)), + signingPrivateKey: yup.string().matches(/^[0-9a-fA-F]*$/, "Key must be in hex format").when('encryptionPrivateKey', { + is: k => !!k?.trim(), + then: yup.string().required("Both keys must be supplied when importing") + }), + encryptionPrivateKey: yup.string().matches(/^[0-9a-fA-F]*$/, "Key must be in hex format").when('signingPrivateKey', { + is: k => !!k?.trim(), + then: yup.string().required("Both keys must be supplied when importing") + }), + }, [['signingPrivateKey', 'encryptionPrivateKey']]), handleSubmit: (values, { props, resetForm, setSubmitting }) => { props.addKey(values).then(() => { diff --git a/src/components/KeyManagement/DisplayPrivateKeysModal.tsx b/src/components/KeyManagement/DisplayPrivateKeysModal.tsx index 278affb..f52660b 100644 --- a/src/components/KeyManagement/DisplayPrivateKeysModal.tsx +++ b/src/components/KeyManagement/DisplayPrivateKeysModal.tsx @@ -17,34 +17,20 @@ export const DisplayPrivateKeysModal: FunctionComponent -

Below are the newly generated Signing/Encryption key pairs.

-

Please save these to a secure location, as this is the only time you will be able to view the private keys.

+ return +

Below is the newly generated Auth key pair.

+

Please save these to a secure location, as this is the only time you will be able to view the private key.

- Signing Key + Auth Key
Public:
-

{contractKey.signingKey.hexPublicKey}

+

{contractKey.authKey.hexPublicKey}

Private:
-

{contractKey.signingKey.hexPrivateKey}

-
-
-
- - Encryption Key - - - -
Public:
-

{contractKey.encryptionKey.hexPublicKey}

-
- -
Private:
-

{contractKey.encryptionKey.hexPrivateKey}

+

{contractKey.authKey.hexPrivateKey}

diff --git a/src/components/KeyManagement/KeyManager.tsx b/src/components/KeyManagement/KeyManager.tsx index 9c8be74..da31bc0 100644 --- a/src/components/KeyManagement/KeyManager.tsx +++ b/src/components/KeyManagement/KeyManager.tsx @@ -30,38 +30,30 @@ export const KeyCard: FunctionComponent = ({ keyType, contractOrSe const publicKey = keyType === 'CONTRACT' ? (contractOrServiceKey as ContractKey).signingKey : (contractOrServiceKey as ServiceKey).publicKey; const encryptionPublicKey = keyType === 'CONTRACT' ? (contractOrServiceKey as ContractKey).encryptionKey : null; + const authPublicKey = keyType === 'CONTRACT' ? (contractOrServiceKey as ContractKey).authKey : null; const updateAlias = (alias: string) => updateKey(keyType, publicKey.hexPublicKey, alias); return - -
Signing Public Key
+ {([ + { label: 'Signing Public Key', value: publicKey }, + { label: 'Encryption Public Key', value: encryptionPublicKey }, + { label: 'Auth Public Key', value: authPublicKey } + ] as const).filter(({ value }) => !!value).map(key => +
{key.label}

Hex:  -

{publicKey.hexPublicKey}
+
{key.value?.hexPublicKey}
Base 64:  -
{publicKey.publicKey}
+
{key.value?.publicKey}

-
- {encryptionPublicKey && -
Encryption Public Key
-

- - Hex:  -

{encryptionPublicKey.hexPublicKey}
- - - Base 64:  -
{encryptionPublicKey.publicKey}
-
-

-
} +
)} {keyType === 'CONTRACT' &&
Index Name

{(contractOrServiceKey as ContractKey).indexName}

diff --git a/src/components/KeyManagement/KeyManagerContainer.tsx b/src/components/KeyManagement/KeyManagerContainer.tsx index 87f4af6..f2da326 100644 --- a/src/components/KeyManagement/KeyManagerContainer.tsx +++ b/src/components/KeyManagement/KeyManagerContainer.tsx @@ -14,7 +14,7 @@ export const KeyManagerContainer = () => { const handleAddKey = (type: KeyTypes) => setAddingKey({ adding: !addingKey.adding, type }); const [newKey, setNewKey] = useState(null) - const handleCreateKey = ({ signingPrivateKey, encryptionPrivateKey, useSigningKeyForEncryption, indexName, alias }: AddKeyFields) => addContractKey(signingPrivateKey, encryptionPrivateKey, useSigningKeyForEncryption, indexName, alias) + const handleCreateKey = ({ signingPrivateKey, encryptionPrivateKey, keyProvider, indexName, alias }: AddKeyFields) => addContractKey(signingPrivateKey, encryptionPrivateKey, keyProvider, indexName, alias) .then((key) => { setNewKey(key); resetAddingKey() diff --git a/src/hooks/key-hooks.ts b/src/hooks/key-hooks.ts index 63b8e22..14f6b0a 100644 --- a/src/hooks/key-hooks.ts +++ b/src/hooks/key-hooks.ts @@ -2,12 +2,13 @@ import { useDispatch, useSelector } from 'react-redux'; import { useEffect } from 'react'; import { getKeys, fetchServiceAccountKey, removeServiceAccountKey, addContractKey as addContractKeyAction, addServiceKey as addServiceKeyAction, updateContractKey, updateServiceKey, fetchContractKeyShares, addContractKeyShare, removeContractKeyShare, linkServiceKey, unlinkServiceKey } from 'actions'; import { KeyTypes, ContractKey, ServiceKey, KeyShare } from 'models/keys'; +import { KEY_PROVIDERS } from 'Constant/affiliate'; export const usePublicKeys = () => { const dispatch = useDispatch(); const { currentPublicKey, publicKeys, serviceAccountKey, serviceAccountKeyFetched } = useSelector(({ keyReducer }) => keyReducer); - const addContractKey = (signingPrivateKey: string, encryptionPrivateKey: string, useSigningKeyForEncryption: boolean, indexName: string, alias?: string): Promise => dispatch(addContractKeyAction(signingPrivateKey, encryptionPrivateKey, useSigningKeyForEncryption, indexName, alias)); + const addContractKey = (signingPrivateKey: string, encryptionPrivateKey: string, keyProvider: KEY_PROVIDERS, indexName: string, alias?: string): Promise => dispatch(addContractKeyAction(signingPrivateKey, encryptionPrivateKey, keyProvider, indexName, alias)); const addServiceKey = (privateKey: string, alias: string): Promise => dispatch(addServiceKeyAction(privateKey, alias)); const updateKey = (type: KeyTypes, publicKey: string, alias: string): Promise => { const action = (type === 'CONTRACT' ? updateContractKey : updateServiceKey) diff --git a/src/models/keys.ts b/src/models/keys.ts index 39d70f2..878506e 100644 --- a/src/models/keys.ts +++ b/src/models/keys.ts @@ -5,6 +5,7 @@ export interface ContractKey { indexName: string; signingKey: P8EPublicKey; encryptionKey: P8EPublicKey; + authKey: P8EPublicKey; serviceKeys: ServiceKey[]; } From 6c62cececfdc5a31fa5fc25a2ca039ad9d44ee27 Mon Sep 17 00:00:00 2001 From: Pierce Trey Date: Fri, 21 May 2021 16:21:39 -0600 Subject: [PATCH 2/3] fix warnings --- src/components/Form/Select/Select.tsx | 2 +- src/components/KeyManagement/AddKeyModalContainer.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Form/Select/Select.tsx b/src/components/Form/Select/Select.tsx index 87ee8f6..e27e6b4 100644 --- a/src/components/Form/Select/Select.tsx +++ b/src/components/Form/Select/Select.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEventHandler, FunctionComponent, ReactChildren } from 'react'; +import React, { ChangeEventHandler, FunctionComponent } from 'react'; import styled, { css } from 'styled-components'; import theme from 'styled-theming'; import useWhatInput from 'react-use-what-input'; diff --git a/src/components/KeyManagement/AddKeyModalContainer.tsx b/src/components/KeyManagement/AddKeyModalContainer.tsx index c458b54..2455e37 100644 --- a/src/components/KeyManagement/AddKeyModalContainer.tsx +++ b/src/components/KeyManagement/AddKeyModalContainer.tsx @@ -8,10 +8,9 @@ import { KeyTypes } from 'models/keys'; import { Sprite } from 'components/Sprite'; import { H4 } from 'components/Text'; import { FlexContainer } from 'components/Layout/Flex'; -import { Checkbox, FormWrapper, TextInput } from 'components/Form'; +import { FormWrapper, TextInput } from 'components/Form'; import { Modal } from 'components/Modal'; import { KEY_PROVIDERS, KEY_PROVIDERS_ARRAY, KEY_PROVIDER_META } from 'Constant/affiliate'; -import { Dropdown } from 'components/Dropdown'; import Select from 'components/Form/Select/Select'; import styled from 'styled-components'; From b07dbcb95381a8d1e8ceac6f17be39241fa82e7d Mon Sep 17 00:00:00 2001 From: Pierce Trey Date: Wed, 30 Jun 2021 13:45:50 -0600 Subject: [PATCH 3/3] fix SmartKey provider name --- src/Constant/affiliate.ts | 4 ++-- src/components/KeyManagement/AddKeyModalContainer.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Constant/affiliate.ts b/src/Constant/affiliate.ts index c0f3654..70cb25c 100644 --- a/src/Constant/affiliate.ts +++ b/src/Constant/affiliate.ts @@ -1,11 +1,11 @@ export enum KEY_PROVIDERS { DATABASE = 'DATABASE', - SMART_KEY = 'SMART_KEY', + SMARTKEY = 'SMARTKEY', } export const KEY_PROVIDER_META = { [KEY_PROVIDERS.DATABASE]: { label: 'Database', description: 'Store keys directly in the database. Note: this is a less secure method of key storage' }, - [KEY_PROVIDERS.SMART_KEY]: { label: 'SmartKey', description: 'Use external cloud Hardware Security Module (HSM) provider Equinix SmartKey® to securely store private keys' }, + [KEY_PROVIDERS.SMARTKEY]: { label: 'SmartKey', description: 'Use external cloud Hardware Security Module (HSM) provider Equinix SmartKey® to securely store private keys' }, } as const export const KEY_PROVIDERS_ARRAY = Object.entries(KEY_PROVIDERS).map(([key, value]) => ({ ...KEY_PROVIDER_META[key], value })); \ No newline at end of file diff --git a/src/components/KeyManagement/AddKeyModalContainer.tsx b/src/components/KeyManagement/AddKeyModalContainer.tsx index 2455e37..fb88f0e 100644 --- a/src/components/KeyManagement/AddKeyModalContainer.tsx +++ b/src/components/KeyManagement/AddKeyModalContainer.tsx @@ -81,7 +81,7 @@ export const AddKeyModalContainer = withFormik ({ alias: '', indexName: '', - keyProvider: KEY_PROVIDERS.SMART_KEY, + keyProvider: KEY_PROVIDERS.SMARTKEY, signingPrivateKey: '', encryptionPrivateKey: '', }),