├── .travis.yml ├── index.js ├── jsdoc.json ├── password.proto ├── .eslintrc.js ├── package.json ├── LICENSE ├── .gitignore ├── README.md ├── __test__ ├── password.test.js ├── hash.test.js └── signature.test.js ├── hash.js ├── password.js ├── jest.config.js └── signature.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 11 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = undefined; 2 | 3 | /** 4 | * @global 5 | * @typedef {(string|Buffer|TypedArray|DataView)} Data 6 | */ 7 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "plugins/markdown" 4 | ], 5 | "opts": { 6 | "template": "node_modules/minami" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /password.proto: -------------------------------------------------------------------------------- 1 | enum Algorithm { 2 | INVALID = 1; 3 | SCRYPT = 2; 4 | } 5 | 6 | message Password { 7 | required Algorithm algorithm = 1; 8 | required bytes salt = 2; 9 | required float length = 3; 10 | required bytes hash = 4; 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parserOptions": { 3 | "ecmaVersion": 2017, 4 | }, 5 | "env": { 6 | "es6": true, 7 | "node": true, 8 | "jest": true 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-crypto", 3 | "version": "0.2.0", 4 | "description": "A WIP module aimed at providing a safer, easier to use and beginner friendly crypto API for Node.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "lint": "eslint *.js", 9 | "build-doc": "jsdoc README.md *.js -c jsdoc.json", 10 | "publish-doc": "gh-pages -d out" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ryzokuken/easy-crypto.git" 15 | }, 16 | "keywords": [ 17 | "crypto" 18 | ], 19 | "author": "Ujjwal Sharma ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/ryzokuken/easy-crypto/issues" 23 | }, 24 | "homepage": "https://ryzokuken.github.io/easy-crypto", 25 | "devDependencies": { 26 | "eslint": "^5.12.0", 27 | "gh-pages": "^2.0.1", 28 | "jest": "^23.6.0", 29 | "jsdoc": "^3.5.5", 30 | "minami": "^1.2.3" 31 | }, 32 | "dependencies": { 33 | "protocol-buffers": "^4.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ujjwal Sharma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | out 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # next.js build output 64 | .next 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easy-crypto 2 | 3 | > A WIP module aimed at providing a safer, easier to use and beginner friendly 4 | > crypto API for Node.js 5 | 6 | ![](https://img.shields.io/npm/v/easy-crypto.svg?style=plastic) 7 | ![](https://img.shields.io/travis/com/ryzokuken/easy-crypto.svg?style=plastic) 8 | ![](https://img.shields.io/badge/blessed-by%20core-green.svg) 9 | 10 | ## Goals 11 | 12 | 1. Make `crypto` **easy** to use. 13 | 2. Make `crypto` **safe** to use. 14 | 3. Require as little crypto-specific knowledge as possible. 15 | 16 | ## Features/Roadmap 17 | 18 | - [ ] Symmetric Encryption, Decryption and AEAD 19 | - [ ] Asymmetric Encryption and Decryption 20 | - [X] Asymmetric Signing and Verification of signatures 21 | - [X] Cryptographic hashing 22 | - [X] Password-based key derivation 23 | - [X] Password hashing and verification 24 | - [ ] Random number generation 25 | 26 | ## Installation 27 | 28 | ``` 29 | $ npm install easy-crypto 30 | ``` 31 | 32 | ## Usage 33 | 34 | Importing the module itself will return `undefined` since the behavior of the 35 | entire module is broken down into a set of intent-based submodules. 36 | 37 | ```js 38 | const password = require('easy-crypto/password'); 39 | 40 | const hashedPassword = password.hashPasswordSync('correct horse battery staple'); 41 | fs.writeFileSync('myfile', hashedPassword); // Ideally, store it in a database. 42 | ``` 43 | 44 | For an exhaustive list of all submodules and their members, check out the 45 | [API docs](https://ryzokuken.github.io/easy-crypto) 46 | 47 | ## License 48 | 49 | [MIT](LICENSE) 50 | 51 | `Copyright (c) 2019 Ujjwal Sharma` 52 | 53 | ## Notice 54 | 55 | This module is currently a work-in-progress. Please do not use it in production 56 | until before the `1.0.0` release since the API may break or might as well be 57 | outright unusable to unsafe. 58 | -------------------------------------------------------------------------------- /__test__/password.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const protobuf = require('protocol-buffers'); 5 | 6 | const password = require('../password'); 7 | 8 | const passphrase = 'correct horse battery staple'; 9 | const passwordPath = path.resolve(__dirname, '../password.proto'); 10 | 11 | test('hashes and verifies passwords synchronously', () => { 12 | const hashed = password.hashPasswordSync(passphrase); 13 | expect(password.verifyHashSync(hashed, passphrase)).toBe(true); 14 | }); 15 | 16 | test('hashes passwords asynchronously', async () => { 17 | expect.assertions(1); 18 | 19 | const hashed = await password.hashPassword(passphrase); 20 | expect(password.verifyHashSync(hashed, passphrase)).toBe(true); 21 | }); 22 | 23 | test('verifies passwords asynchronously', async () => { 24 | expect.assertions(1); 25 | 26 | const hashed = password.hashPasswordSync(passphrase); 27 | const verified = await password.verifyHash(hashed, passphrase); 28 | expect(verified).toBe(true); 29 | }); 30 | 31 | test('hashes and verifies passwords asynchronously', async () => { 32 | expect.assertions(1); 33 | 34 | const hashed = await password.hashPasswordSync(passphrase); 35 | const verified = await password.verifyHash(hashed, passphrase); 36 | expect(verified).toBe(true); 37 | }); 38 | 39 | test('fails for incorrect password', async () => { 40 | expect.assertions(1); 41 | 42 | const hashed = await password.hashPasswordSync(passphrase); 43 | const verified = await password.verifyHash(hashed, 'Hello, World!'); 44 | expect(verified).toBe(false); 45 | }); 46 | 47 | test('requires rehash', () => { 48 | const messages = protobuf(fs.readFileSync(passwordPath)); 49 | 50 | const hashed = password.hashPasswordSync(passphrase); 51 | const { salt, length, hash } = messages.Password.decode(hashed); 52 | const algorithm = messages.Algorithm.INVALID; 53 | const encoded = messages.Password.encode({ algorithm, salt, length, hash }); 54 | 55 | expect(() => password.verifyHashSync(encoded, passphrase)).toThrowError( 56 | password.InvalidHashError 57 | ); 58 | }); 59 | -------------------------------------------------------------------------------- /__test__/hash.test.js: -------------------------------------------------------------------------------- 1 | const stream = require('stream'); 2 | const hash = require('../hash'); 3 | 4 | const hashOutput = 5 | 'dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f'; 6 | 7 | test('hashes strings', () => { 8 | expect(hash.hash('sha256', 'Hello, World!').toString('hex')).toBe(hashOutput); 9 | }); 10 | 11 | test('hashes buffers', () => { 12 | const buf = Buffer.from('Hello, World!'); 13 | expect(hash.hash('sha256', buf).toString('hex')).toBe(hashOutput); 14 | }); 15 | 16 | test('hashes string streams', async () => { 17 | expect.assertions(1); 18 | 19 | const inputStream = new stream.Readable({ 20 | objectMode: true, 21 | read() {} 22 | }); 23 | process.nextTick(() => { 24 | inputStream.push('Hello, World!'); 25 | inputStream.push(null); 26 | }); 27 | 28 | const output = await hash.hashStream('sha256', inputStream); 29 | expect(output.toString('hex')).toBe(hashOutput); 30 | }); 31 | 32 | test('hashes buffer streams', async () => { 33 | expect.assertions(1); 34 | 35 | const inputStream = new stream.Readable({ 36 | objectMode: true, 37 | read() {} 38 | }); 39 | process.nextTick(() => { 40 | inputStream.push(Buffer.from('Hello, World!')); 41 | inputStream.push(null); 42 | }); 43 | 44 | const output = await hash.hashStream('sha256', inputStream); 45 | expect(output.toString('hex')).toBe(hashOutput); 46 | }); 47 | 48 | test('cannot hash blank streams', async () => { 49 | expect.assertions(1); 50 | 51 | const inputStream = new stream.Readable({ 52 | objectMode: true, 53 | read() {} 54 | }); 55 | process.nextTick(() => { 56 | inputStream.push(null); 57 | }); 58 | 59 | try { 60 | await hash.hashStream('sha256', inputStream); 61 | } catch (err) { 62 | expect(err).toEqual(new Error('No data to hash.')); 63 | } 64 | }); 65 | 66 | test('fails for inconsistent streams', async () => { 67 | expect.assertions(1); 68 | 69 | const inputStream = new stream.Readable({ 70 | objectMode: true, 71 | read() {} 72 | }); 73 | process.nextTick(() => { 74 | inputStream.push('Hello, World!'); 75 | inputStream.push(new Buffer('Hello, World!')); 76 | inputStream.push(null); 77 | }); 78 | 79 | try { 80 | await hash.hashStream('sha256', inputStream); 81 | } catch (err) { 82 | expect(err).toEqual(new Error('Inconsisent data.')); 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /hash.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | /** 4 | * The hash module contains a small set of utilities for computing hashes. 5 | * @module easy-crypto/hash 6 | */ 7 | 8 | /** 9 | * Hash a given message using the specified hash algorithm. 10 | * @since 0.1.0 11 | * @function hash 12 | * @param {string} algorithm The hashing algorithm to be used. 13 | * @param {Data} message The message to be hashed. 14 | * @param {string} inputEncoding The encoding of the `message`. If nothing is provided and `message` is a string, an encoding of `'utf8'` is enforced. If `message` is a Buffer, TypedArray, or DataView, then inputEncoding is ignored. 15 | * @param {string} outputEncoding The encoding of the output. If encoding is provided a string will be returned; otherwise a Buffer is returned. 16 | * @returns {string|Buffer} The hash of the input message. 17 | * @static 18 | */ 19 | function hash(algorithm, message, inputEncoding, outputEncoding) { 20 | const func = crypto.createHash(algorithm); 21 | func.update(message, inputEncoding); 22 | return func.digest(outputEncoding); 23 | } 24 | 25 | /** 26 | * Hash a message encapsulated inside a stream using the specified hash algorithm. 27 | * @since 0.1.0 28 | * @async 29 | * @function hashStream 30 | * @param {string} algorithm The hashing algorithm to be used. 31 | * @param {ReadableStream} input The stream containing the message to be hashed. 32 | * @param {string} inputEncoding The encoding of the `message`. If nothing is provided and `message` is a string, an encoding of `'utf8'` is enforced. If `message` is a Buffer, TypedArray, or DataView, then inputEncoding is ignored. 33 | * @param {string} outputEncoding The encoding of the output. If encoding is provided a string will be returned; otherwise a Buffer is returned. 34 | * @returns {Promise} The hash of the input message. 35 | * @static 36 | */ 37 | function hashStream(algorithm, input, inputEncoding, outputEncoding) { 38 | return new Promise((resolve, reject) => { 39 | let data; 40 | let wasBuffer; 41 | 42 | const dataHandler = chunk => { 43 | const isBuffer = Buffer.isBuffer(chunk); 44 | if ((!isBuffer && wasBuffer) || (isBuffer && wasBuffer === false)) { 45 | reject(new Error('Inconsisent data.')); 46 | input.removeListener('data', dataHandler); 47 | return; 48 | } 49 | 50 | if (isBuffer) { 51 | wasBuffer = true; 52 | chunk = chunk.toString('utf8'); 53 | } else { 54 | wasBuffer = false; 55 | } 56 | 57 | if (data === undefined) { 58 | data = chunk; 59 | } else { 60 | data += chunk; 61 | } 62 | }; 63 | input.on('data', dataHandler); 64 | 65 | input.on('end', () => { 66 | if (data === undefined) { 67 | reject(new Error('No data to hash.')); 68 | } else { 69 | resolve( 70 | hash( 71 | algorithm, 72 | data, 73 | wasBuffer ? 'utf8' : inputEncoding, 74 | outputEncoding 75 | ) 76 | ); 77 | } 78 | }); 79 | }); 80 | } 81 | 82 | module.exports = { 83 | hash, 84 | hashStream 85 | }; 86 | -------------------------------------------------------------------------------- /__test__/signature.test.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const stream = require('stream'); 3 | const signature = require('../signature'); 4 | 5 | const message = 'Hello, World!'; 6 | const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { 7 | modulusLength: 4096, 8 | publicKeyEncoding: { 9 | type: 'spki', 10 | format: 'pem' 11 | }, 12 | privateKeyEncoding: { 13 | type: 'pkcs8', 14 | format: 'pem' 15 | } 16 | }); 17 | 18 | test('it should work with simple sign and simple verify', () => { 19 | const signed = signature.sign(privateKey, 'sha256', message); 20 | expect(signature.verify(publicKey, 'sha256', message, signed)).toBe(true); 21 | }); 22 | 23 | test('it should work with simple sign and stream verify', async () => { 24 | expect.assertions(1); 25 | 26 | const signed = signature.sign(privateKey, 'sha256', message); 27 | const inputStream = new stream.Readable({ 28 | objectMode: true, 29 | read() {} 30 | }); 31 | process.nextTick(() => { 32 | inputStream.push('Hello, World!'); 33 | inputStream.push(null); 34 | }); 35 | 36 | const output = await signature.verifyStream( 37 | publicKey, 38 | 'sha256', 39 | inputStream, 40 | signed 41 | ); 42 | expect(output).toBe(true); 43 | }); 44 | 45 | test('it should work with stream sign and simple verify', async () => { 46 | expect.assertions(1); 47 | 48 | const inputStream = new stream.Readable({ 49 | objectMode: true, 50 | read() {} 51 | }); 52 | process.nextTick(() => { 53 | inputStream.push('Hello, World!'); 54 | inputStream.push(null); 55 | }); 56 | const signed = await signature.signStream(privateKey, 'sha256', inputStream); 57 | 58 | const output = await signature.verify(publicKey, 'sha256', message, signed); 59 | expect(output).toBe(true); 60 | }); 61 | 62 | test('it should work with stream sign and stream verify', async () => { 63 | expect.assertions(1); 64 | 65 | const inputStreamOne = new stream.Readable({ 66 | objectMode: true, 67 | read() {} 68 | }); 69 | process.nextTick(() => { 70 | inputStreamOne.push('Hello, World!'); 71 | inputStreamOne.push(null); 72 | }); 73 | const signed = await signature.signStream( 74 | privateKey, 75 | 'sha256', 76 | inputStreamOne 77 | ); 78 | 79 | const inputStreamTwo = new stream.Readable({ 80 | objectMode: true, 81 | read() {} 82 | }); 83 | process.nextTick(() => { 84 | inputStreamTwo.push('Hello, World!'); 85 | inputStreamTwo.push(null); 86 | }); 87 | 88 | const output = await signature.verifyStream( 89 | publicKey, 90 | 'sha256', 91 | inputStreamTwo, 92 | signed 93 | ); 94 | expect(output).toBe(true); 95 | }); 96 | 97 | test('it should accept string (pem) keys', () => { 98 | const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { 99 | modulusLength: 4096, 100 | publicKeyEncoding: { 101 | type: 'spki', 102 | format: 'pem' 103 | }, 104 | privateKeyEncoding: { 105 | type: 'pkcs8', 106 | format: 'pem' 107 | } 108 | }); 109 | 110 | const signed = signature.sign(privateKey, 'sha256', message); 111 | expect(signature.verify(publicKey, 'sha256', message, signed)).toBe(true); 112 | }); 113 | 114 | test('it should accept buffer (der) keys', () => { 115 | const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { 116 | modulusLength: 4096, 117 | publicKeyEncoding: { 118 | type: 'spki', 119 | format: 'der' 120 | }, 121 | privateKeyEncoding: { 122 | type: 'pkcs8', 123 | format: 'der' 124 | } 125 | }); 126 | 127 | const signed = signature.sign(privateKey, 'sha256', message); 128 | expect(signature.verify(publicKey, 'sha256', message, signed)).toBe(true); 129 | }); 130 | 131 | test('it should accept object (KeyObject) keys', () => { 132 | const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { 133 | modulusLength: 4096 134 | }); 135 | 136 | const signed = signature.sign(privateKey, 'sha256', message); 137 | expect(signature.verify(publicKey, 'sha256', message, signed)).toBe(true); 138 | }); 139 | 140 | test('it should accept dsa keys', () => { 141 | const { publicKey, privateKey } = crypto.generateKeyPairSync('dsa', { 142 | modulusLength: 4096, 143 | publicKeyEncoding: { 144 | type: 'spki', 145 | format: 'pem' 146 | }, 147 | privateKeyEncoding: { 148 | type: 'pkcs8', 149 | format: 'pem' 150 | } 151 | }); 152 | 153 | const signed = signature.sign(privateKey, 'sha256', message); 154 | expect(signature.verify(publicKey, 'sha256', message, signed)).toBe(true); 155 | }); 156 | 157 | test('it should accept ec keys', () => { 158 | const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', { 159 | namedCurve: 'sect239k1', 160 | publicKeyEncoding: { 161 | type: 'spki', 162 | format: 'pem' 163 | }, 164 | privateKeyEncoding: { 165 | type: 'pkcs8', 166 | format: 'pem' 167 | } 168 | }); 169 | 170 | const signed = signature.sign(privateKey, 'sha256', message); 171 | expect(signature.verify(publicKey, 'sha256', message, signed)).toBe(true); 172 | }); 173 | -------------------------------------------------------------------------------- /password.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | var protobuf = require('protocol-buffers'); 8 | 9 | /** 10 | * The password module provides utilities for dealing with passwords. This 11 | * includes hashing and verifying passwords as well as derive secure 12 | * cryptographic keys from passwords. 13 | * @module easy-crypto/password 14 | */ 15 | 16 | /** 17 | * The message format for encoding and decoding data using protobuf. 18 | * @constant {Object} 19 | * @inner 20 | */ 21 | const messages = protobuf( 22 | fs.readFileSync(path.resolve(__dirname, './password.proto')) 23 | ); 24 | 25 | /** 26 | * An error that hints that the hashing algorithm used is no longer valid and a 27 | * rehash is required. 28 | * @constant {Error} 29 | * @since 0.2.0 30 | * @static 31 | */ 32 | const InvalidHashError = new Error('Invalid algorithm, rehash required.'); 33 | 34 | /** 35 | * Derive a cryptographically secure key using a password and a salt. 36 | * @since 0.2.0 37 | * @async 38 | * @function deriveKey 39 | * @param {Data} password The password to be used 40 | * for key derivation. 41 | * @param {Data} salt The salt to be applied to the 42 | * password. The salt should be as unique as possible. It is recommended that a 43 | * salt is random and at least 16 bytes long. See NIST SP 800-132 for details. 44 | * @param {number} iterations The number of iterations to be performed. The 45 | * value must be a number set as high as possible. The higher the number of 46 | * iterations, the more secure the derived key will be, but will take a longer 47 | * amount of time to complete. 48 | * @param {number} keylen The length of the key to be produced. 49 | * @param {string} digest The HMAC digest algorithm to be used. 50 | * @returns {Promise} The derived key. 51 | * @static 52 | */ 53 | function deriveKey(password, salt, iterations, keylen, digest) { 54 | return new Promise((resolve, reject) => { 55 | crypto.pbkdf2(password, salt, iterations, keylen, digest, (err, key) => { 56 | if (err) reject(err); 57 | resolve(key); 58 | }); 59 | }); 60 | } 61 | 62 | /** 63 | * Derive a cryptographically secure key synchronously using a password and a salt. 64 | * @since 0.2.0 65 | * @function deriveKeySync 66 | * @param {Data} password The password to be used 67 | * for key derivation. 68 | * @param {Data} salt The salt to be applied to the 69 | * password. The salt should be as unique as possible. It is recommended that a 70 | * salt is random and at least 16 bytes long. See NIST SP 800-132 for details. 71 | * @param {number} iterations The number of iterations to be performed. The 72 | * value must be a number set as high as possible. The higher the number of 73 | * iterations, the more secure the derived key will be, but will take a longer 74 | * amount of time to complete. 75 | * @param {number} keylen The length of the key to be produced. 76 | * @param {string} digest The HMAC digest algorithm to be used. 77 | * @returns {Buffer} The derived key. 78 | * @static 79 | */ 80 | function deriveKeySync(password, salt, iterations, keylen, digest) { 81 | return crypto.pbkdf2Sync(password, salt, iterations, keylen, digest); 82 | } 83 | 84 | /** 85 | * Hash a password for storage. 86 | * @since 0.2.0 87 | * @async 88 | * @function hashPassword 89 | * @param {Data} password The password to be hashed. 90 | * @returns {Promise} The hashed password optimized for storage. 91 | * @static 92 | */ 93 | function hashPassword(password) { 94 | const salt = crypto.randomBytes(32); 95 | return new Promise((resolve, reject) => { 96 | crypto.scrypt(password, salt, 64, (err, hashedPassword) => { 97 | if (err) return reject(err); 98 | resolve( 99 | messages.Password.encode({ 100 | algorithm: messages.Algorithm.SCRYPT, 101 | salt, 102 | length: 64, 103 | hash: hashedPassword 104 | }) 105 | ); 106 | }); 107 | }); 108 | } 109 | 110 | /** 111 | * Hash a password synchronously for storage. 112 | * @since 0.2.0 113 | * @function hashPasswordSync 114 | * @param {Data} password The password to be hashed. 115 | * @returns {Buffer} The hashed password optimized for storage. 116 | * @static 117 | */ 118 | function hashPasswordSync(password) { 119 | const salt = crypto.randomBytes(32); 120 | const hashedPassword = crypto.scryptSync(password, salt, 64); 121 | return messages.Password.encode({ 122 | algorithm: messages.Algorithm.SCRYPT, 123 | salt, 124 | length: 64, 125 | hash: hashedPassword 126 | }); 127 | } 128 | 129 | /** 130 | * Verify a previously hashed and stored password. 131 | * @since 0.2.0 132 | * @async 133 | * @function verifyHash 134 | * @param {Buffer} hashed The hashed password to be verified. 135 | * @param {Data} password The actual password. 136 | * @returns {Promise} Wether the hash was valid for the given password. 137 | * @static 138 | */ 139 | function verifyHash(hashed, password) { 140 | return new Promise((resolve, reject) => { 141 | const { algorithm, salt, length, hash } = messages.Password.decode(hashed); 142 | if (algorithm !== messages.Algorithm.SCRYPT || hash.length !== length) 143 | return reject(InvalidHashError); 144 | crypto.scrypt(password, salt, 64, (err, recomputed) => { 145 | if (err) return reject(err); 146 | resolve(crypto.timingSafeEqual(recomputed, hash)); 147 | }); 148 | }); 149 | } 150 | 151 | /** 152 | * Verify a previously hashed and stored password synchronously. 153 | * @since 0.2.0 154 | * @function verifyHashSync 155 | * @param {Buffer} hashed The hashed password to be verified. 156 | * @param {Data} password The actual password. 157 | * @returns {Promise} Wether the hash was valid for the given password. 158 | * @throws {module:easy-crypto/password.InvalidHashError} The hash was produced 159 | * using an invalid algorithm. 160 | * A rehash with the currently valid algorithm is required. 161 | * @static 162 | */ 163 | function verifyHashSync(hashed, password) { 164 | const { algorithm, salt, length, hash } = messages.Password.decode(hashed); 165 | if (algorithm !== messages.Algorithm.SCRYPT || hash.length !== length) 166 | throw InvalidHashError; 167 | const recomputed = crypto.scryptSync(password, salt, 64); 168 | return crypto.timingSafeEqual(recomputed, hash); 169 | } 170 | 171 | module.exports = { 172 | deriveKey, 173 | deriveKeySync, 174 | hashPassword, 175 | hashPasswordSync, 176 | verifyHash, 177 | verifyHashSync, 178 | InvalidHashError 179 | }; 180 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after the first failure 9 | // bail: false, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/var/folders/l_/9mqk_6w1221637q69drm28km0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files usin a array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: null, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: null, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // An array of directory names to be searched recursively up from the requiring module's location 61 | // moduleDirectories: [ 62 | // "node_modules" 63 | // ], 64 | 65 | // An array of file extensions your modules use 66 | // moduleFileExtensions: [ 67 | // "js", 68 | // "json", 69 | // "jsx", 70 | // "node" 71 | // ], 72 | 73 | // A map from regular expressions to module names that allow to stub out resources with a single module 74 | // moduleNameMapper: {}, 75 | 76 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 77 | // modulePathIgnorePatterns: [], 78 | 79 | // Activates notifications for test results 80 | // notify: false, 81 | 82 | // An enum that specifies notification mode. Requires { notify: true } 83 | // notifyMode: "always", 84 | 85 | // A preset that is used as a base for Jest's configuration 86 | // preset: null, 87 | 88 | // Run tests from one or more projects 89 | // projects: null, 90 | 91 | // Use this configuration option to add custom reporters to Jest 92 | // reporters: undefined, 93 | 94 | // Automatically reset mock state between every test 95 | // resetMocks: false, 96 | 97 | // Reset the module registry before running each individual test 98 | // resetModules: false, 99 | 100 | // A path to a custom resolver 101 | // resolver: null, 102 | 103 | // Automatically restore mock state between every test 104 | // restoreMocks: false, 105 | 106 | // The root directory that Jest should scan for tests and modules within 107 | // rootDir: null, 108 | 109 | // A list of paths to directories that Jest should use to search for files in 110 | // roots: [ 111 | // "" 112 | // ], 113 | 114 | // Allows you to use a custom runner instead of Jest's default test runner 115 | // runner: "jest-runner", 116 | 117 | // The paths to modules that run some code to configure or set up the testing environment before each test 118 | // setupFiles: [], 119 | 120 | // The path to a module that runs some code to configure or set up the testing framework before each test 121 | // setupTestFrameworkScriptFile: null, 122 | 123 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 124 | // snapshotSerializers: [], 125 | 126 | // The test environment that will be used for testing 127 | testEnvironment: 'node', 128 | 129 | // Options that will be passed to the testEnvironment 130 | // testEnvironmentOptions: {}, 131 | 132 | // Adds a location field to test results 133 | // testLocationInResults: false, 134 | 135 | // The glob patterns Jest uses to detect test files 136 | // testMatch: [ 137 | // "**/__tests__/**/*.js?(x)", 138 | // "**/?(*.)+(spec|test).js?(x)" 139 | // ], 140 | 141 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 142 | // testPathIgnorePatterns: [ 143 | // "/node_modules/" 144 | // ], 145 | 146 | // The regexp pattern Jest uses to detect test files 147 | // testRegex: "", 148 | 149 | // This option allows the use of a custom results processor 150 | // testResultsProcessor: null, 151 | 152 | // This option allows use of a custom test runner 153 | // testRunner: "jasmine2", 154 | 155 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 156 | // testURL: "http://localhost", 157 | 158 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 159 | // timers: "real", 160 | 161 | // A map from regular expressions to paths to transformers 162 | // transform: null, 163 | 164 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 165 | // transformIgnorePatterns: [ 166 | // "/node_modules/" 167 | // ], 168 | 169 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 170 | // unmockedModulePathPatterns: undefined, 171 | 172 | // Indicates whether each individual test should be reported during the run 173 | // verbose: null, 174 | 175 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 176 | // watchPathIgnorePatterns: [], 177 | 178 | // Whether to use watchman for file crawling 179 | // watchman: true, 180 | }; 181 | -------------------------------------------------------------------------------- /signature.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | /** 4 | * The signature module contains a small set of basic utilities for generating 5 | * and verifying signatures. 6 | * @module easy-crypto/signature 7 | */ 8 | 9 | /** 10 | * Generate signatures for a message using a private key. 11 | * @since 0.1.0 12 | * @function sign 13 | * @param {Object|string|Buffer|KeyObject} privateKey The private key to be used for signing the message. 14 | * @param {string} algorithm The hashing algorithm to be used. 15 | * @param {Data} message The message to be signed. 16 | * @param {string} inputEncoding The encoding of the `message`. If `message` is a string and no value is provided, an encoding of `'utf8'` will be enforced. Ignored if message is a `Buffer`, `TypedArray` or `DataView`. 17 | * @param {string} outputEncoding The encoding of the output signature. If provided a `string` is returned, otherwise a `Buffer` is returned. 18 | * @returns {string|Buffer} The generated signature for the provided message. 19 | * @static 20 | */ 21 | function sign(privateKey, algorithm, message, inputEncoding, outputEncoding) { 22 | const signFunc = crypto.createSign(algorithm); 23 | signFunc.update(message, inputEncoding); 24 | signFunc.end(); 25 | return signFunc.sign(privateKey, outputEncoding); 26 | } 27 | 28 | /** 29 | * Generate signatures for a message encapsulated in a stream using a private key. 30 | * @since 0.1.0 31 | * @async 32 | * @function signStream 33 | * @param {Object|string|Buffer|KeyObject} privateKey The private key to be used for signing the message. 34 | * @param {string} algorithm The hashing algorithm to be used. 35 | * @param {ReadableStream} input The input stream containing the message to be signed. 36 | * @param {string} inputEncoding The encoding of the `message`. If `message` is a string and no value is provided, an encoding of `'utf8'` will be enforced. Ignored if message is a `Buffer`, `TypedArray` or `DataView`. 37 | * @param {string} outputEncoding The encoding of the output signature. If provided a `string` is returned, otherwise a `Buffer` is returned. 38 | * @returns {Promise} The generated signature for the provided message. 39 | * @static 40 | */ 41 | function signStream( 42 | privateKey, 43 | algorithm, 44 | input, 45 | inputEncoding, 46 | outputEncoding 47 | ) { 48 | return new Promise((resolve, reject) => { 49 | let data; 50 | let wasBuffer; 51 | 52 | const dataHandler = chunk => { 53 | const isBuffer = Buffer.isBuffer(chunk); 54 | if ((!isBuffer && wasBuffer) || (isBuffer && wasBuffer === false)) { 55 | reject(new Error('Inconsistent data.')); 56 | input.removeListener('data', dataHandler); 57 | return; 58 | } 59 | 60 | if (isBuffer) { 61 | wasBuffer = true; 62 | chunk = chunk.toString('utf8'); 63 | } else { 64 | wasBuffer = false; 65 | } 66 | 67 | if (data === undefined) { 68 | data = chunk; 69 | } else { 70 | data += chunk; 71 | } 72 | }; 73 | input.on('data', dataHandler); 74 | 75 | input.on('end', () => { 76 | if (data === undefined) { 77 | reject(new Error('No data to sign.')); 78 | } else { 79 | resolve( 80 | sign(privateKey, algorithm, data, inputEncoding, outputEncoding) 81 | ); 82 | } 83 | }); 84 | }); 85 | } 86 | 87 | /** 88 | * Verify if a signature is valid for a given message using the corresponding public key. 89 | * @since 0.1.0 90 | * @function verify 91 | * @param {Object|string|Buffer|KeyObject} publicKey The public key to be used for verifying the signature. 92 | * @param {string} algorithm The hashing algorithm to be used. 93 | * @param {Data} message The message for which the signature has been generated. 94 | * @param {string|Buffer} signature The signature to be verified. 95 | * @param {string} inputEncoding The encoding of the `message`. If `message` is a string and no value is provided, an encoding of `'utf8'` will be enforced. Ignored if message is a `Buffer`, `TypedArray` or `DataView`. 96 | * @param {string} signatureEncoding The encoding of the provided `signature`. If a signatureEncoding is specified, the signature is expected to be a string; otherwise signature is expected to be a Buffer, TypedArray, or DataView. 97 | * @returns {boolean} Wether the signature was valid or not. 98 | * @static 99 | */ 100 | function verify( 101 | publicKey, 102 | algorithm, 103 | message, 104 | signature, 105 | inputEncoding, 106 | signatureEncoding 107 | ) { 108 | const verifyFunc = crypto.createVerify(algorithm); 109 | verifyFunc.update(message, inputEncoding); 110 | verifyFunc.end(); 111 | return verifyFunc.verify(publicKey, signature, signatureEncoding); 112 | } 113 | 114 | /** 115 | * Verify if a signature is valid for a given message encapsulated in a stream using the corresponding public key. 116 | * @since 0.1.0 117 | * @async 118 | * @function verifyStream 119 | * @param {Object|string|Buffer|KeyObject} publicKey The public key to be used for verifying the signature. 120 | * @param {string} algorithm The hashing algorithm to be used. 121 | * @param {ReadableStream} input The stream containing the message for which the signature has been generated. 122 | * @param {string|Buffer} signature The signature to be verified. 123 | * @param {string} inputEncoding The encoding of the `message`. If `message` is a string and no value is provided, an encoding of `'utf8'` will be enforced. Ignored if message is a `Buffer`, `TypedArray` or `DataView`. 124 | * @param {string} signatureEncoding The encoding of the provided `signature`. If a signatureEncoding is specified, the signature is expected to be a string; otherwise signature is expected to be a Buffer, TypedArray, or DataView. 125 | * @returns {Promise} Wether the signature was valid or not. 126 | * @static 127 | */ 128 | function verifyStream( 129 | publicKey, 130 | algorithm, 131 | input, 132 | signature, 133 | inputEncoding, 134 | signatureEncoding 135 | ) { 136 | return new Promise((resolve, reject) => { 137 | let data; 138 | let wasBuffer; 139 | 140 | const dataHandler = chunk => { 141 | const isBuffer = Buffer.isBuffer(chunk); 142 | if ((!isBuffer && wasBuffer) || (isBuffer && wasBuffer === false)) { 143 | reject(new Error('Inconsistent data.')); 144 | input.removeListener('data', dataHandler); 145 | return; 146 | } 147 | 148 | if (isBuffer) { 149 | wasBuffer = true; 150 | chunk = chunk.toString('utf8'); 151 | } else { 152 | wasBuffer = false; 153 | } 154 | 155 | if (data === undefined) { 156 | data = chunk; 157 | } else { 158 | data += chunk; 159 | } 160 | }; 161 | input.on('data', dataHandler); 162 | 163 | input.on('end', () => { 164 | if (data === undefined) { 165 | reject(new Error('No data to sign.')); 166 | } else { 167 | resolve( 168 | verify( 169 | publicKey, 170 | algorithm, 171 | data, 172 | signature, 173 | inputEncoding, 174 | signatureEncoding 175 | ) 176 | ); 177 | } 178 | }); 179 | }); 180 | } 181 | 182 | module.exports = { sign, signStream, verify, verifyStream }; 183 | --------------------------------------------------------------------------------