Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2002 config encryption in UI #2050

Merged
merged 9 commits into from
Jun 30, 2022
11 changes: 11 additions & 0 deletions monkey/monkey_island/cc/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions monkey/monkey_island/cc/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -48,42 +73,23 @@ 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);
}
})
}

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() {
Expand All @@ -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]);
}
Expand All @@ -115,7 +130,6 @@ const ConfigImportModal = (props: Props) => {
}}
onContinueClick={() => {
setUnsafeOptionsVerified(true);
setConfigContents(candidateConfig);
}}
/>
);
Expand Down Expand Up @@ -150,20 +164,21 @@ const ConfigImportModal = (props: Props) => {
key={fileFieldKey}/>
<UploadStatusIcon status={uploadStatus}/>

{showPassword && <PasswordInput onChange={setPassword}/>}
{showPassword && <PasswordInput onChange={(password) => {setPassword(password);
setErrorMessage('')}}/>}

{errorMessage &&
<Alert variant={'danger'} className={'import-error'}>
<FontAwesomeIcon icon={faExclamationCircle} style={{'marginRight': '5px'}}/>
{errorMessage}
</Alert>
<Alert variant={'danger'} className={'import-error'}>
<FontAwesomeIcon icon={faExclamationCircle} style={{'marginRight': '5px'}}/>
{errorMessage}
</Alert>
}
</Form>
</div>
</Modal.Body>
<Modal.Footer>
<Button variant={'info'}
onClick={sendConfigToServer}
onClick={tryImport}
disabled={isImportDisabled()}>
Import
</Button>
Expand Down
Loading