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 new file mode 100644 index 0000000..ffdc18a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +sudo: enabled +group: edge +node_js: + - "node" + - "8" +install: + - npm install +script: + - npm test diff --git a/Basic256.js b/Basic256.js deleted file mode 100644 index f0db765..0000000 --- a/Basic256.js +++ /dev/null @@ -1,81 +0,0 @@ -// HELP ME MAKE THIS SHITTY CIPHER API GREAT AGAIN // - -crypto = require('crypto'); - -function randomValueHex (len) { - return crypto.randomBytes(Math.ceil(len/2)) - .toString('hex') // convert to hexadecimal format - .slice(0,len); // return required number of characters -}; - -var ALGORITHM, KEY, HMAC_ALGORITHM, HMAC_KEY; - -ALGORITHM = 'AES-256-CBC'; // CBC because CTR isn't possible with the current version of the Node.JS crypto library -HMAC_ALGORITHM = 'SHA256'; -KEY = randomValueHex(32); // This key should be stored in an environment variable -HMAC_KEY = randomValueHex(32); // This key should be stored in an environment variable - -var constant_time_compare = function (val1, val2) { - var sentinel; - - if (val1.length !== val2.length) { - return false; - } - - - for (var i = 0; i <= (val1.length - 1); i++) { - sentinel |= val1.charCodeAt(i) ^ val2.charCodeAt(i); - } - - return sentinel === 0 -}; - -module.exports = { - - "enc": { - run : function (plain_text) { - - var IV = new Buffer(randomValueHex(16)); // ensure that the IV (initialization vector) is random - var cipher_text; - var hmac; - var encryptor; - - encryptor = crypto.createCipheriv(ALGORITHM, KEY, IV); - encryptor.setEncoding('hex'); - encryptor.write(plain_text); - encryptor.end(); - - 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 - - // The IV isn't a secret so it can be stored along side everything else - return cipher_text + "$" + IV.toString('hex') + "$" + hmac.digest('hex') - } - }, - - "dec": { - run : function (cipher_text) { - var cipher_blob = cipher_text.split("$"); - var ct = cipher_blob[0]; - var IV = new Buffer(cipher_blob[1], 'hex'); - var hmac = cipher_blob[2]; - var 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', 'utf8'); - return decryptedText + decryptor.final('utf-8'); - } - } -} diff --git a/DontRunMe.js b/DontRunMe.js new file mode 100644 index 0000000..40850dd --- /dev/null +++ b/DontRunMe.js @@ -0,0 +1,60 @@ +"use strict"; + +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; + +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 + .slice(0,len); // return required number of characters +}; + +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`); + } + } + + 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.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"); +}; + +main(); diff --git a/LICENSE b/LICENSE index 2cf16c0..3d847b3 100644 --- a/LICENSE +++ b/LICENSE @@ -175,8 +175,8 @@ END OF TERMS AND CONDITIONS - Copyright 2014 Levi Gross, tandrewnichols - Copyright 2016 linuxgemini. All Rights Reserved. + Copyright 2014 Levi Gross + 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 f0545af..c8bd960 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,39 @@ -Basic256.js +basic256.js ========================= -A basic encryption/decription script/API for Node.js users. -Based on the work by [Levi Gross](http://www.levigross.com/2014/03/30/how-to-write-an-encrypt-and-decrypt-api-for-data-at-rest-in-nodejs/). +[![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 ----- -Gather Basic256.js, -Make your script connected. Example: +Open a terminal in your project folder and make sure that you have a package.json file. + +And do this on your terminal if you are not root: + +` +$ npm install --save basic256.js +` + +Then make your script connected. Example: + +```js +const b256 = require("basic256.js"); +const basic256 = new b256(); + +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 new file mode 100644 index 0000000..b27b4f0 --- /dev/null +++ b/basic256.js @@ -0,0 +1,93 @@ +"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"); + } + + 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. + } + + encrypt(plain_text) { + if (!plain_text || typeof (plain_text) !== "string") throw new Error("Plain text was not found."); + + var IV = Buffer.from(tools.randomValueHex(16)); // ensure that the IV (initialization vector) is random + var encryptor, cipher_text, hmac; + + encryptor = crypto.createCipheriv(this.ALGORITHM, this.KEY, IV); + encryptor.setEncoding("hex"); + encryptor.write(plain_text); + encryptor.end(); + + cipher_text = encryptor.read(); + + 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 + + // 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."); + + var cipher_blob = cipher_text.split("$"); + + if (cipher_blob.length !== 3) throw new Error("Cipher text is broken."); + + var ct = cipher_blob[0]; + var IV = Buffer.from(cipher_blob[1], "hex"); + var hmac = cipher_blob[2]; + var chmac, decryptor; + + chmac = crypto.createHmac(this.HMAC_ALGORITHM, this.HMAC_KEY); + chmac.update(ct); + chmac.update(IV.toString("hex")); + + if (!tools.constant_time_compare(chmac.digest("hex"), hmac)) { + throw new Error("Encrypted Blob has been tampered with."); + } + + decryptor = crypto.createDecipheriv(this.ALGORITHM, this.KEY, IV); + var decryptedText = decryptor.update(ct, "hex", "utf-8"); + return decryptedText + decryptor.final("utf-8"); + } + +} + +class tools { + static constant_time_compare(val1, val2) { + var sentinel; + + if (val1.length !== val2.length) { + return false; + } + + 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 new file mode 100644 index 0000000..865a697 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "basic256.js", + "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)" + ], + "author": "linuxgemini", + "repository": { + "type": "git", + "url": "git+https://github.com/linuxgemini/basic256.js.git" + }, + "license": "Apache-2.0", + "scripts": { + "install": "node DontRunMe.js", + "test": "node test.js" + }, + "dependencies": { + "detect-newline": "^2.1.0" + }, + "main": "./basic256" +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..d7622d6 --- /dev/null +++ b/test.js @@ -0,0 +1,59 @@ +"use strict"; + +try { + var base = require("./basic256"); +} catch (error) { + setTimeout(() => { + console.error(`Huge error in library\n${error.stack}`); + process.exit(1); + }, 1000); +} + +const basic256 = new base(); + +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