├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── cjs ├── helpers.js ├── index.js ├── package.json └── web.js ├── esm ├── helpers.js ├── index.js └── web.js ├── package.json └── test ├── index.html ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | test/dest/ 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | rollup/ 4 | test/ 5 | package-lock.json 6 | .travis.yml 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - 12 5 | git: 6 | depth: 1 7 | branches: 8 | only: 9 | - master 10 | after_success: 11 | - "npm run coveralls" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secretly 2 | 3 | [![Build Status](https://travis-ci.com/WebReflection/secretly.svg?branch=master)](https://travis-ci.com/WebReflection/secretly) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/secretly/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/secretly?branch=master) 4 | 5 | **Social Media Photo by [Felix Hanspach](https://unsplash.com/@fhanspach) on [Unsplash](https://unsplash.com/)** 6 | 7 | 8 | A basic class to encrypt and decrypt. 9 | 10 | ## Usage 11 | 12 | ```js 13 | // NodeJS 14 | import Secretly from 'secretly'; 15 | // or const Secretly = require('secretly'); 16 | 17 | // Web 18 | // import Secretly from 'secretly/web'; 19 | // or import Secretly from 'https://unpkg.com/secretly/esm/web.js'; 20 | 21 | const pvt = new Secretly(secret, Secretly.PATH); 22 | 23 | const encrypted = await pvt.encrypt('any text'); 24 | const decrypted = await pvt.decrypt(encrypted); 25 | ``` 26 | 27 | ## API 28 | 29 | * `constructor(password, salt = Secretly.PATH, random = true)` where both `password` and `salt` cannot be empty strings. The `Secretly.PATH` is the `process.cwd()` in *NodeJS*, and the current location up to the last `/` in the browser. The third `random` is used to have *different* results across sessions, while if forced to `false` there won't be randomness in the derived *iv key*, so while encrypted content will be reusable across different sessions, assuming also the `salt` is the same, it might be less secure. 30 | * `async encrypt(plain_text) => encrypted_hex` 31 | * `async decrypt(encrypted_hex) => plain_text` 32 | 33 | ## Compatibility 34 | 35 | This module requires *ES2015* compatible browsers on the client side, and *NodeJS* 15+ on the backend for the native `crypto.webcrypto` API, which is polyfilled via [node-webcrypto-ossl](https://www.npmjs.com/package/node-webcrypto-ossl). 36 | 37 | If interested in using the synchronous, *NodeJS* only version of this module, which produces different results but in terms of API it works identically, you can use `secretly@1` instead, which has been successfully tested, and used, from *NodeJS* version *8* up to version *15*. 38 | 39 | ### Breaking V2 40 | 41 | After bringing this module to the *Web*, and discovering that *NodeJS* has a `crypto.webcrypto` that works the same, I've decided to make this module identical for both *Web* and *NodeJS*, making it portable client/server. 42 | -------------------------------------------------------------------------------- /cjs/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const $String = String; 3 | const $TextDecoder = TextDecoder; 4 | const $TextEncoder = TextEncoder; 5 | const $Uint8Array = Uint8Array; 6 | const $parseInt = parseInt; 7 | 8 | // https://github.com/WebReflection/caller-of#readme 9 | const callerOf = method => method.call.bind(method); 10 | const proto = ({prototype}) => prototype; 11 | 12 | const wm = new WeakMap; 13 | const get = wm.get.bind(wm); 14 | const set = wm.set.bind(wm); 15 | 16 | const arrayProto = proto(Array); 17 | const join = callerOf(arrayProto.join); 18 | const map = callerOf(arrayProto.map); 19 | const push = callerOf(arrayProto.push); 20 | 21 | const stringProto = proto($String); 22 | const slice = callerOf(stringProto.slice); 23 | const substr = callerOf(stringProto.substr); 24 | 25 | const toString = callerOf(proto(Number).toString); 26 | 27 | const decode = callerOf(proto($TextDecoder).decode); 28 | const encode = callerOf(proto($TextEncoder).encode); 29 | 30 | const {freeze} = Object; 31 | 32 | const securedClass = (crypto, PATH, asHex, fromHex) => { 33 | const getRandomValues = crypto.getRandomValues.bind(crypto); 34 | 35 | const {subtle} = crypto; 36 | const decrypt = callerOf(subtle.decrypt); 37 | const encrypt = callerOf(subtle.encrypt); 38 | const deriveKey = callerOf(subtle.deriveKey); 39 | const importKey = callerOf(subtle.importKey); 40 | 41 | // big thanks to Webbjocke article for this bit of the Web Crypto API 42 | // https://webbjocke.com/javascript-web-encryption-and-hashing-with-the-crypto-api/ 43 | const createKey = async (password, salt) => { 44 | const name = 'PBKDF2'; 45 | const derived = {name: 'AES-GCM', length: 256}; 46 | const encoded = encode(new $TextEncoder, password); 47 | const key = await importKey(subtle, 'raw', encoded, {name}, false, ['deriveKey']); 48 | return deriveKey( 49 | subtle, 50 | { 51 | name, 52 | hash: 'SHA-256', 53 | salt: encode(new $TextEncoder, salt), 54 | iterations: 1000 55 | }, 56 | key, 57 | derived, 58 | false, 59 | ['encrypt', 'decrypt'] 60 | ); 61 | }; 62 | 63 | class Secretly { 64 | 65 | /** 66 | * Used as default salt, it returns `process.cwd()` in NodeJS, 67 | * and location up to the last path's `/` on the Web. 68 | * @returns {string} 69 | */ 70 | static get PATH() { return PATH; } 71 | 72 | /** 73 | * Initialize a Secretly instance. 74 | * @param {string} password a generic password. 75 | * @param {string?} salt a non-empty string to use as salt, it's Secretly.PATH by default. 76 | * @param {boolean?} random avoid iv's randomness to enable enc/decryption across sessions. 77 | */ 78 | constructor( 79 | password, 80 | salt = PATH, 81 | random = true 82 | ) { 83 | if (!password || !salt) { 84 | password = salt = ''; 85 | throw new TypeError(`invalid password or salt`); 86 | } 87 | const buffer = new $Uint8Array(16); 88 | set(this, { 89 | key: createKey($String(password), $String(salt)), 90 | info: { 91 | name: 'AES-GCM', 92 | length: 256, 93 | iv: random ? 94 | getRandomValues(buffer) : 95 | buffer 96 | } 97 | }); 98 | } 99 | 100 | /** 101 | * Decrypt a previously encrypted string. 102 | * @param {string} encrypted an encrypted string as hex. 103 | * @returns {Promise} 104 | */ 105 | async decrypt(encrypted) { 106 | const {key, info} = get(this); 107 | return decode( 108 | new $TextDecoder, 109 | await decrypt(subtle, info, await key, fromHex(encrypted)) 110 | ); 111 | } 112 | 113 | /** 114 | * Encrypt a generic string. 115 | * @param {string} decrypted a generic string to encrypt. 116 | * @returns {Promise} 117 | */ 118 | async encrypt(decrypted) { 119 | const {key, info} = get(this); 120 | const encrypted = encode(new $TextEncoder, decrypted); 121 | return asHex(await encrypt(subtle, info, await key, encrypted)); 122 | } 123 | } 124 | 125 | freeze(proto(Secretly)); 126 | 127 | return freeze(Secretly); 128 | }; 129 | 130 | exports.Uint8Array = $Uint8Array; 131 | exports.parseInt = $parseInt; 132 | exports.callerOf = callerOf; 133 | exports.proto = proto; 134 | exports.securedClass = securedClass; 135 | exports.join = join; 136 | exports.map = map; 137 | exports.push = push; 138 | exports.slice = slice; 139 | exports.substr = substr; 140 | exports.toString = toString; 141 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {webcrypto} = require('crypto'); 3 | const {cwd} = require('process'); 4 | const {Crypto} = require('node-webcrypto-ossl'); 5 | 6 | const {callerOf, proto, securedClass} = require('./helpers.js'); 7 | 8 | const {from} = Buffer; 9 | const toString = callerOf(proto(Buffer).toString); 10 | 11 | module.exports = securedClass( 12 | webcrypto || new Crypto, 13 | cwd(), 14 | buffer => toString(from(buffer), 'hex'), 15 | str => from(str, 'hex') 16 | ); 17 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /cjs/web.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Uint8Array, 4 | parseInt, 5 | securedClass, 6 | join, 7 | map, 8 | push, 9 | slice, 10 | substr, 11 | toString 12 | } = require('./helpers.js'); 13 | 14 | const {protocol, host, pathname} = location; 15 | 16 | const asHEX = i => slice('0' + toString(i, 16), -2); 17 | 18 | module.exports = securedClass( 19 | self.crypto, 20 | protocol + '//' + host + pathname.replace(/\/[^/]*$/i, ''), 21 | buffer => join(map(new Uint8Array(buffer), asHEX), ''), 22 | str => { 23 | const bytes = []; 24 | for (let i = 0, {length} = str; i < length; i += 2) 25 | push(bytes, parseInt(substr(str, i, 2), 16)); 26 | return new Uint8Array(bytes); 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /esm/helpers.js: -------------------------------------------------------------------------------- 1 | const $String = String; 2 | const $TextDecoder = TextDecoder; 3 | const $TextEncoder = TextEncoder; 4 | const $Uint8Array = Uint8Array; 5 | const $parseInt = parseInt; 6 | 7 | // https://github.com/WebReflection/caller-of#readme 8 | const callerOf = method => method.call.bind(method); 9 | const proto = ({prototype}) => prototype; 10 | 11 | const wm = new WeakMap; 12 | const get = wm.get.bind(wm); 13 | const set = wm.set.bind(wm); 14 | 15 | const arrayProto = proto(Array); 16 | const join = callerOf(arrayProto.join); 17 | const map = callerOf(arrayProto.map); 18 | const push = callerOf(arrayProto.push); 19 | 20 | const stringProto = proto($String); 21 | const slice = callerOf(stringProto.slice); 22 | const substr = callerOf(stringProto.substr); 23 | 24 | const toString = callerOf(proto(Number).toString); 25 | 26 | const decode = callerOf(proto($TextDecoder).decode); 27 | const encode = callerOf(proto($TextEncoder).encode); 28 | 29 | const {freeze} = Object; 30 | 31 | const securedClass = (crypto, PATH, asHex, fromHex) => { 32 | const getRandomValues = crypto.getRandomValues.bind(crypto); 33 | 34 | const {subtle} = crypto; 35 | const decrypt = callerOf(subtle.decrypt); 36 | const encrypt = callerOf(subtle.encrypt); 37 | const deriveKey = callerOf(subtle.deriveKey); 38 | const importKey = callerOf(subtle.importKey); 39 | 40 | // big thanks to Webbjocke article for this bit of the Web Crypto API 41 | // https://webbjocke.com/javascript-web-encryption-and-hashing-with-the-crypto-api/ 42 | const createKey = async (password, salt) => { 43 | const name = 'PBKDF2'; 44 | const derived = {name: 'AES-GCM', length: 256}; 45 | const encoded = encode(new $TextEncoder, password); 46 | const key = await importKey(subtle, 'raw', encoded, {name}, false, ['deriveKey']); 47 | return deriveKey( 48 | subtle, 49 | { 50 | name, 51 | hash: 'SHA-256', 52 | salt: encode(new $TextEncoder, salt), 53 | iterations: 1000 54 | }, 55 | key, 56 | derived, 57 | false, 58 | ['encrypt', 'decrypt'] 59 | ); 60 | }; 61 | 62 | class Secretly { 63 | 64 | /** 65 | * Used as default salt, it returns `process.cwd()` in NodeJS, 66 | * and location up to the last path's `/` on the Web. 67 | * @returns {string} 68 | */ 69 | static get PATH() { return PATH; } 70 | 71 | /** 72 | * Initialize a Secretly instance. 73 | * @param {string} password a generic password. 74 | * @param {string?} salt a non-empty string to use as salt, it's Secretly.PATH by default. 75 | * @param {boolean?} random avoid iv's randomness to enable enc/decryption across sessions. 76 | */ 77 | constructor( 78 | password, 79 | salt = PATH, 80 | random = true 81 | ) { 82 | if (!password || !salt) { 83 | password = salt = ''; 84 | throw new TypeError(`invalid password or salt`); 85 | } 86 | const buffer = new $Uint8Array(16); 87 | set(this, { 88 | key: createKey($String(password), $String(salt)), 89 | info: { 90 | name: 'AES-GCM', 91 | length: 256, 92 | iv: random ? 93 | getRandomValues(buffer) : 94 | buffer 95 | } 96 | }); 97 | } 98 | 99 | /** 100 | * Decrypt a previously encrypted string. 101 | * @param {string} encrypted an encrypted string as hex. 102 | * @returns {Promise} 103 | */ 104 | async decrypt(encrypted) { 105 | const {key, info} = get(this); 106 | return decode( 107 | new $TextDecoder, 108 | await decrypt(subtle, info, await key, fromHex(encrypted)) 109 | ); 110 | } 111 | 112 | /** 113 | * Encrypt a generic string. 114 | * @param {string} decrypted a generic string to encrypt. 115 | * @returns {Promise} 116 | */ 117 | async encrypt(decrypted) { 118 | const {key, info} = get(this); 119 | const encrypted = encode(new $TextEncoder, decrypted); 120 | return asHex(await encrypt(subtle, info, await key, encrypted)); 121 | } 122 | } 123 | 124 | freeze(proto(Secretly)); 125 | 126 | return freeze(Secretly); 127 | }; 128 | 129 | export { 130 | $Uint8Array as Uint8Array, 131 | $parseInt as parseInt, 132 | callerOf, proto, securedClass, 133 | join, map, push, slice, substr, toString 134 | }; 135 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import {webcrypto} from 'crypto'; 2 | import {cwd} from 'process'; 3 | import {Crypto} from 'node-webcrypto-ossl'; 4 | 5 | import {callerOf, proto, securedClass} from './helpers.js'; 6 | 7 | const {from} = Buffer; 8 | const toString = callerOf(proto(Buffer).toString); 9 | 10 | export default securedClass( 11 | webcrypto || new Crypto, 12 | cwd(), 13 | buffer => toString(from(buffer), 'hex'), 14 | str => from(str, 'hex') 15 | ); 16 | -------------------------------------------------------------------------------- /esm/web.js: -------------------------------------------------------------------------------- 1 | import { 2 | Uint8Array, 3 | parseInt, 4 | securedClass, 5 | join, map, push, 6 | slice, substr, toString 7 | } from './helpers.js'; 8 | 9 | const {protocol, host, pathname} = location; 10 | 11 | const asHEX = i => slice('0' + toString(i, 16), -2); 12 | 13 | export default securedClass( 14 | self.crypto, 15 | protocol + '//' + host + pathname.replace(/\/[^/]*$/i, ''), 16 | buffer => join(map(new Uint8Array(buffer), asHEX), ''), 17 | str => { 18 | const bytes = []; 19 | for (let i = 0, {length} = str; i < length; i += 2) 20 | push(bytes, parseInt(substr(str, i, 2), 16)); 21 | return new Uint8Array(bytes); 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secretly", 3 | "version": "2.0.2", 4 | "description": "A basic class to encrypt and decrypt", 5 | "main": "./cjs/index.js", 6 | "module": "./esm/index.js", 7 | "type": "module", 8 | "exports": { 9 | ".": { 10 | "import": "./esm/index.js", 11 | "default": "./cjs/index.js" 12 | }, 13 | "./web": { 14 | "import": "./esm/web.js", 15 | "default": "./cjs/web.js" 16 | } 17 | }, 18 | "scripts": { 19 | "build": "npm run cjs && npm run test", 20 | "cjs": "ascjs --no-default esm cjs", 21 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 22 | "test": "nyc node test/index.js" 23 | }, 24 | "keywords": [ 25 | "encrypt", 26 | "decrypt", 27 | "simple", 28 | "utility", 29 | "Web" 30 | ], 31 | "author": "Andrea Giammarchi", 32 | "license": "ISC", 33 | "devDependencies": { 34 | "ascjs": "^4.0.3", 35 | "coveralls": "^3.1.0", 36 | "nyc": "^15.1.0" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/WebReflection/secretly.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/WebReflection/secretly/issues" 44 | }, 45 | "homepage": "https://github.com/WebReflection/secretly#readme", 46 | "dependencies": { 47 | "node-webcrypto-ossl": "^2.1.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./test.js')(require('../cjs')); 2 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async Secretly => { 3 | 4 | console.assert(Secretly.PATH.length, 'unexpected Secretly.PATH'); 5 | 6 | const safe = new Secretly('no-shenaigans'); 7 | 8 | const IN = `this 9 | is 10 | a test!`; 11 | 12 | const OUT = await safe.encrypt(IN); 13 | 14 | console.log(await new Secretly('pass', 'salt', false).encrypt(IN)); 15 | 16 | console.assert( 17 | await safe.decrypt(OUT) === IN, 18 | 'decrypted output is same as input' 19 | ); 20 | 21 | console.assert( 22 | await new Secretly('no-shenaigans').encrypt(IN) !== OUT, 23 | 'encrypted output is never the same' 24 | ); 25 | 26 | console.assert( 27 | OUT !== IN, 28 | 'encrypted output is different from the input' 29 | ); 30 | 31 | console.assert( 32 | await safe.decrypt(await safe.encrypt(IN)) === IN, 33 | 'decrypted text is identical from the input' 34 | ); 35 | 36 | try { 37 | const unsafe = new Secretly; 38 | process.exit(1); 39 | } catch(e) { 40 | console.assert(true, e.message); 41 | } 42 | 43 | const predictable = () => new Secretly('no-shenaigans', 'salt', false); 44 | 45 | console.assert( 46 | await predictable().encrypt(IN) === await predictable().encrypt(IN), 47 | 'encrypted output is always the same' 48 | ); 49 | 50 | console.assert( 51 | await predictable().decrypt(await predictable().encrypt(IN)) === IN, 52 | 'decrypted output still works' 53 | ); 54 | 55 | }; 56 | --------------------------------------------------------------------------------