Skip to content

Commit

Permalink
key provider selection and auth key support
Browse files Browse the repository at this point in the history
  • Loading branch information
celloman committed May 21, 2021
1 parent b242d95 commit d1eec2f
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 52 deletions.
11 changes: 11 additions & 0 deletions src/Constant/affiliate.ts
Original file line number Diff line number Diff line change
@@ -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 }));
5 changes: 3 additions & 2 deletions src/actions/key-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -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 })
Expand Down
3 changes: 2 additions & 1 deletion src/components/Form/InputGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { HTMLProps } from 'react';
import styled from 'styled-components';

type InputGroupProps = {
/** Overrides the default margin */
margin?: string;
disabled?: boolean;
}
} & HTMLProps<HTMLDivElement>

export const InputGroup = styled.div<InputGroupProps>`
margin: ${({ margin }) => (margin ? margin : '20px 0')};
Expand Down
193 changes: 193 additions & 0 deletions src/components/Form/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -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<ElementProps>(({ errorText }) => ({
'data-error': errorText && errorText.length > 0,
}))<ElementProps>`
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<ArrowDownProps>`
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<HTMLSelectElement>;
/** Thin is used for smaller height selects in tighter designs */
thin?: boolean;
};

const Select: FunctionComponent<SelectProps> = ({ 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 (
<InputGroup className={className} colSpan={colSpan} disabled={disabled}>
<Label htmlFor={id}>{label}</Label>
<SelectWrapper>
<Element {...inputProps}>{children}</Element>
<ArrowDownContainer thin={thin}>
<Sprite icon={Icon.CARET} color="currentColor" size="9px" />
</ArrowDownContainer>
</SelectWrapper>
<Error>{errorText}</Error>
</InputGroup>
);
};

Select.defaultProps = {
className: '',
colSpan: 1,
disabled: false,
errorText: '',
thin: false,
};

export default Select;
1 change: 1 addition & 0 deletions src/components/Form/Select/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Select';
39 changes: 30 additions & 9 deletions src/components/KeyManagement/AddKeyModalContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -23,6 +27,10 @@ export const AddServiceKeyModal: FunctionComponent = () => {
return <AddKeyModalContainer keyType="SERVICE" isOpen={!serviceAccountKey} addKey={handleAddKey}></AddKeyModalContainer>
}

const SubText = styled.p`
margin-top: -10px;
`

const AddKeyModal: FunctionComponent<AddKeyModalContainerProps & FormikProps<AddKeyFields>> = ({ keyType = 'CONTRACT', isOpen, onClose = () => {}, isSubmitting, handleSubmit, setFieldValue, values, errors, touched }) => {
const closable = keyType === 'CONTRACT';

Expand All @@ -35,9 +43,16 @@ const AddKeyModal: FunctionComponent<AddKeyModalContainerProps & FormikProps<Add
<FormWrapper>
<TextInput disabled={isSubmitting} label="Alias" id="alias" value={values.alias} onChange={e => setFieldValue('alias', e.target.value)} />
{keyType === 'CONTRACT' && <TextInput disabled={isSubmitting} label="Index Name*" id="indexName" value={values.indexName} errorText={errors.indexName} onChange={e => setFieldValue('indexName', e.target.value)}/>}
<TextInput disabled={isSubmitting} placeholder="leave blank to generate a new key" label="Signing Private Key" id="signingPrivatekey" value={values.signingPrivateKey} errorText={errors.signingPrivateKey} onChange={(e) => setFieldValue('signingPrivateKey', e.target.value)} />
{keyType === 'CONTRACT' && <Checkbox label="Use same key for both signing and encryption" value="useSigningKeyForEncryption" id="useSigningKeyForEncryption" checked={values.useSigningKeyForEncryption} onChange={e => setFieldValue('useSigningKeyForEncryption', !values.useSigningKeyForEncryption)} inline={false} />}
{keyType === 'CONTRACT' && <TextInput disabled={isSubmitting || values.useSigningKeyForEncryption} placeholder="leave blank to generate a new key" label="Encryption Private Key" id="encryptionPrivatekey" value={values.encryptionPrivateKey} errorText={errors.encryptionPrivateKey} onChange={(e) => setFieldValue('encryptionPrivateKey', e.target.value)} />}
{keyType === 'CONTRACT' && <>
<Select id="keyProvider" disabled={isSubmitting} value={values.keyProvider.toString()} label="Key Provider" onChange={e => setFieldValue('keyProvider', e.target.value)}>
{KEY_PROVIDERS_ARRAY.map(kp => <option aria-label={kp.description} key={kp.value} value={kp.value}>{kp.label}</option>)}
</Select>
<SubText>{KEY_PROVIDER_META[values.keyProvider]?.description}</SubText>
</>}
{values.keyProvider === KEY_PROVIDERS.DATABASE && <>
<TextInput disabled={isSubmitting} placeholder="leave blank to generate a new key" label="Signing Private Key" id="signingPrivatekey" value={values.signingPrivateKey} errorText={errors.signingPrivateKey} onChange={(e) => setFieldValue('signingPrivateKey', e.target.value)} />
{keyType === 'CONTRACT' && <TextInput disabled={isSubmitting} placeholder="leave blank to generate a new key" label="Encryption Private Key" id="encryptionPrivatekey" value={values.encryptionPrivateKey} errorText={errors.encryptionPrivateKey} onChange={(e) => setFieldValue('encryptionPrivateKey', e.target.value)} />}
</>}
</FormWrapper>
<ButtonGroup>
<Button disabled={isSubmitting} type="submit">Add Key</Button>
Expand All @@ -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<AddKeyModalContainerProps, AddKeyFields>({
Expand All @@ -67,18 +82,24 @@ export const AddKeyModalContainer = withFormik<AddKeyModalContainerProps, AddKey
mapPropsToValues: () => ({
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(() => {
Expand Down
26 changes: 6 additions & 20 deletions src/components/KeyManagement/DisplayPrivateKeysModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,20 @@ export const DisplayPrivateKeysModal: FunctionComponent<DisplayPrivateKeysModalP
setCloseable(true);
}, []);

return <Modal header={`Signing/Encryption Key Pairs${contractKey.alias ? ` for ${contractKey.alias}` : ''}`} closable={closeable} onClose={onClose}>
<p>Below are the newly generated Signing/Encryption key pairs.</p>
<p><b>Please save these to a secure location, as this is the only time you will be able to view the private keys.</b></p>
return <Modal header={`Auth Key Pair${contractKey.alias ? ` for ${contractKey.alias}` : ''}`} closable={closeable} onClose={onClose}>
<p>Below is the newly generated Auth key pair.</p>
<p><b>Please save these to a secure location, as this is the only time you will be able to view the private key.</b></p>

<CardHeader>Signing Key</CardHeader>
<CardHeader>Auth Key</CardHeader>
<Card>
<HorizontalTable>
<HorizontalTableRow>
<H5>Public:</H5>
<p>{contractKey.signingKey.hexPublicKey}</p>
<p>{contractKey.authKey.hexPublicKey}</p>
</HorizontalTableRow>
<HorizontalTableRow>
<H5>Private:</H5>
<p>{contractKey.signingKey.hexPrivateKey}</p>
</HorizontalTableRow>
</HorizontalTable>
</Card>

<CardHeader>Encryption Key</CardHeader>
<Card>
<HorizontalTable>
<HorizontalTableRow>
<H5>Public:</H5>
<p>{contractKey.encryptionKey.hexPublicKey}</p>
</HorizontalTableRow>
<HorizontalTableRow>
<H5>Private:</H5>
<p>{contractKey.encryptionKey.hexPrivateKey}</p>
<p>{contractKey.authKey.hexPrivateKey}</p>
</HorizontalTableRow>
</HorizontalTable>
</Card>
Expand Down
Loading

0 comments on commit d1eec2f

Please sign in to comment.