diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..30cb1a0 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,38 @@ +{ + "env": { + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2017, + "sourceType": "module", + "impliedStrict": true, + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } + }, + "rules": { + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ], + "no-console": 0, + "no-unused-vars": "warn" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fb1670 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.basic256rc.js diff --git a/.travis.yml b/.travis.yml index 34a663e..ffdc18a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,7 @@ sudo: enabled group: edge node_js: - "node" - - "7" - - "6" - - "5" - - "4" + - "8" install: - npm install script: diff --git a/DontRunMe.js b/DontRunMe.js index 8eaf6f2..40850dd 100644 --- a/DontRunMe.js +++ b/DontRunMe.js @@ -1,28 +1,60 @@ -'use strict'; +"use strict"; -let crypto = require('crypto'); // define crypto -let fs = require('fs'); // define filesys +const detectNewline = require("detect-newline"); +const crypto = require("crypto"); // define crypto +const fs = require("fs"); // define filesys +let projectRoot = require("path").dirname(require.main.filename).replace(/[\/\\]node_modules[\/\\].*/g, ""); // eslint-disable-line no-useless-escape +let fetchedKey, fetchedHMAC, convertedConfig = false; -function randomValueHex (len) { +const exit = (msg) => { + console.log(msg); + return setTimeout(() => { + process.exit(0); + }, 2000); +}; + +const randomValueHex = (len) => { return crypto.randomBytes(Math.ceil(len/2)) - .toString('hex') // convert to hexadecimal format + .toString("hex") // convert to hexadecimal format .slice(0,len); // return required number of characters }; -if (fs.existsSync("./config.js")) { - return setTimeout(function(){ - process.exit(0); // exit script if config already exists - }, 833); -} +const main = () => { + if (fs.existsSync(`${projectRoot}/.gitignore`)) { + var file = fs.readFileSync(`${projectRoot}/.gitignore`).toString(); + var newlineChar = detectNewline(file); + if (!file.includes(".basic256rc.js")) fs.appendFileSync(`${projectRoot}/.gitignore`, `${newlineChar}.basic256rc.js${newlineChar}`); + } + + if (fs.existsSync(`${projectRoot}/.basic256rc.js`)) { + return exit("\n.basic256rc.js already exists, stopping setup.\n"); + } + + if (fs.existsSync("./config.js")) { + try { + var c = require("./config.js").k; + if (c.key) fetchedKey = c.key; + if (c.hmac_key) fetchedHMAC = c.hmac_key; + convertedConfig = true; + } catch (e) { + fetchedKey = null, + fetchedHMAC = null; + console.warn(`\nThere is an old config.js file in package.\nHowever, reading of the keys have failed:\n\n${e.stack}\n`); + } + } -let key = randomValueHex(32); // create random hex val for enc key -let hmac = randomValueHex(32); // create random hex val for hmac key -let randFold = "./" + randomValueHex(32) + "/"; // create random hex val with filesys encoding for folder -let randFile = randomValueHex(32) + ".json"; // create random hex val with .json ending for file -let resSysop = randFold + randFile; // merge foldername and filename + const enduserconfig = { + key: fetchedKey || randomValueHex(32), // create random hex val for enc key + hmac_key: fetchedHMAC || randomValueHex(32) // create random hex val for hmac key + }; -fs.mkdirSync(randFold); // create folder -fs.appendFileSync(resSysop, "{\n \"key\": \"" + key + "\",\n \"hmac_key\": \"" + hmac + "\"\n}\n"); // create file with keys necessary -fs.appendFileSync("./config.js", "\'use strict\';\n\nvar k = require(\"" + resSysop + "\");\n\nmodule.exports = {\n k\n};\n\n"); // generate config file with necessary info + fs.appendFileSync(`${projectRoot}/.basic256rc.js`, `"use strict"; + +module.exports = ${JSON.stringify(enduserconfig, null, 4)} +`); // generate config file with necessary info + + if (convertedConfig) return exit("\nYour old configuration is saved to a file named .basic256rc.js has been created on the project root.\nDON'T FORGET TO BACK THIS UP.\n"); + return exit("\nA file named .basic256rc.js has been created on the project root. DON'T FORGET TO BACK THIS UP.\n"); +}; -setTimeout(function(){process.exit(0);}, 2000); // exit script +main(); diff --git a/LICENSE b/LICENSE index 2ccfd4d..3d847b3 100644 --- a/LICENSE +++ b/LICENSE @@ -176,7 +176,7 @@ END OF TERMS AND CONDITIONS Copyright 2014 Levi Gross - Copyright 2016 linuxgemini. All Rights Reserved. + Copyright 2018 linuxgemini. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index c39df8c..c8bd960 100644 --- a/README.md +++ b/README.md @@ -3,29 +3,37 @@ basic256.js [![Build Status](https://travis-ci.org/linuxgemini/basic256.js.svg?branch=master)](https://travis-ci.org/linuxgemini/basic256.js) +WARNING +------- + +**THIS PACKAGE SAVES IMPORTANT KEYS ON YOUR PROJECT, DON'T LOSE IT.** + A basic encryption/decryption script/API for resting data for Node.js users. *Slightly* modified the work of [Levi Gross](http://www.levigross.com/2014/03/30/how-to-write-an-encrypt-and-decrypt-api-for-data-at-rest-in-nodejs/). -Usage without downloading from NPM +Usage ----- -Gather basic256.js first, copy all files inside to your project folder/direcory. +Open a terminal in your project folder and make sure that you have a package.json file. -And do this on your terminalif you are not root: +And do this on your terminal if you are not root: - npm install +` +$ npm install --save basic256.js +` -If you are running as root, do this: +Then make your script connected. Example: - npm install --unsafe-perm +```js +const b256 = require("basic256.js"); +const basic256 = new b256(); -Then make your script connected. Example: +var blob = basic256.encrypt("FOO"); // This encrypts the string "FOO". +console.log(blob); // This will show the encrypted string. + +var unblob = basic256.decrypt(blob); // This decrypts the encrypted string. +console.log(unblob); // This will show the decrypted string. (Which in this case, it is "FOO") +``` - var crypter = require("./basic256.js"); - - var blob = crypter.enc.run("FOO"); // This encrypts the string "FOO". - console.log(blob); // This will show the encrypted string. - - var unblob = crypter.dec.run(blob); // This decrypts the encrypted string. - console.log(unblob); // This will show the decrypted string. (Which in this case, it is "FOO") +**Don't forget to back your .basic256rc.js file as it contains your keys to encrypt and decrypt strings.** diff --git a/basic256.js b/basic256.js index 304b2f4..b27b4f0 100644 --- a/basic256.js +++ b/basic256.js @@ -1,90 +1,93 @@ -'use strict'; +"use strict"; + +let crypto = require("crypto"); +let projectRoot = require("path").dirname(require.main.filename).replace(/[\/\\]node_modules[\/\\].*/g, ""); // eslint-disable-line no-useless-escape + +/** + * A basic encryption/decryption script/API for resting data for Node.js users. + * @class + */ +class basic256 { + constructor() { + try { + var savedKeys = require(`${projectRoot}/.basic256rc.js`); + } catch (e) { + throw new Error("An error happened while loading the key"); + } -let crypto = require('crypto'); + this.ALGORITHM = "AES-256-CBC"; + this.HMAC_ALGORITHM = "SHA256"; + this.KEY = savedKeys.key; // Use the automated script. + this.HMAC_KEY = savedKeys.hmac_key; // Use the automated script. + } -function UserException(message) { - this.message = message; - this.name = 'UserException'; -} + encrypt(plain_text) { + if (!plain_text || typeof (plain_text) !== "string") throw new Error("Plain text was not found."); -try { - var savedKeys = require("./config.js").k; -} catch (e) { - throw new UserException('No Configuration Exists'); -} + var IV = Buffer.from(tools.randomValueHex(16)); // ensure that the IV (initialization vector) is random + var encryptor, cipher_text, hmac; -var ALGORITHM, KEY, HMAC_ALGORITHM, HMAC_KEY; + encryptor = crypto.createCipheriv(this.ALGORITHM, this.KEY, IV); + encryptor.setEncoding("hex"); + encryptor.write(plain_text); + encryptor.end(); -ALGORITHM = 'AES-256-CBC'; // CBC because CTR isn't possible with the current version of the Node.JS crypto library -HMAC_ALGORITHM = 'SHA256'; -KEY = savedKeys.key; // Use the automated script. -HMAC_KEY = savedKeys.hmac_key; // Use the automated script. + cipher_text = encryptor.read(); -function randomValueHex (len) { - return crypto.randomBytes(Math.ceil(len/2)) - .toString('hex') // convert to hexadecimal format - .slice(0,len); // return required number of characters -}; + hmac = crypto.createHmac(this.HMAC_ALGORITHM, this.HMAC_KEY); + hmac.update(cipher_text); + hmac.update(IV.toString("hex")); // ensure that both the IV and the cipher-text is protected by the HMAC -var constant_time_compare = function (val1, val2) { - var sentinel; - - if (val1.length !== val2.length) { - return false; + // The IV isn't a secret so it can be stored along side everything else + return cipher_text + "$" + IV.toString("hex") + "$" + hmac.digest("hex"); } + decrypt(cipher_text) { + if (!cipher_text || typeof (cipher_text) !== "string" || !cipher_text.match(/\$/g)) throw new Error("A valid cipher text was not found."); - for (var i = 0; i <= (val1.length - 1); i++) { - sentinel |= val1.charCodeAt(i) ^ val2.charCodeAt(i); - } + var cipher_blob = cipher_text.split("$"); - return sentinel === 0 -}; + if (cipher_blob.length !== 3) throw new Error("Cipher text is broken."); -module.exports = { + var ct = cipher_blob[0]; + var IV = Buffer.from(cipher_blob[1], "hex"); + var hmac = cipher_blob[2]; + var chmac, decryptor; - "enc": { - run : function (plain_text) { + chmac = crypto.createHmac(this.HMAC_ALGORITHM, this.HMAC_KEY); + chmac.update(ct); + chmac.update(IV.toString("hex")); - var IV = Buffer.from(randomValueHex(16)); // ensure that the IV (initialization vector) is random - var encryptor, cipher_text, hmac; + if (!tools.constant_time_compare(chmac.digest("hex"), hmac)) { + throw new Error("Encrypted Blob has been tampered with."); + } - encryptor = crypto.createCipheriv(ALGORITHM, KEY, IV); - encryptor.setEncoding('hex'); - encryptor.write(plain_text); - encryptor.end(); + decryptor = crypto.createDecipheriv(this.ALGORITHM, this.KEY, IV); + var decryptedText = decryptor.update(ct, "hex", "utf-8"); + return decryptedText + decryptor.final("utf-8"); + } - cipher_text = encryptor.read(); +} - hmac = crypto.createHmac(HMAC_ALGORITHM, HMAC_KEY); - hmac.update(cipher_text); - hmac.update(IV.toString('hex')); // ensure that both the IV and the cipher-text is protected by the HMAC +class tools { + static constant_time_compare(val1, val2) { + var sentinel; - // The IV isn't a secret so it can be stored along side everything else - return cipher_text + "$" + IV.toString('hex') + "$" + hmac.digest('hex') + if (val1.length !== val2.length) { + return false; } - }, - - "dec": { - run : function (cipher_text) { - var cipher_blob = cipher_text.split("$"); - var ct = cipher_blob[0]; - var IV = Buffer.from(cipher_blob[1], 'hex'); - var hmac = cipher_blob[2]; - var chmac, decryptor; - - chmac = crypto.createHmac(HMAC_ALGORITHM, HMAC_KEY); - chmac.update(ct); - chmac.update(IV.toString('hex')); - - if (!constant_time_compare(chmac.digest('hex'), hmac)) { - console.log("Encrypted Blob has been tampered with..."); - return null; - } - - decryptor = crypto.createDecipheriv(ALGORITHM, KEY, IV); - var decryptedText = decryptor.update(ct, 'hex', 'utf-8'); - return decryptedText + decryptor.final('utf-8'); + + for (var i = 0; i <= (val1.length - 1); i++) { + sentinel |= val1.charCodeAt(i) ^ val2.charCodeAt(i); } + + return sentinel === 0; + } + static randomValueHex(len) { + return crypto.randomBytes(Math.ceil(len / 2)) + .toString("hex") // convert to hexadecimal format + .slice(0, len); // return required number of characters } } + +module.exports = basic256; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..26253dc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "basic256.js", + "version": "1.2.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" + } + } +} diff --git a/package.json b/package.json index 12c0918..865a697 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "basic256.js", - "version": "0.0.1", + "version": "1.2.3", "description": "A basic encryption/decryption script/API for resting data for Node.js users.", + "engines": { + "node": ">=8.4.0" + }, "readme": "README.md", "maintainers": [ "linuxgemini (ilteris@asenkron.com.tr)" @@ -13,7 +16,11 @@ }, "license": "Apache-2.0", "scripts": { - "install": "node ./DontRunMe.js", - "test": "node ./test.js" - } + "install": "node DontRunMe.js", + "test": "node test.js" + }, + "dependencies": { + "detect-newline": "^2.1.0" + }, + "main": "./basic256" } diff --git a/test.js b/test.js index 0e51652..d7622d6 100644 --- a/test.js +++ b/test.js @@ -1,17 +1,59 @@ -'use strict'; +"use strict"; -var m = require("./basic256"); +try { + var base = require("./basic256"); +} catch (error) { + setTimeout(() => { + console.error(`Huge error in library\n${error.stack}`); + process.exit(1); + }, 1000); +} -console.log("Encrypting string \"foo\"..."); -var encStr = m.enc.run("foo"); -console.log("\nDecrypting the string below...\n" + encStr); -var decStr = m.dec.run(encStr); -console.log("\n\nResult: " + decStr); +const basic256 = new base(); -if (decStr === "foo") { - console.log("\nSUCCESS!"); - setTimeout(function(){process.exit(0);},853); -} else { - console.error("\nFAILURE!"); - setTimeout(function(){process.exit(1);},853); +const testText = "Lorem ipsum dolor sit amet."; +let errCount = 0; +let successCount = 0; + +var ciphertext, returningtext; + +try { + ciphertext = basic256.encrypt(testText); + returningtext = basic256.decrypt(ciphertext); + if (returningtext === testText) { + successCount++; + console.log("Initial example works."); + } +} catch (e) { + console.log("Initial example doesn't work."); + errCount++; +} + +try { + ciphertext = basic256.encrypt(testText.split(" ")); // planned error. + errCount++; +} catch (er) { + console.log("String detection before encryption works."); + successCount++; +} + +try { + ciphertext = basic256.encrypt(testText); + returningtext = basic256.decrypt(ciphertext.slice(3)); // planned error. + errCount++; +} catch (err) { + console.log("Cipher text tampering detection works."); + successCount++; } + +if (errCount === 0 && successCount === 3) { + setTimeout(() => { + console.log("Test passed."); + process.exit(0); + }, 2222); +} else { + setTimeout(() => { + console.log("Test failed."); + process.exit(1); + }, 2222); +} \ No newline at end of file