From cef752dd6fd0f38f9e5c9e584a73011936a51da3 Mon Sep 17 00:00:00 2001 From: miketwc1984 Date: Sun, 2 May 2021 15:12:33 -0400 Subject: [PATCH] update encryption to use AES --- Docker/Dockerfile | 9 +++++---- DockerfileDev | 34 ++++++++++++++++++++++++++++++++++ lib/api/secret.js | 10 ++++++---- lib/engine.js | 29 +++++++++++++++++++++++++++-- lib/job.js | 21 ++++++++++++++------- 5 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 DockerfileDev diff --git a/Docker/Dockerfile b/Docker/Dockerfile index 60040a4..8fa2d28 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -1,4 +1,4 @@ -# build: docker build -t cronicle:edge -f Dockerfile --build-arg branch=main --build-arg echo=1 --build-arg bldonly=1 . +# build: docker build -t cronicle:edge2 -f Dockerfile --build-arg branch=main --build-arg echo=1 --build-arg bldonly=1 . # test run: docker run -it -v $HOME/data:/opt/cronicle/data -p 3012:3012 cronicle:edge manager FROM node:14-alpine3.12 @@ -52,9 +52,10 @@ ARG branch=main RUN git clone https://github.com/cronicle-edge/cronicle-edge.git /opt/cronicle RUN git checkout ${branch} RUN npm audit fix --force; npm install -ARG bldonly -RUN echo $bldonly -RUN git pull && node bin/build dist +RUN node bin/build dist +# ARG bldonly +# RUN echo $bldonly +# RUN git pull && node bin/build dist # protect sensitive folders RUN mkdir -p /opt/cronicle/data /opt/cronicle/conf && chmod 0700 /opt/cronicle/data /opt/cronicle/conf diff --git a/DockerfileDev b/DockerfileDev new file mode 100644 index 0000000..23bf082 --- /dev/null +++ b/DockerfileDev @@ -0,0 +1,34 @@ +# build: docker build -t cronicle:dev -f DockerfileDev --build-arg echo=1 . +# test run: docker run --rm -it -p 3019:3012 cronicle:dev bash + +FROM node:14-alpine3.12 +RUN apk add --no-cache git tini util-linux bash openssl procps coreutils curl acl jq +# required: all: tini; alpine: util-linux procps coreutils + +# optional lolcat for tty/color debugging +RUN apk add lolcat --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing + + +ENV CRONICLE_foreground=1 +ENV CRONICLE_echo=1 +ENV TZ=America/New_York +ENV EDITOR=nvim + +ENV PATH "/opt/cronicle/bin:${PATH}" + +# non root user for shell plugin +ARG CRONICLE_UID=1007 +ARG CRONICLE_GID=1099 +RUN addgroup cronicle --gid $CRONICLE_GID && adduser -D -h /opt/cronicle -u $CRONICLE_UID -G cronicle cronicle + +COPY . /opt/cronicle +WORKDIR /opt/cronicle +ARG echo +RUN echo $echo +RUN npm audit fix --force; npm install +RUN node bin/build dist + +# protect sensitive folders +RUN mkdir -p /opt/cronicle/data /opt/cronicle/conf && chmod 0700 /opt/cronicle/data /opt/cronicle/conf + +ENTRYPOINT ["/sbin/tini", "--"] diff --git a/lib/api/secret.js b/lib/api/secret.js index 04ed04b..62d3840 100644 --- a/lib/api/secret.js +++ b/lib/api/secret.js @@ -30,8 +30,9 @@ module.exports = Class.create({ if (secret.encrypted && secret.data) { try { - secret.data = await self.decrypt(secret.data) - if (secret.data) secret.data = secret.data.toString(); + //secret.data = await self.decrypt(secret.data) + //if (secret.data) secret.data = secret.data.toString(); + secret.data = self.decryptObject(secret.data) } catch (err) { secret.data = "Failed to decrypt secret:\n" + err; @@ -124,8 +125,9 @@ module.exports = Class.create({ if (params.encrypted) { try { - params.data = await self.encrypt(params.data) - if (params.data) params.data = params.data.toString(); + // params.data = await self.encrypt(params.data) + params.data = self.encryptObject(params.data) + //if (params.data) params.data = params.data.toString(); } catch (err) { return self.doError('secret', "Failed to encrypt secret (missing key file?)", callback); diff --git a/lib/engine.js b/lib/engine.js index 716abd3..c39f96b 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -21,6 +21,11 @@ const cp = require('child_process'); const dotenv = require('dotenv'); const openssl = util.promisify(require('openssl-wrapper').exec); +const crypto = require("crypto"); +const algorithm = "aes-256-ctr"; +const inputEncoding = "utf8"; +const outputEncoding = "base64"; + module.exports = Class.create({ __name: 'Cronicle', @@ -220,6 +225,24 @@ module.exports = Class.create({ return openssl('cms.encrypt', Buffer.from(cipher), encOpts); }, + encryptObject: function (obj) { + const IV = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm, this.server.config.get('secret_key'), IV); + let crypted = cipher.update(JSON.stringify(obj), inputEncoding, outputEncoding ); + crypted += cipher.final(outputEncoding); + return `${IV.toString(outputEncoding)}:${crypted}`; + }, + + decryptObject: function ( encObj ) { + const encParts = encObj.split(":"); + const IV = Buffer.from(encParts[0], outputEncoding); + const encryptedText = Buffer.from(encParts[1], outputEncoding); + const decipher = crypto.createDecipheriv(algorithm, this.server.config.get('secret_key'), IV); + let decrypted = decipher.update(encryptedText, outputEncoding, inputEncoding); + decrypted += decipher.final(inputEncoding); + return JSON.parse(decrypted); + }, + updateSecrets: function() { const self = this; this.storage.listGet('global/secrets', 0, 0, async function (err, items, list) { @@ -231,9 +254,10 @@ module.exports = Class.create({ for (i = 0; i < items.length; i++) { let secret = JSON.parse(JSON.stringify(items[i])); try { - if(secret.encrypted) secret.data = await self.decrypt(secret.data) + //if(secret.encrypted) secret.data = await self.decrypt(secret.data) + if(secret.encrypted) secret.data = self.decryptObject(secret.data) if(secret.form == 'props') secret.data = dotenv.parse(secret.data); - if(secret.form == 'json') secret.data = JSON.parse(secret.data); + //if(secret.form == 'json') secret.data = JSON.parse(secret.data); } catch (err) { @@ -248,6 +272,7 @@ module.exports = Class.create({ } }); }, + checkmanagerEligibility: function (callback) { // determine manager server eligibility diff --git a/lib/job.js b/lib/job.js index d58c08d..883a634 100644 --- a/lib/job.js +++ b/lib/job.js @@ -214,14 +214,14 @@ module.exports = Class.create({ // pull in properties from plugin let globalenv = (self.secrets['globalenv'] || {}).data - if(typeof globalenv != 'object') globalenv = {} + if(globalenv) job.secret = self.encryptObject(globalenv) ; job.command = plugin.command; if (plugin.cwd) job.cwd = plugin.cwd; if (plugin.uid) job.uid = plugin.uid; if (job.debug_sudo) job.uid = process.getuid(); if (plugin.gid) job.gid = plugin.gid; - job.env = Tools.mergeHashes(plugin.env || {}, globalenv) + job.env = Tools.mergeHashes(plugin.env || {}, self.server.config.get('job_env') || {}); if (plugin.secret) job.env['PLUGIN_SECRET'] = plugin.secret; if (plugin.id == 'workflow') { // todo - change to flag let temp_key = Tools.digestHex(job.id + (new Date).toDateString()) @@ -489,19 +489,26 @@ module.exports = Class.create({ }); } + if(job.secret) { + try { + job.env = Tools.mergeHashes(job.env, self.decryptObject(job.secret)); + } + catch (e) { + self.logDebug(6, 'failed to decrypt job secret:', e); + } + } + // setup environment for child var child_opts = { cwd: job.cwd || process.cwd(), uid: job.uid || process.getuid(), gid: process.getgid(), - env: Tools.mergeHashes( - this.server.config.get('job_env') || {}, - Tools.mergeHashes(process.env, job.env || {}) - ) + env: Tools.mergeHashes(process.env, job.env || {}) }; - // drop job.env to prevent from logging (it may include sensitive data) + // drop job.env and secret to prevent logging (it may include sensitive data) delete job.env; + delete job.secret; child_opts.env['CRONICLE'] = this.server.__version; child_opts.env['JOB_ID'] = job.id;