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 10, 2022
1 parent 456b5c7 commit 4d420a2
Show file tree
Hide file tree
Showing 14 changed files with 488 additions and 20 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
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface CreateCredentialWizardState {
credentialName: string;
authType: string;
credentialMaterialsType: string;
userName: string;
username: string;
password: string;
dual: boolean;
toasts: EuiGlobalToastListToast[];
Expand All @@ -51,8 +51,8 @@ export class CreateCredentialWizard extends React.Component<
this.state = {
credentialName: '',
authType: 'shared',
credentialMaterialsType: 'username_password_credential',
userName: '',
credentialMaterialsType: 'username_password',
username: '',
password: '',
dual: true,
toasts: [],
Expand All @@ -71,7 +71,7 @@ export class CreateCredentialWizard extends React.Component<
const header = this.renderHeader();

const options = [
{ value: 'username_password_credential', text: 'Username and Password Credential' },
{ value: 'username_password', text: 'Username and Password Credential' },
{ value: 'no_auth', text: 'No Auth' },
];

Expand Down Expand Up @@ -127,8 +127,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 +181,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
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';
31 changes: 31 additions & 0 deletions src/plugins/data_source/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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: 'wrappingKeyName',
}),
wrappingKeyNamespace: schema.string({
minLength: KEY_NAME_MIN_LENGTH,
maxLength: KEY_NAME_MAX_LENGTH,
defaultValue: 'wrappingKeyNamespace',
}),
wrappingKey: 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,20 @@
/*
* 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));
});

// TODO: Add more test cases for encrypt / decrypt https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2068
70 changes: 70 additions & 0 deletions src/plugins/data_source/server/cryptography/cryptography_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
buildClient,
CommitmentPolicy,
RawAesKeyringNode,
RawAesWrappingSuiteIdentifier,
} from '@aws-crypto/client-node';

export const ENCODING_STRATEGY: BufferEncoding = 'base64';
export const WRAPPING_KEY_SIZE: number = 32;

export class CryptographyClient {
private readonly _commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT;
private readonly _wrappingSuite = RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING;

private readonly _keyring: RawAesKeyringNode;

private readonly _encrypt;
private readonly _decrypt;

/**
* @param {string} wrappingKeyName
* @param {string} wrappingKeyNamespace
* @param {number[]} wrappingKey 32 Bytes raw wrapping key used to perform envelope encryption
*/
constructor(wrappingKeyName: string, wrappingKeyNamespace: string, wrappingKey: number[]) {
if (wrappingKey.length !== WRAPPING_KEY_SIZE) {
const wrappingKeySizeMismatchMsg = `Wrapping key size shoule be 32 bytes, as used in envelope encryption. Current wrapping key size: '${wrappingKey.length}' bytes`;
throw new Error(wrappingKeySizeMismatchMsg);
}

// Create raw AES keyring
this._keyring = new RawAesKeyringNode({
keyName: wrappingKeyName,
keyNamespace: wrappingKeyNamespace,
unencryptedMasterKey: new Uint8Array(wrappingKey),
wrappingSuite: this._wrappingSuite,
});

// Destructuring encrypt and decrypt functions from client
const { encrypt, decrypt } = buildClient(this._commitmentPolicy);

this._encrypt = encrypt;
this._decrypt = decrypt;
}

/**
* Input text content and output encrypted string encoded with ENCODING_STRATEGY
* @param {string} plainText
* @returns {Promise}
*/
public async encrypt(plainText: string | Buffer): Promise<string> {
const result = await this._encrypt(this._keyring, plainText);
return result.result.toString(ENCODING_STRATEGY);
}

/**
* Input encrypted content and output decrypted string
* @param {string} encrypted
* @returns {Promise}
*/
public async decrypt(encrypted: string): Promise<string> {
const result = await this._decrypt(this._keyring, Buffer.from(encrypted, ENCODING_STRATEGY));
return result.plaintext.toString();
}
}
6 changes: 6 additions & 0 deletions src/plugins/data_source/server/cryptography/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 { CryptographyClient } from './cryptography_client';
8 changes: 1 addition & 7 deletions src/plugins/data_source/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { schema, TypeOf } from '@osd/config-schema';
import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server';
import { DataSourcePlugin } from './plugin';

export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
});

export type DataSourcePluginConfigType = TypeOf<typeof configSchema>;
import { configSchema, DataSourcePluginConfigType } from '../config';

export const config: PluginConfigDescriptor<DataSourcePluginConfigType> = {
schema: configSchema,
Expand Down
43 changes: 37 additions & 6 deletions src/plugins/data_source/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,65 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { first } from 'rxjs/operators';

import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'src/core/server';
import { dataSource, credential } from './saved_objects';
import { dataSource, credential, CredentialSavedObjectsClientWrapper } from './saved_objects';
import { DataSourcePluginConfigType } from '../config';

import { DataSourcePluginSetup, DataSourcePluginStart } from './types';

import { CryptographyClient } from './cryptography';

export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourcePluginStart> {
private readonly logger: Logger;
private readonly _logger: Logger;
private _initializerContext: PluginInitializerContext<DataSourcePluginConfigType>;

constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
this._initializerContext = initializerContext;
this._logger = initializerContext.logger.get();
}

public setup(core: CoreSetup) {
this.logger.debug('data_source: Setup');
public async setup(core: CoreSetup) {
this._logger.debug('data_source: Setup');

// Register credential saved object type
core.savedObjects.registerType(credential);

// Register data source saved object type
core.savedObjects.registerType(dataSource);

await this.createCryptographyClient().then((cryptographyClient) => {
// Create credential saved objects client wrapper
const credentialSavedObjectsClientWrapper = new CredentialSavedObjectsClientWrapper(
cryptographyClient
);

// Add credential saved objects client wrapper factory
core.savedObjects.addClientWrapper(
1,
'credential',
credentialSavedObjectsClientWrapper.wrapperFactory
);
});

return {};
}

public start(core: CoreStart) {
this.logger.debug('data_source: Started');
this._logger.debug('data_source: Started');
return {};
}

public stop() {}

private async createCryptographyClient() {
const {
wrappingKeyName,
wrappingKeyNamespace,
wrappingKey,
} = await this._initializerContext.config.create().pipe(first()).toPromise();

return new CryptographyClient(wrappingKeyName, wrappingKeyNamespace, wrappingKey);
}
}
Loading

0 comments on commit 4d420a2

Please sign in to comment.