Skip to content

Commit

Permalink
Add encrypt/decrypt module on data source plugin
Browse files Browse the repository at this point in the history
Signed-off-by: Louis Chu <[email protected]>
  • Loading branch information
noCharger committed Aug 11, 2022
1 parent 5e59e5d commit 1b6519c
Show file tree
Hide file tree
Showing 18 changed files with 666 additions and 38 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
]
},
"dependencies": {
"@aws-crypto/client-node": "^3.1.1",
"@elastic/datemath": "5.0.3",
"@elastic/eui": "34.6.0",
"@elastic/good": "^9.0.1-kibana3",
Expand Down
3 changes: 3 additions & 0 deletions src/dev/jest/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,7 @@ export default {
'<rootDir>/node_modules/enzyme-to-json/serializer',
],
reporters: ['default', '<rootDir>/src/dev/jest/junit_reporter.js'],
globals: {
Uint8Array: Uint8Array,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@ import {
EuiFieldPassword,
} from '@elastic/eui';
import { DocLinksStart } from 'src/core/public';
import { Credential } from 'src/plugins/data_source/common';

import { getCreateBreadcrumbs } from '../breadcrumbs';
import { CredentialManagmentContextValue } from '../../types';
import { Header } from './components/header';
import { context as contextType } from '../../../../opensearch_dashboards_react/public';

const SHARED_AUTH_TYPE: Credential.SharedAuthType = 'shared';
const NO_AUTH: Credential.NoAuthType = 'no_auth';
const USERNAME_PASSWORD: Credential.UsernamePasswordType = 'username_password';

interface CreateCredentialWizardState {
credentialName: string;
credentialName?: string;
authType: string;
credentialMaterialsType: string;
userName: string;
password: string;
credentialMaterialsType?: string;
username?: string;
password?: string;
dual: boolean;
toasts: EuiGlobalToastListToast[];
docLinks: DocLinksStart;
Expand All @@ -49,11 +55,11 @@ export class CreateCredentialWizard extends React.Component<
context.services.setBreadcrumbs(getCreateBreadcrumbs());

this.state = {
credentialName: '',
authType: 'shared',
credentialMaterialsType: 'username_password_credential',
userName: '',
password: '',
credentialName: undefined,
authType: SHARED_AUTH_TYPE,
credentialMaterialsType: undefined,
username: undefined,
password: undefined,
dual: true,
toasts: [],
docLinks: context.services.docLinks,
Expand All @@ -71,8 +77,9 @@ export class CreateCredentialWizard extends React.Component<
const header = this.renderHeader();

const options = [
{ value: 'username_password_credential', text: 'Username and Password Credential' },
{ value: 'no_auth', text: 'No Auth' },
{ value: undefined, text: 'Select Credential Materials Type' },
{ value: USERNAME_PASSWORD, text: 'Username and Password Credential' },
{ value: NO_AUTH, text: 'No Auth' },
];

return (
Expand Down Expand Up @@ -127,8 +134,8 @@ export class CreateCredentialWizard extends React.Component<
<EuiFormRow label="User Name">
<EuiFieldText
placeholder="Your User Name"
value={this.state.userName || ''}
onChange={(e) => this.setState({ userName: e.target.value })}
value={this.state.username || ''}
onChange={(e) => this.setState({ username: e.target.value })}
/>
</EuiFormRow>
<EuiFormRow label="Password">
Expand Down Expand Up @@ -181,7 +188,7 @@ export class CreateCredentialWizard extends React.Component<
credentialMaterials: {
credentialMaterialsType: this.state.credentialMaterialsType,
credentialMaterialsContent: {
userName: this.state.userName,
username: this.state.username,
password: this.state.password,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,23 @@ import {
EuiConfirmModal,
} from '@elastic/eui';
import { DocLinksStart } from 'src/core/public';
import { Credential } from 'src/plugins/data_source/common';

import { getCreateBreadcrumbs } from '../breadcrumbs';
import { CredentialManagmentContextValue } from '../../types';
// TODO: Add Header https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2051
import { context as contextType } from '../../../../opensearch_dashboards_react/public';
import { CredentialEditPageItem } from '../types';
import * as localizedContent from '../text_content/text_content';
import { localizedContent } from '../text_content';

const NO_AUTH: Credential.NoAuthType = 'no_auth';
const USERNAME_PASSWORD: Credential.UsernamePasswordType = 'username_password';

interface EditCredentialState {
credentialName: string;
credentialMaterialsType: string;
userName: string;
password: string;
username?: string;
password?: string;
dual: boolean;
toasts: EuiGlobalToastListToast[];
docLinks: DocLinksStart;
Expand All @@ -61,8 +66,8 @@ export class EditCredentialComponent extends React.Component<
this.state = {
credentialName: props.credential.title,
credentialMaterialsType: props.credential.credentialMaterialsType,
userName: '',
password: '',
username: undefined,
password: undefined,
dual: true,
toasts: [],
docLinks: context.services.docLinks,
Expand Down Expand Up @@ -139,11 +144,8 @@ export class EditCredentialComponent extends React.Component<
// TODO: Add rendering spanner https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2050
renderContent() {
const options = [
{
value: 'username_password_credential',
text: 'Username and Password Credential',
},
{ value: 'no_auth', text: 'No Auth' },
{ value: USERNAME_PASSWORD, text: 'Username and Password Credential' },
{ value: NO_AUTH, text: 'No Auth' },
];

return (
Expand Down Expand Up @@ -198,8 +200,8 @@ export class EditCredentialComponent extends React.Component<
<EuiFormRow label="User Name">
<EuiFieldText
placeholder="Your User Name"
value={this.state.userName || ''}
onChange={(e) => this.setState({ userName: e.target.value })}
value={this.state.username || ''}
onChange={(e) => this.setState({ username: e.target.value })}
/>
</EuiFormRow>
<EuiFormRow label="Password">
Expand Down Expand Up @@ -251,10 +253,11 @@ export class EditCredentialComponent extends React.Component<
try {
await savedObjects.client.update('credential', this.props.credential.id, {
title: this.state.credentialName,
authType: this.props.credential.authType,
credentialMaterials: {
credentialMaterialsType: this.state.credentialMaterialsType,
credentialMaterialsContent: {
userName: this.state.userName,
username: this.state.username,
password: this.state.password,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const EditCredentialPage: React.FC<RouteComponentProps<{ id: string }>> = ({ ...
const object = {
id: savedObject.id,
title: savedObject.attributes.title,
authType: savedObject.attributes.authType,
credentialMaterialsType: savedObject.attributes.credentialMaterials.credentialMaterialsType,
};
setCredential(object);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * as localizedContent from '../text_content/text_content';
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export interface CredentialsTableItem {
export interface CredentialEditPageItem {
id: string;
title: string;
authType: string;
credentialMaterialsType: string;
}
6 changes: 6 additions & 0 deletions src/plugins/data_source/common/credentials/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * as Credential from './types';
27 changes: 27 additions & 0 deletions src/plugins/data_source/common/credentials/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectAttributes } from 'src/core/types';

export type SharedAuthType = 'shared';
export type UsernamePasswordType = 'username_password';
export type NoAuthType = 'no_auth';

export interface CredentialSavedObjectAttributes extends SavedObjectAttributes {
title: string;
authType: SharedAuthType;
credentialMaterials: CredentialMaterials;
description?: string;
}

export interface CredentialMaterials extends SavedObjectAttributes {
credentialMaterialsType: UsernamePasswordType | NoAuthType;
credentialMaterialsContent?: UsernamePasswordTypedContent;
}

export interface UsernamePasswordTypedContent extends SavedObjectAttributes {
username: string;
password: string;
}
2 changes: 2 additions & 0 deletions src/plugins/data_source/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@

export const PLUGIN_ID = 'dataSource';
export const PLUGIN_NAME = 'data_source';

export { Credential } from './credentials';
33 changes: 33 additions & 0 deletions src/plugins/data_source/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { schema, TypeOf } from '@osd/config-schema';

const KEY_NAME_MIN_LENGTH: number = 1;
const KEY_NAME_MAX_LENGTH: number = 100;
// Wrapping key size shoule be 32 bytes, as used in envelope encryption algorithms.
const WRAPPING_KEY_SIZE: number = 32;

export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
wrappingKeyName: schema.string({
minLength: KEY_NAME_MIN_LENGTH,
maxLength: KEY_NAME_MAX_LENGTH,
defaultValue: 'changeme',
}),
wrappingKeyNamespace: schema.string({
minLength: KEY_NAME_MIN_LENGTH,
maxLength: KEY_NAME_MAX_LENGTH,
defaultValue: 'changeme',
}),
wrappingKey: schema.maybe(
schema.arrayOf(schema.number(), {
minSize: WRAPPING_KEY_SIZE,
maxSize: WRAPPING_KEY_SIZE,
})
),
});

export type DataSourcePluginConfigType = TypeOf<typeof configSchema>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CryptographyClient } from './cryptography_client';
import { randomBytes } from 'crypto';

const dummyWrappingKeyName = 'dummy_wrapping_key_name';
const dummyWrappingKeyNamespace = 'dummy_wrapping_key_namespace';

test('Invalid wrapping key size throws error', () => {
const dummyRandomBytes = [...randomBytes(31)];
const expectedErrorMsg = `Wrapping key size shoule be 32 bytes, as used in envelope encryption. Current wrapping key size: '${dummyRandomBytes.length}' bytes`;
expect(() => {
new CryptographyClient(dummyWrappingKeyName, dummyWrappingKeyNamespace, dummyRandomBytes);
}).toThrowError(new Error(expectedErrorMsg));
});

describe('Test encrpyt and decrypt module', () => {
const dummyPlainText = 'dummy';
const dummyNumArray1 = [...randomBytes(32)];
const dummyNumArray2 = [...randomBytes(32)];

describe('Positive test cases', () => {
test('Encrypt and Decrypt with same in memory keyring', async () => {
const cryptographyClient = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient.encryptAndEncode(dummyPlainText);
const outputText = await cryptographyClient.decodeAndDecrypt(encrypted);
expect(outputText).toBe(dummyPlainText);
});
test('Encrypt and Decrypt with two different keyrings with exact same identifiers', async () => {
const cryptographyClient1 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient1.encryptAndEncode(dummyPlainText);

const cryptographyClient2 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const outputText = await cryptographyClient2.decodeAndDecrypt(encrypted);
expect(cryptographyClient1 === cryptographyClient2).toBeFalsy();
expect(outputText).toBe(dummyPlainText);
});
});

describe('Negative test cases', () => {
const defaultWrappingKeyName = 'changeme';
const defaultWrappingKeyNamespace = 'changeme';
const expectedErrorMsg = 'unencryptedDataKey has not been set';
test('Encrypt and Decrypt with different key names', async () => {
const cryptographyClient1 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient1.encryptAndEncode(dummyPlainText);

const cryptographyClient2 = new CryptographyClient(
defaultWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
try {
await cryptographyClient2.decodeAndDecrypt(encrypted);
} catch (error) {
expect(error.message).toMatch(expectedErrorMsg);
}
});
test('Encrypt and Decrypt with different key namespaces', async () => {
const cryptographyClient1 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient1.encryptAndEncode(dummyPlainText);

const cryptographyClient2 = new CryptographyClient(
dummyWrappingKeyName,
defaultWrappingKeyNamespace,
dummyNumArray1
);
try {
await cryptographyClient2.decodeAndDecrypt(encrypted);
} catch (error) {
expect(error.message).toMatch(expectedErrorMsg);
}
});
test('Encrypt and Decrypt with different wrapping keys', async () => {
const cryptographyClient1 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient1.encryptAndEncode(dummyPlainText);

const cryptographyClient2 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray2
);
try {
await cryptographyClient2.decodeAndDecrypt(encrypted);
} catch (error) {
expect(error.message).toMatch(expectedErrorMsg);
}
});
});
});
Loading

0 comments on commit 1b6519c

Please sign in to comment.