diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 2d92658bc..4b6f83089 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -20,6 +20,7 @@ "bootstrap": "^4.5.3", "classnames": "^2.3.1", "core-js": "^3.18.2", + "crypto-js": "^4.1.1", "d3": "^5.14.1", "downloadjs": "^1.4.7", "fetch": "^1.1.0", @@ -4191,6 +4192,11 @@ "semver": "bin/semver" } }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "node_modules/css-loader": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", @@ -19166,6 +19172,11 @@ } } }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "css-loader": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index b244113df..b18bda191 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -76,6 +76,7 @@ "bootstrap": "^4.5.3", "classnames": "^2.3.1", "core-js": "^3.18.2", + "crypto-js": "^4.1.1", "d3": "^5.14.1", "downloadjs": "^1.4.7", "fetch": "^1.1.0", diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx index 9dc785f73..469d91ec3 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx @@ -1,53 +1,44 @@ -import {Button, Modal, Form} from 'react-bootstrap'; +import {Button, Form, Modal} from 'react-bootstrap'; import React, {useState} from 'react'; import FileSaver from 'file-saver'; -import AuthComponent from '../AuthComponent'; import '../../styles/components/configuration-components/ExportConfigModal.scss'; +import {encryptText} from '../utils/PasswordBasedEncryptor'; type Props = { show: boolean, + configuration: object, onHide: () => void } const ConfigExportModal = (props: Props) => { - // TODO: Change this endpoint to new agent-configuration endpoint - const configExportEndpoint = '/api/configuration/export'; - const [pass, setPass] = useState(''); const [radioValue, setRadioValue] = useState('password'); - const authComponent = new AuthComponent({}); function isExportBtnDisabled() { return pass === '' && radioValue === 'password'; } function onSubmit() { - authComponent.authFetch(configExportEndpoint, - { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - should_encrypt: (radioValue === 'password'), - password: pass - }) - } - ) - .then(res => res.json()) - .then(res => { - let configToExport = res['config_export']; - if (res['encrypted']) { - configToExport = new Blob([configToExport]); - } else { - configToExport = new Blob( - [JSON.stringify(configToExport, null, 2)], - {type: 'text/plain;charset=utf-8'} - ); - } - FileSaver.saveAs(configToExport, 'monkey.conf'); - props.onHide(); - }) + let config = props.configuration; + let config_export = {'metadata': {}, 'contents': null}; + + if (radioValue === 'password') { + config_export.contents = encryptText(JSON.stringify(config), pass); + config_export.metadata = {'encrypted': true}; + } else { + config_export.contents = config; + config_export.metadata = {'encrypted': false}; + } + + let export_json = JSON.stringify(config_export, null, 2); + let export_blob = new Blob( + [export_json], + {type: 'text/plain;charset=utf-8'} + ); + FileSaver.saveAs(export_blob, 'monkey.conf'); + props.onHide(); } return ( diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx index 70e366d20..a60a25c0f 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx @@ -9,10 +9,12 @@ import UnsafeConfigOptionsConfirmationModal from './UnsafeConfigOptionsConfirmationModal.js'; import UploadStatusIcon, {UploadStatuses} from '../ui-components/UploadStatusIcon'; import isUnsafeOptionSelected from '../utils/SafeOptionValidator.js'; +import {decryptText} from '../utils/PasswordBasedEncryptor'; type Props = { show: boolean, + schema: object, onClose: (importSuccessful: boolean) => void } @@ -23,9 +25,9 @@ const ConfigImportModal = (props: Props) => { const [uploadStatus, setUploadStatus] = useState(UploadStatuses.clean); const [configContents, setConfigContents] = useState(null); - const [candidateConfig, setCandidateConfig] = useState(null); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); + const [configEncrypted, setConfigEncrypted] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [unsafeOptionsVerified, setUnsafeOptionsVerified] = useState(false); const [showUnsafeOptionsConfirmation, @@ -36,10 +38,33 @@ const ConfigImportModal = (props: Props) => { useEffect(() => { if (configContents !== null) { - sendConfigToServer(); + tryImport(); } }, [configContents, unsafeOptionsVerified]) + function tryImport() { + if (configEncrypted && !showPassword){ + setShowPassword(true); + } else if (configEncrypted && showPassword) { + try { + let decryptedConfig = JSON.parse(decryptText(configContents, password)); + setConfigEncrypted(false); + setConfigContents(decryptedConfig); + } catch (e) { + setUploadStatus(UploadStatuses.error); + setErrorMessage('Decryption failed: Password is wrong or the file is corrupted'); + } + } else if(!unsafeOptionsVerified) { + if(isUnsafeOptionSelected(props.schema, configContents)){ + setShowUnsafeOptionsConfirmation(true); + } else { + setUnsafeOptionsVerified(true); + } + } else { + sendConfigToServer(); + setUploadStatus(UploadStatuses.success); + } + } function sendConfigToServer() { authComponent.authFetch(configImportEndpoint, @@ -48,34 +73,14 @@ const ConfigImportModal = (props: Props) => { headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ config: configContents, - password: password, - unsafeOptionsVerified: unsafeOptionsVerified }) } ).then(res => res.json()) .then(res => { - if (res['import_status'] === 'invalid_credentials') { - setUploadStatus(UploadStatuses.success); - if (showPassword){ - setErrorMessage(res['message']); - } else { - setShowPassword(true); - setErrorMessage(''); - } - } else if (res['import_status'] === 'invalid_configuration') { + if (res['import_status'] === 'invalid_configuration') { setUploadStatus(UploadStatuses.error); setErrorMessage(res['message']); - } else if (res['import_status'] === 'unsafe_options_verification_required') { - setUploadStatus(UploadStatuses.success); - setErrorMessage(''); - - if (isUnsafeOptionSelected(res['config_schema'], JSON.parse(res['config']))) { - setShowUnsafeOptionsConfirmation(true); - setCandidateConfig(res['config']); - } else { - setUnsafeOptionsVerified(true); - } - } else if (res['import_status'] === 'imported'){ + } else if (res['import_status'] === 'imported') { resetState(); props.onClose(true); } @@ -83,7 +88,8 @@ const ConfigImportModal = (props: Props) => { } function isImportDisabled(): boolean { - return uploadStatus !== UploadStatuses.success || (showPassword && password === '') + // Don't allow import if password input is empty or there's an error + return (showPassword && password === '') || (errorMessage !== ''); } function resetState() { @@ -95,13 +101,22 @@ const ConfigImportModal = (props: Props) => { setShowUnsafeOptionsConfirmation(false); setUnsafeOptionsVerified(false); setFileFieldKey(Date.now()); // Resets the file input + setConfigEncrypted(false); } function uploadFile(event) { setShowPassword(false); let reader = new FileReader(); reader.onload = (event) => { - setConfigContents(event.target.result); + let importContents = null; + try { + importContents = JSON.parse(event.target.result); + } catch (e){ + setErrorMessage('File is not in a valid json format'); + return + } + setConfigEncrypted(importContents['metadata']['encrypted']); + setConfigContents(importContents['contents']); }; reader.readAsText(event.target.files[0]); } @@ -115,7 +130,6 @@ const ConfigImportModal = (props: Props) => { }} onContinueClick={() => { setUnsafeOptionsVerified(true); - setConfigContents(candidateConfig); }} /> ); @@ -150,20 +164,21 @@ const ConfigImportModal = (props: Props) => { key={fileFieldKey}/> - {showPassword && } + {showPassword && {setPassword(password); + setErrorMessage('')}}/>} {errorMessage && - - - {errorMessage} - + + + {errorMessage} + } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index 5f67c9010..b59659ba8 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -23,6 +23,10 @@ const CONFIG_URL = '/api/configuration/island'; export const API_PBA_LINUX = '/api/file-upload/PBAlinux'; export const API_PBA_WINDOWS = '/api/file-upload/PBAwindows'; +const configSubmitAction = 'config-submit'; +const configExportAction = 'config-export'; +const configSaveAction = 'config-saved'; + class ConfigurePageComponent extends AuthComponent { constructor(props) { @@ -87,14 +91,16 @@ class ConfigurePageComponent extends AuthComponent { }; onUnsafeConfirmationCancelClick = () => { - this.setState({showUnsafeOptionsConfirmation: false}); + this.setState({showUnsafeOptionsConfirmation: false, lastAction: 'none'}); } onUnsafeConfirmationContinueClick = () => { this.setState({showUnsafeOptionsConfirmation: false}); - - if (this.state.lastAction === 'submit_attempt') { + if (this.state.lastAction === configSubmitAction) { this.configSubmit(); + } else if (this.state.lastAction === configExportAction) { + this.configSubmit(); + this.setState({showConfigExportModal: true}); } } @@ -113,37 +119,31 @@ class ConfigurePageComponent extends AuthComponent { }; onSubmit = () => { - this.attemptConfigSubmit(); + this.setState({lastAction: configSubmitAction}, this.attemptConfigSubmit) }; canSafelySubmitConfig(config) { return !isUnsafeOptionSelected(this.state.schema, config); } - checkAndShowUnsafeAttackWarning = () => { - if (isUnsafeOptionSelected(this.state.schema, this.state.configuration)) { - this.setState({showUnsafeAttackOptionsWarning: true}); + async attemptConfigSubmit() { + await this.updateConfigSection(); + if (this.canSafelySubmitConfig(this.state.configuration)) { + this.configSubmit(); + if(this.state.lastAction === configExportAction){ + this.setState({showConfigExportModal: true}) + } + } else { + this.setState({showUnsafeOptionsConfirmation: true}); } } - attemptConfigSubmit() { - this.updateConfigSection(); - this.setState({lastAction: 'submit_attempt'}, () => { - if (this.canSafelySubmitConfig(this.state.configuration)) { - this.configSubmit(); - } else { - this.setState({showUnsafeOptionsConfirmation: true}); - } - } - ); - } - configSubmit() { - this.sendConfig() + return this.sendConfig() .then(res => res.json()) .then(res => { this.setState({ - lastAction: 'saved', + lastAction: configSaveAction, schema: res.schema, configuration: res.configuration }); @@ -167,11 +167,12 @@ class ConfigurePageComponent extends AuthComponent { if (Object.keys(this.state.currentFormData).length > 0) { newConfig[this.currentSection] = this.state.currentFormData; } - this.setState({configuration: newConfig, lastAction: 'none'}); + this.setState({configuration: newConfig}); }; renderConfigExportModal = () => { return ( { this.setState({showConfigExportModal: false}); }}/>); @@ -179,6 +180,7 @@ class ConfigurePageComponent extends AuthComponent { renderConfigImportModal = () => { return (); } @@ -307,9 +309,9 @@ class ConfigurePageComponent extends AuthComponent { this.authFetch(apiEndpoint, request_options); } - exportConfig = () => { - this.updateConfigSection(); - this.setState({showConfigExportModal: true}); + exportConfig = async () => { + await this.setState({lastAction: configExportAction}); + await this.attemptConfigSubmit(); }; sendConfig() { @@ -451,7 +453,7 @@ class ConfigurePageComponent extends AuthComponent { Configuration reset successfully. : ''} - {this.state.lastAction === 'saved' ? + {this.state.lastAction === configSaveAction ?
Configuration saved successfully. diff --git a/monkey/monkey_island/cc/ui/src/components/utils/PasswordBasedEncryptor.tsx b/monkey/monkey_island/cc/ui/src/components/utils/PasswordBasedEncryptor.tsx new file mode 100644 index 000000000..500ca7729 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/utils/PasswordBasedEncryptor.tsx @@ -0,0 +1,11 @@ +import AES from 'crypto-js/aes'; +import Utf8 from 'crypto-js/enc-utf8'; + +export function encryptText(content: string, password: string): string { + return AES.encrypt(content, password).toString(); +} + +export function decryptText(ciphertext: string, password: string): string { + let bytes = AES.decrypt(ciphertext, password); + return bytes.toString(Utf8); +}