diff --git a/packages/api/package.json b/packages/api/package.json index 30267b0b3..315ffc817 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -40,6 +40,7 @@ "ncp": "^2.0.0", "nedb-promises": "^4.0.1", "node-cron": "^2.0.3", + "simple-encryptor": "^4.0.0", "tar": "^6.0.5", "uuid": "^3.4.0" }, diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index f7050eac6..2f93458c6 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -5,6 +5,7 @@ const nedb = require('nedb-promises'); const { datadir } = require('../utility/directories'); const socket = require('../utility/socket'); +const { encryptConnection } = require('../utility/crypting'); function getPortalCollections() { if (process.env.CONNECTIONS) { @@ -59,10 +60,11 @@ module.exports = { async save(connection) { if (portalConnections) return; let res; + const encrypted = encryptConnection(connection); if (connection._id) { - res = await this.datastore.update(_.pick(connection, '_id'), connection); + res = await this.datastore.update(_.pick(connection, '_id'), encrypted); } else { - res = await this.datastore.insert(connection); + res = await this.datastore.insert(encrypted); } socket.emitChanged('connection-list-changed'); return res; diff --git a/packages/api/src/controllers/plugins.js b/packages/api/src/controllers/plugins.js index 75981d768..cfbea2831 100644 --- a/packages/api/src/controllers/plugins.js +++ b/packages/api/src/controllers/plugins.js @@ -28,7 +28,7 @@ const hasPermission = require('../utility/hasPermission'); // } const preinstallPluginMinimalVersions = { - 'dbgate-plugin-mssql': '1.0.8', + 'dbgate-plugin-mssql': '1.0.9', 'dbgate-plugin-mysql': '1.0.2', 'dbgate-plugin-postgres': '1.0.2', 'dbgate-plugin-csv': '1.0.8', diff --git a/packages/api/src/proc/connectProcess.js b/packages/api/src/proc/connectProcess.js index a4c916b2a..8fb780371 100644 --- a/packages/api/src/proc/connectProcess.js +++ b/packages/api/src/proc/connectProcess.js @@ -1,12 +1,13 @@ const childProcessChecker = require('../utility/childProcessChecker'); const requireEngineDriver = require('../utility/requireEngineDriver'); +const { decryptConnection } = require('../utility/crypting'); function start() { childProcessChecker(); process.on('message', async (connection) => { try { const driver = requireEngineDriver(connection); - const conn = await driver.connect(connection); + const conn = await driver.connect(decryptConnection(connection)); const res = await driver.getVersion(conn); process.send({ msgtype: 'connected', ...res }); } catch (e) { diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index 58b965265..81c5a8c48 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -1,6 +1,7 @@ const stableStringify = require('json-stable-stringify'); const childProcessChecker = require('../utility/childProcessChecker'); const requireEngineDriver = require('../utility/requireEngineDriver'); +const { decryptConnection } = require('../utility/crypting'); let systemConnection; let storedConnection; @@ -59,7 +60,7 @@ async function handleConnect({ connection, structure }) { if (!structure) setStatusName('pending'); const driver = requireEngineDriver(storedConnection); - systemConnection = await checkedAsyncCall(driver.connect(storedConnection)); + systemConnection = await checkedAsyncCall(driver.connect(decryptConnection(storedConnection))); if (structure) { analysedStructure = structure; handleIncrementalRefresh(); diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js index 0cba8c760..a3697c015 100644 --- a/packages/api/src/proc/serverConnectionProcess.js +++ b/packages/api/src/proc/serverConnectionProcess.js @@ -1,6 +1,7 @@ const stableStringify = require('json-stable-stringify'); const childProcessChecker = require('../utility/childProcessChecker'); const requireEngineDriver = require('../utility/requireEngineDriver'); +const { decryptConnection } = require('../utility/crypting'); let systemConnection; let storedConnection; @@ -47,7 +48,7 @@ async function handleConnect(connection) { const driver = requireEngineDriver(storedConnection); try { - systemConnection = await driver.connect(storedConnection); + systemConnection = await driver.connect(decryptConnection(storedConnection)); handleRefresh(); setInterval(handleRefresh, 30 * 1000); } catch (err) { @@ -66,7 +67,7 @@ function handlePing() { async function handleCreateDatabase({ name }) { const driver = requireEngineDriver(storedConnection); - systemConnection = await driver.connect(storedConnection); + systemConnection = await driver.connect(decryptConnection(storedConnection)); console.log(`RUNNING SCRIPT: CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`); await driver.query(systemConnection, `CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`); await handleRefresh(); diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 9777baf05..04ff0b1dd 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -7,6 +7,7 @@ const goSplit = require('../utility/goSplit'); const { jsldir } = require('../utility/directories'); const requireEngineDriver = require('../utility/requireEngineDriver'); +const { decryptConnection } = require('../utility/crypting'); let systemConnection; let storedConnection; @@ -73,7 +74,7 @@ class StreamHandler { // use this for cancelling - not implemented // this.stream = null; - + this.plannedStats = false; this.resultIndexHolder = resultIndexHolder; this.resolve = resolve; @@ -130,7 +131,7 @@ async function handleConnect(connection) { storedConnection = connection; const driver = requireEngineDriver(storedConnection); - systemConnection = await driver.connect(storedConnection); + systemConnection = await driver.connect(decryptConnection(storedConnection)); for (const [resolve] of afterConnectCallbacks) { resolve(); } diff --git a/packages/api/src/shell/executeQuery.js b/packages/api/src/shell/executeQuery.js index 3ee97c19f..72a85d545 100644 --- a/packages/api/src/shell/executeQuery.js +++ b/packages/api/src/shell/executeQuery.js @@ -1,11 +1,12 @@ const goSplit = require('../utility/goSplit'); const requireEngineDriver = require('../utility/requireEngineDriver'); +const { decryptConnection } = require('../utility/crypting'); async function executeQuery({ connection, sql }) { console.log(`Execute query ${sql}`); const driver = requireEngineDriver(connection); - const pool = await driver.connect(connection); + const pool = await driver.connect(decryptConnection(connection)); console.log(`Connected.`); for (const sqlItem of goSplit(sql)) { diff --git a/packages/api/src/shell/queryReader.js b/packages/api/src/shell/queryReader.js index 13b2e4159..ce96f40f8 100644 --- a/packages/api/src/shell/queryReader.js +++ b/packages/api/src/shell/queryReader.js @@ -1,10 +1,11 @@ -const requireEngineDriver = require("../utility/requireEngineDriver"); +const requireEngineDriver = require('../utility/requireEngineDriver'); +const { decryptConnection } = require('../utility/crypting'); async function queryReader({ connection, sql }) { console.log(`Reading query ${sql}`); const driver = requireEngineDriver(connection); - const pool = await driver.connect(connection); + const pool = await driver.connect(decryptConnection(connection)); console.log(`Connected.`); return await driver.readQuery(pool, sql); } diff --git a/packages/api/src/shell/tableReader.js b/packages/api/src/shell/tableReader.js index 7eb407a39..7fdca1414 100644 --- a/packages/api/src/shell/tableReader.js +++ b/packages/api/src/shell/tableReader.js @@ -1,9 +1,10 @@ const { quoteFullName, fullNameToString } = require('dbgate-tools'); const requireEngineDriver = require('../utility/requireEngineDriver'); +const { decryptConnection } = require('../utility/crypting'); async function tableReader({ connection, pureName, schemaName }) { const driver = requireEngineDriver(connection); - const pool = await driver.connect(connection); + const pool = await driver.connect(decryptConnection(connection)); console.log(`Connected.`); const fullName = { pureName, schemaName }; diff --git a/packages/api/src/shell/tableWriter.js b/packages/api/src/shell/tableWriter.js index 699655914..63c393e8f 100644 --- a/packages/api/src/shell/tableWriter.js +++ b/packages/api/src/shell/tableWriter.js @@ -1,11 +1,12 @@ const { fullNameToString } = require('dbgate-tools'); const requireEngineDriver = require('../utility/requireEngineDriver'); +const { decryptConnection } = require('../utility/crypting'); async function tableWriter({ connection, schemaName, pureName, ...options }) { console.log(`Writing table ${fullNameToString({ schemaName, pureName })}`); const driver = requireEngineDriver(connection); - const pool = await driver.connect(connection); + const pool = await driver.connect(decryptConnection(connection)); console.log(`Connected.`); return await driver.writeTable(pool, { schemaName, pureName }, options); } diff --git a/packages/api/src/utility/crypting.js b/packages/api/src/utility/crypting.js new file mode 100644 index 000000000..02823d1e6 --- /dev/null +++ b/packages/api/src/utility/crypting.js @@ -0,0 +1,69 @@ +const crypto = require('crypto'); +const simpleEncryptor = require('simple-encryptor'); +const fs = require('fs'); +const path = require('path'); + +const { datadir } = require('./directories'); + +const defaultEncryptionKey = 'mQAUaXhavRGJDxDTXSCg7Ej0xMmGCrx6OKA07DIMBiDcYYkvkaXjTAzPUEHEHEf9'; + +let _encryptionKey = null; + +function loadEncryptionKey() { + if (_encryptionKey) { + return _encryptionKey; + } + const encryptor = simpleEncryptor.createEncryptor(defaultEncryptionKey); + + const keyFile = path.join(datadir(), '.key'); + + if (!fs.existsSync(keyFile)) { + const generatedKey = crypto.randomBytes(32); + const newKey = generatedKey.toString('hex'); + const result = { + encryptionKey: newKey, + }; + fs.writeFileSync(keyFile, encryptor.encrypt(result), 'utf-8'); + } + + const encryptedData = fs.readFileSync(keyFile, 'utf-8'); + const data = encryptor.decrypt(encryptedData); + _encryptionKey = data['encryptionKey']; + return _encryptionKey; +} + +let _encryptor = null; + +function getEncryptor() { + if (_encryptor) { + return _encryptor; + } + _encryptor = simpleEncryptor.createEncryptor(loadEncryptionKey()); + return _encryptor; +} + +function encryptConnection(connection) { + if (connection && connection.password && !connection.password.startsWith('crypt:')) { + return { + ...connection, + password: 'crypt:' + getEncryptor().encrypt(connection.password), + }; + } + return connection; +} + +function decryptConnection(connection) { + if (connection && connection.password && connection.password.startsWith('crypt:')) { + return { + ...connection, + password: getEncryptor().decrypt(connection.password.substring('crypt:'.length)), + }; + } + return connection; +} + +module.exports = { + loadEncryptionKey, + encryptConnection, + decryptConnection, +}; diff --git a/packages/web/src/utility/forms.js b/packages/web/src/utility/forms.js index c24558bda..1fde75cc2 100644 --- a/packages/web/src/utility/forms.js +++ b/packages/web/src/utility/forms.js @@ -33,7 +33,7 @@ export function FormCondition({ condition, children }) { export function FormTextFieldRaw({ name, focused = false, ...other }) { const { values, setFieldValue } = useForm(); - const handleChange = event => { + const handleChange = (event) => { setFieldValue(name, event.target.value); }; const textFieldRef = React.useRef(null); @@ -44,6 +44,35 @@ export function FormTextFieldRaw({ name, focused = false, ...other }) { return ; } +export function FormPasswordFieldRaw({ name, focused = false, ...other }) { + const { values, setFieldValue } = useForm(); + const [showPassword, setShowPassword] = React.useState(false); + const handleChange = (event) => { + setFieldValue(name, event.target.value); + }; + const textFieldRef = React.useRef(null); + React.useEffect(() => { + if (textFieldRef.current && focused) textFieldRef.current.focus(); + }, [textFieldRef.current, focused]); + const value = values[name]; + const isCrypted = value && value.startsWith('crypt:'); + + return ( + <> + + + {!isCrypted && setShowPassword((x) => !x)} />} + + ); +} + export function FormTextField({ name, label, focused = false, ...other }) { const FieldTemplate = useFormFieldTemplate(); return ( @@ -55,18 +84,16 @@ export function FormTextField({ name, label, focused = false, ...other }) { export function FormPasswordField({ name, label, focused = false, ...other }) { const FieldTemplate = useFormFieldTemplate(); - const [showPassword, setShowPassword] = React.useState(false); return ( - - setShowPassword(x => !x)} /> + ); } export function FormCheckboxFieldRaw({ name = undefined, defaultValue = undefined, ...other }) { const { values, setFieldValue } = useForm(); - const handleChange = event => { + const handleChange = (event) => { setFieldValue(name, event.target.checked); }; let isChecked = values[name]; @@ -86,7 +113,7 @@ export function FormCheckboxField({ label, ...other }) { export function FormSelectFieldRaw({ children, name, ...other }) { const { values, setFieldValue } = useForm(); - const handleChange = event => { + const handleChange = (event) => { setFieldValue(name, event.target.value); }; return ( @@ -142,7 +169,7 @@ export function FormReactSelect({ options, name, isMulti = false, Component = Se return ( ({ + theme={(t) => ({ ...t, colors: { ...t.colors, @@ -167,10 +194,12 @@ export function FormReactSelect({ options, name, isMulti = false, Component = Se options={options} value={ isMulti - ? options.filter(x => values[name] && values[name].includes(x.value)) - : options.find(x => x.value == values[name]) + ? options.filter((x) => values[name] && values[name].includes(x.value)) + : options.find((x) => x.value == values[name]) + } + onChange={(item) => + setFieldValue(name, isMulti ? getAsArray(item).map((x) => x.value) : item ? item.value : null) } - onChange={item => setFieldValue(name, isMulti ? getAsArray(item).map(x => x.value) : item ? item.value : null)} menuPortalTarget={document.body} isMulti={isMulti} closeMenuOnSelect={!isMulti} @@ -183,7 +212,7 @@ export function FormConnectionSelect({ name }) { const connections = useConnectionList(); const connectionOptions = React.useMemo( () => - (connections || []).map(conn => ({ + (connections || []).map((conn) => ({ value: conn._id, label: conn.displayName || conn.server, })), @@ -199,7 +228,7 @@ export function FormDatabaseSelect({ conidName, name }) { const databases = useDatabaseList({ conid: values[conidName] }); const databaseOptions = React.useMemo( () => - (databases || []).map(db => ({ + (databases || []).map((db) => ({ value: db.name, label: db.name, })), @@ -215,7 +244,7 @@ export function FormSchemaSelect({ conidName, databaseName, name }) { const dbinfo = useDatabaseInfo({ conid: values[conidName], database: values[databaseName] }); const schemaOptions = React.useMemo( () => - ((dbinfo && dbinfo.schemas) || []).map(schema => ({ + ((dbinfo && dbinfo.schemas) || []).map((schema) => ({ value: schema.schemaName, label: schema.schemaName, })), @@ -232,8 +261,8 @@ export function FormTablesSelect({ conidName, databaseName, schemaName, name }) const tablesOptions = React.useMemo( () => [...((dbinfo && dbinfo.tables) || []), ...((dbinfo && dbinfo.views) || [])] - .filter(x => !values[schemaName] || x.schemaName == values[schemaName]) - .map(x => ({ + .filter((x) => !values[schemaName] || x.schemaName == values[schemaName]) + .map((x) => ({ value: x.pureName, label: x.pureName, })), @@ -249,7 +278,7 @@ export function FormArchiveFilesSelect({ folderName, name }) { const files = useArchiveFiles({ folder: folderName }); const filesOptions = React.useMemo( () => - (files || []).map(x => ({ + (files || []).map((x) => ({ value: x.name, label: x.name, })), @@ -265,13 +294,13 @@ export function FormArchiveFolderSelect({ name, additionalFolders = [], ...other const folders = useArchiveFolders(); const folderOptions = React.useMemo( () => [ - ...(folders || []).map(folder => ({ + ...(folders || []).map((folder) => ({ value: folder.name, label: folder.name, })), ...additionalFolders - .filter(x => !(folders || []).find(y => y.name == x)) - .map(folder => ({ + .filter((x) => !(folders || []).find((y) => y.name == x)) + .map((folder) => ({ value: folder, label: folder, })), @@ -279,7 +308,7 @@ export function FormArchiveFolderSelect({ name, additionalFolders = [], ...other [folders] ); - const handleCreateOption = folder => { + const handleCreateOption = (folder) => { axios.post('archive/create-folder', { folder }); setFieldValue(name, folder); }; diff --git a/yarn.lock b/yarn.lock index 3ed60528a..4887a2b84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10554,6 +10554,11 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +scmp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/scmp/-/scmp-2.0.0.tgz#247110ef22ccf897b13a3f0abddb52782393cd6a" + integrity sha1-JHEQ7yLM+JexOj8KvdtSeCOTzWo= + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -10742,6 +10747,13 @@ simple-concat@^1.0.0: resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== +simple-encryptor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/simple-encryptor/-/simple-encryptor-4.0.0.tgz#aac74b12c365879115ed683c05e17c235a457cc8" + integrity sha512-J3oCeJDjRf/X6ZQkpowMKutEDxkjDESRIbdov+PiPwmatepkGZQaF2WHTr7V1cUQnd843E4dQq4zlwruGKGM7w== + dependencies: + scmp "2.0.0" + simple-get@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3"