diff --git a/README.md b/README.md index 9256a91..730f99a 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Dazu muss golgender Befehl ausgeführt werden: ts-node .\index.ts --privkey private.key --decrypt --safe .\out\credentials.json ## Config-Datei +``` { "mail":{ "host": "", @@ -91,10 +92,9 @@ ts-node .\index.ts --privkey private.key --decrypt --safe .\out\credentials.json } }, "mailFrom": "", - "outFileCodes": "", "outFileMatch": "" } - +``` ## Syntax @@ -106,6 +106,8 @@ z.B. `ts-node .\index.ts --privkey private.key --pubkey public.key --genkey` ==> Codes Erzeugen und versenden `ts-node .\index.ts --config --pubkey --send --safe .\out\credentials.json --mails -html ` +Achtung: Es wird im Safe geprüft, ob Mailadressen bereits "bedient" wurden. Sollte dies der Fall sein, werden keine Mails an diese Adresse gesendet. Dies lässt sich mit dem Schalter `--force` umgehen. + z.B. `ts-node .\index.ts --config config.json --pubkey public.key --send --safe .\out\credentials.json --mails mail.txt -html template.html` ==> Safe entschlüsseln @@ -113,8 +115,13 @@ z.B. `ts-node .\index.ts --config config.json --pubkey public.key --send --safe z.B. `ts-node .\index.ts --privkey private.key --decrypt --safe .\out\credentials.json` -## Kompillierte Binaries -Die Kompilierten Binaries sind für Linux, MacOS und Windoof verfügbar: [Binaries](https://gitlab.dennisgunia.de/dennisgunia/one-time-code-js/-/tree/master/bin)` +### Erweiterte Schalter + + - `--dryrun` : Mails werden nicht versendet und der Safe wird nicht verändert. + - `--force` : Alle Codes werden neu generiert und alle mails werden gesendet. Ignoriere bereits gesendete mails. + +## Gepackte Binaries +Die gepackten Binaries sind für Linux, MacOS und Windoof verfügbar: [Binaries](https://gitlab.dennisgunia.de/dennisgunia/one-time-code-js/-/tree/master/bin) Die befehle ändern sich wie folgt: @@ -132,3 +139,55 @@ z.B. `./opentoken --config config.json --pubkey public.key --send --safe .\out\c `./opentoken --privkey --decrypt --safe .\out\credentials.json` z.B. `./opentoken --privkey private.key --decrypt --safe .\out\credentials.json` + +## Ausführen des Quellcodes +Der Sourcecode kann auch über ts-node ausgeführt werden. +Dazu ist Node.js Version 12 zu verwenden + +`nvm use 12` + +Zum Ausführen sind folgende npm Pakete notwendig: + - typescript + - tslint + - ts-node + +Installerien sie diese mit: +`npm install -g typescript tslint ts-node` + +Clonen sie dieses Repository auf ihren lokalen rechner und wechseln sie anschließend in dessen verzeichniss: + +`git clone https://gitlab.dennisgunia.de/dennisgunia/one-time-code-js.git` + +`cd one-time-code-js` + +Installieren sie alle lokalen npm Pakete + +`npm install` + +Kopieren Sie die Config-Template und passen Sie die SMTP-Zugangsdaten an: + +`cp config.template.json config.json` + +`vim config.json` + +Das Skript kann nun über `npm run-script exec` oder `ts-node index.js` ausgeführt werden. + +## Packen des Quellcodes +Zum Packen des Quellcodes ist das npm-Paket `pkg` zu installieren: + +`npm install -g pkg` + +Anschließend wird der Code in JS transpiliert und durch pkg gepackt: + +`npm run-script build` + +Die Binaries werden in `./bin` gespeichert. Diese sind auch auf Systemen ohne node.js ausführbar. + +## Was landet im Safe? +Im safe landen die verschlüsselten Zuordnungen zwischen Codes und Mailadressen. + +Zudem werden die verwendeten Codes und die bereits gesendeten Mailadressen seperat voneinander und zufällig gemischt in Klartext gespeichert. + +Dies ermöglicht es, nachträglich benutzer hinzuzufügen, ohne allen anderen neue Mails oder gar neue Codes zukommen lassen zu müssen. + +Es ist jedoch nocht empfohlen, nachträglich mails hinzuzufügen, da dies, abhängig von der Menge der gleichzeitig hinzugefügten Adressen eine grobe oder ggf. auch sehr genaue zuordnung zwischen Code und Mail der Nachzügler möglich ist. diff --git a/bin/opentoken-linux b/bin/opentoken-linux index c465d7b..4b5152d 100755 Binary files a/bin/opentoken-linux and b/bin/opentoken-linux differ diff --git a/bin/opentoken-macos b/bin/opentoken-macos index 38cd6e2..67c48ae 100755 Binary files a/bin/opentoken-macos and b/bin/opentoken-macos differ diff --git a/bin/opentoken-win.exe b/bin/opentoken-win.exe index 6b481d8..0e42bdf 100644 Binary files a/bin/opentoken-win.exe and b/bin/opentoken-win.exe differ diff --git a/dist/index.js b/dist/index.js index 36d0daf..599ecbf 100644 --- a/dist/index.js +++ b/dist/index.js @@ -22,7 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); var fs = __importStar(require("fs")); var generate_1 = require("./src/generate"); var vault_1 = require("./src/vault"); -var configPath = "", action = -1, pubKey = "", privKey = "", safeFile = "", mails = "", html = ""; +var configPath = "", action = -1, pubKey = "", privKey = "", safeFile = "", mails = "", html = "", dryrun = false, force = false; for (var i = 1; i < process.argv.length; i++) { if (process.argv[i] === "--config") { if (i + 1 < process.argv.length && !process.argv[i + 1].startsWith("--")) { @@ -81,6 +81,12 @@ for (var i = 1; i < process.argv.length; i++) { if (process.argv[i] === "--genkey") { action = 3; } + if (process.argv[i] === "--dryrun") { + dryrun = true; + } + if (process.argv[i] === "--force") { + force = true; + } } if (action == -1) { throw new Error("No Action specified"); @@ -107,18 +113,39 @@ if (action == 1) { var dataSafe_1 = new vault_1.SecureVault(pubKey, privKey); var confRaw = fs.readFileSync(configPath, 'utf8'); var config = {}; + var addition_1 = false; + config = JSON.parse(confRaw); + if (fs.existsSync(safeFile)) { + dataSafe_1.loadData(safeFile); + config.usedTokens = dataSafe_1.getStorage(dataSafe_1.findStorage("usedTokens")[0].u); + config.usedMails = dataSafe_1.getStorage(dataSafe_1.findStorage("usedMails")[0].u); + addition_1 = true; + } + else { + config.usedTokens = []; + config.usedMails = []; + } try { - config = JSON.parse(confRaw); config.inFileMail = mails; config.htmlPath = html; + config.dryrun = dryrun; + config.force = force; } catch (error) { console.error("Cannote read config file!"); process.exit(100); } generate_1.generateToken(config, dataSafe_1).then(function (el) { - console.log(el); - dataSafe_1.saveData(safeFile); + if (addition_1) { + dataSafe_1.setStorage(dataSafe_1.findStorage("usedTokens")[0].u, el.codes); + dataSafe_1.setStorage(dataSafe_1.findStorage("usedMails")[0].u, el.mails); + dataSafe_1.saveData(safeFile); + } + else { + dataSafe_1.pushStorage('usedTokens', el.codes); + dataSafe_1.pushStorage('usedMails', el.mails); + dataSafe_1.saveData(safeFile); + } }).catch(function (err) { return console.error("error", err); }); } else if (action == 2) { diff --git a/dist/src/generate.js b/dist/src/generate.js index 7da9f6b..c3da574 100644 --- a/dist/src/generate.js +++ b/dist/src/generate.js @@ -54,55 +54,66 @@ var __generator = (this && this.__generator) || function (thisArg, body) { if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; +var __spreadArrays = (this && this.__spreadArrays) || function () { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateToken = void 0; var fs = __importStar(require("fs")); +var path = __importStar(require("path")); var nodemailer = __importStar(require("nodemailer")); +var cliProgress = __importStar(require("cli-progress")); var shuffle_1 = require("./util/shuffle"); var token_1 = require("./util/token"); var Handlebars = __importStar(require("handlebars")); +var mailParser_1 = require("./mailParser"); function generateToken(config, dataSafe) { - var pr = new Promise(function (resolve, error) { - var mailArray = []; - var readline = require('readline'), instream = fs.createReadStream(config.inFileMail), outstream = new (require('stream'))(), rl = readline.createInterface(instream, outstream); - rl.on('line', function (line) { - console.log(line); - mailArray.push({ - mail: line.substr(0, line.indexOf(";")), - name: line.substr(line.indexOf(";") + 1) - }); - }); - rl.on('close', function (line) { - generateCodes(resolve, error, mailArray, config, dataSafe); + return new Promise(function (resolve, error) { + mailParser_1.parseMails(config).then(function (res) { + generateCodes(resolve, error, res, config, dataSafe); }); }); - return pr; } exports.generateToken = generateToken; function generateCodes(resolve, error, mailArray, config, dataSafe) { return __awaiter(this, void 0, void 0, function () { - var codeArray, checkString, listString, i, code; + var pbar, position, codeArray, checkString, listString, i, code; return __generator(this, function (_a) { + console.log("\nGenerating codes"); + pbar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); + pbar.start(mailArray.length, 0, { + speed: "N/A" + }); + position = 0; codeArray = []; checkString = ''; listString = ''; for (i = 0; i < mailArray.length; i++) { - code = token_1.mkstring(4); - while (codeArray.includes(code)) { + code = ''; + do { code = token_1.mkstring(4); - } + } while ((config.force ? codeArray : __spreadArrays(codeArray, config.usedTokens)).includes(code)); codeArray.push(code); checkString = checkString + "|" + code; listString = listString + "\n" + code; + position++; + pbar.update(position); } checkString = checkString.substr(1); listString = listString.substr(1); + pbar.stop(); try { + if (!fs.existsSync(path.dirname(config.outFileMatch))) { + fs.mkdirSync(path.dirname(config.outFileMatch)); + } fs.writeFileSync(config.outFileMatch, checkString); - fs.writeFileSync(config.outFileCodes, listString); } - catch (error) { - error(error); + catch (err) { + error(err); } sendMails(resolve, error, mailArray, codeArray, config, dataSafe); return [2]; @@ -111,7 +122,7 @@ function generateCodes(resolve, error, mailArray, config, dataSafe) { } function sendMails(resolve, error, mailArray, codeArray, config, dataSafe) { return __awaiter(this, void 0, void 0, function () { - var mailserver, template, htmlSrc, i; + var mailserver, template, htmlSrc, pbar, position, i; return __generator(this, function (_a) { switch (_a.label) { case 0: @@ -124,26 +135,47 @@ function sendMails(resolve, error, mailArray, codeArray, config, dataSafe) { console.error("Cannote read template file!"); error(error); } + console.log("\nSending mails"); + pbar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); + pbar.start(mailArray.length, 0, { + speed: "N/A" + }); + position = 0; shuffle_1.shuffleArray(mailArray); shuffle_1.shuffleArray(codeArray); + if (config.force) { + dataSafe.clearVault(); + } i = 0; _a.label = 1; case 1: if (!(i < mailArray.length)) return [3, 4]; - dataSafe.pushData({ - name: mailArray[i].name, - mail: mailArray[i].mail, - code: codeArray[i] - }); + if (!config.dryrun) { + dataSafe.pushData({ + name: mailArray[i].name, + mail: mailArray[i].mail, + code: codeArray[i] + }); + } return [4, send(mailArray[i].name, mailArray[i].mail, codeArray[i], template, mailserver, config)]; case 2: _a.sent(); + position++; + pbar.update(position); _a.label = 3; case 3: i++; return [3, 1]; case 4: - resolve(codeArray); + pbar.stop(); + shuffle_1.shuffleArray(mailArray); + shuffle_1.shuffleArray(codeArray); + shuffle_1.shuffleArray(mailArray); + shuffle_1.shuffleArray(codeArray); + resolve({ + codes: config.force ? codeArray : (config.dryrun ? config.usedTokens : __spreadArrays(codeArray, config.usedTokens)), + mails: config.force ? mailArray : (config.dryrun ? config.usedMails : __spreadArrays(mailArray, config.usedMails)) + }); return [2]; } }); @@ -155,6 +187,13 @@ function send(name, mail, code, template, mailserver, config) { return __generator(this, function (_a) { switch (_a.label) { case 0: + if (!config.dryrun) return [3, 2]; + return [4, delay(100)]; + case 1: + _a.sent(); + console.log("\n\u001B[36m -> dryrun: would send to " + mail + "\u001B[0m"); + return [3, 6]; + case 2: html = template({ "name": name, "mail": mail, @@ -166,19 +205,26 @@ function send(name, mail, code, template, mailserver, config) { subject: "Dein Zugangscode zur BJR Wahl", html: html }; - _a.label = 1; - case 1: - _a.trys.push([1, 3, , 4]); - return [4, mailserver.sendMail(mailOptions)]; - case 2: - _a.sent(); - return [3, 4]; + _a.label = 3; case 3: + _a.trys.push([3, 5, , 6]); + return [4, mailserver.sendMail(mailOptions)]; + case 4: + _a.sent(); + return [3, 6]; + case 5: error_1 = _a.sent(); console.log("Error sendign mail to " + mail + " : " + error_1); - return [3, 4]; - case 4: return [2]; + return [3, 6]; + case 6: return [2]; } }); }); } +function delay(t, val) { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(val); + }, t); + }); +} diff --git a/dist/src/mailParser.js b/dist/src/mailParser.js new file mode 100644 index 0000000..0e788b8 --- /dev/null +++ b/dist/src/mailParser.js @@ -0,0 +1,70 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.parseMails = void 0; +var fs = __importStar(require("fs")); +function parseMails(config) { + return new Promise(function (resolve, reject) { + var mailArray = []; + var currSection = "global"; + var lineCounter = 0; + var curCounter = 0; + console.log("Reading mails for section " + currSection); + var readline = require('readline'), instream = fs.createReadStream(config.inFileMail), outstream = new (require('stream'))(), rl = readline.createInterface(instream, outstream); + rl.on('line', function (line) { + lineCounter++; + if (line.startsWith('[')) { + if (line.endsWith(']')) { + console.log("Read " + curCounter + " adresses for section " + currSection); + curCounter = 0; + currSection = line.substring(1, line.length - 1); + console.log("Reading mails for section " + currSection); + } + else { + console.error("Error parsing section on line " + lineCounter + ": Syntax Error. Missing closing bracket ]"); + } + } + else if (!line.startsWith('#')) { + var ix_1 = line.indexOf(";"); + if (ix_1 !== -1) { + if (config.force || config.usedMails.filter(function (el) { return el.mail == line.substr(0, ix_1); }).length == 0) { + mailArray.push({ + mail: line.substr(0, ix_1), + name: line.substr(ix_1 + 1) + }); + curCounter++; + } + else { + console.error("Skipping " + line.substr(0, ix_1) + ": Already sent"); + } + } + else { + console.error("Error parsing mail on line " + lineCounter + ": Syntax Error. Missing ;"); + } + } + }); + rl.on('close', function (line) { + console.log("Read " + curCounter + " adresses for section " + currSection + "\n" + mailArray.length + " mails read!"); + resolve(mailArray); + }); + }); +} +exports.parseMails = parseMails; diff --git a/dist/src/vault.js b/dist/src/vault.js index 4e7fa7a..0374278 100644 --- a/dist/src/vault.js +++ b/dist/src/vault.js @@ -60,11 +60,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); exports.SecureVault = void 0; var crypto = __importStar(require("crypto")); +var uuid = __importStar(require("uuid")); var path_1 = __importDefault(require("path")); var fs = __importStar(require("fs")); var crypto_1 = require("crypto"); +var vaultVersion = 'v1.2'; var SecureVault = (function () { function SecureVault(publicKey, privateKey) { + this.storage = []; this.safe = { items: [], publicKey: publicKey ? fs.readFileSync(path_1.default.resolve(publicKey)) : undefined, @@ -75,7 +78,7 @@ var SecureVault = (function () { } SecureVault.prototype.pushData = function (data) { return __awaiter(this, void 0, void 0, function () { - var txtData, key, iv, cipher, encrypted, buffer, asym_encrypted; + var txtData, key, iv, cipher, encrypted, buffer, asym_encrypted, u; return __generator(this, function (_a) { txtData = JSON.stringify(data); key = crypto.randomBytes(32); @@ -88,27 +91,45 @@ var SecureVault = (function () { throw new Error("Public Key not found"); } asym_encrypted = crypto.publicEncrypt(this.safe.publicKey, buffer); + u = uuid.v4(); this.safe.items.push({ + u: u, d: encrypted.toString('hex'), k: asym_encrypted.toString("base64"), iv: iv.toString('hex') }); - return [2]; + return [2, u]; }); }); }; SecureVault.prototype.saveData = function (path) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { - fs.writeFileSync(path, JSON.stringify(this.safe.items)); + fs.writeFileSync(path, JSON.stringify({ + version: vaultVersion, + vault: this.safe.items, + storage: this.storage + })); return [2]; }); }); }; SecureVault.prototype.loadData = function (path) { return __awaiter(this, void 0, void 0, function () { + var loaded; return __generator(this, function (_a) { - this.safe.items = JSON.parse(fs.readFileSync(path, 'utf8')); + loaded = JSON.parse(fs.readFileSync(path, 'utf8')); + switch (loaded.version) { + case 'v1.1': + this.safe.items = loaded.vault; + break; + case 'v1.2': + this.safe.items = loaded.vault; + this.storage = loaded.storage; + break; + default: + console.error("Unknown or unsupported vault file version: " + loaded.version); + } return [2]; }); }); @@ -151,6 +172,51 @@ var SecureVault = (function () { fs.writeFileSync(publicKeyDir, publicKey); }); }; + SecureVault.prototype.pushStorage = function (tag, data) { + if (vaultVersion !== 'v1.2') { + throw new Error("Storage not supported in " + vaultVersion); + } + else { + var objJsonStr = JSON.stringify(data); + var objJsonB64 = Buffer.from(objJsonStr).toString("base64"); + this.storage.push({ + u: uuid.v4(), + d: objJsonB64, + t: tag + }); + } + }; + SecureVault.prototype.setStorage = function (suuid, data) { + if (vaultVersion !== 'v1.2') { + throw new Error("Storage not supported in " + vaultVersion); + } + else { + var objJsonStr = JSON.stringify(data); + var objJsonB64 = Buffer.from(objJsonStr, "utf8").toString("base64"); + this.storage.filter(function (el) { return el.u == suuid; })[0].d = objJsonB64; + } + }; + SecureVault.prototype.getStorage = function (suuid) { + if (vaultVersion !== 'v1.2') { + throw new Error("Storage not supported in " + vaultVersion); + } + else { + var data = this.storage.filter(function (el) { return el.u == suuid; })[0]; + var objJsonB64 = new Buffer(data.d, 'base64'); + return JSON.parse(objJsonB64.toString('utf8')); + } + }; + SecureVault.prototype.findStorage = function (tag) { + if (vaultVersion !== 'v1.2') { + throw new Error("Storage not supported in " + vaultVersion); + } + else { + return this.storage.filter(function (el) { return el.t == tag; }); + } + }; + SecureVault.prototype.clearVault = function () { + this.safe.items = []; + }; return SecureVault; }()); exports.SecureVault = SecureVault; diff --git a/package.json b/package.json index 5bbcbbf..96e106a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", + "exec": "ts-node index.ts", "build": "rm bin/* -f && rm dist/* -Rf && tsc && pkg . --out-path bin --targets linux,macos,win" }, "author": "",