├── .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 | [](https://travis-ci.com/WebReflection/secretly) [](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 |
--------------------------------------------------------------------------------