` and do your work.
305 | 5. Run `npm run build` to build dist files and `npm run test` to ensure all test cases are passing.
306 | 6. Commit your changes to the branch.
307 | 7. Submit a Pull request.
308 |
309 | ## Development Stack
310 |
311 | - Webpack based `src` compilation & bundling and `dist` generation.
312 | - ES6 as a source of writing code.
313 | - Exports in a [umd](https://github.com/umdjs/umd) format so the library works everywhere.
314 | - ES6 test setup with [Jest](https://jestjs.io/)
315 | - Linting with [ESLint](http://eslint.org/).
316 |
317 | ## Process
318 |
319 | ```
320 | ES6 source files
321 | |
322 | |
323 | webpack
324 | |
325 | +--- babel, eslint
326 | |
327 | ready to use
328 | library
329 | in umd format
330 | ```
331 |
332 | ## Credits
333 |
334 | Many thanks to:
335 |
336 | - [@brix](https://github.com/brix) for the awesome **[crypto-js](https://github.com/brix/crypto-js)** library for encrypting and decrypting data securely.
337 |
338 | - [@pieroxy](https://github.com/pieroxy) for the **[lz-string](https://github.com/pieroxy/lz-string)** js library for data compression / decompression.
339 |
340 | ## Copyright and license
341 |
342 | > The [MIT license](https://opensource.org/licenses/MIT) (MIT)
343 | >
344 | > Copyright (c) 2015-2024 Varun Malhotra
345 | >
346 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
347 | >
348 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
349 | >
350 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
351 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
3 | };
4 |
--------------------------------------------------------------------------------
/ci-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "secure-ls",
3 | "description": "Secure localStorage/sessionStorage data with high level of encryption and data compression",
4 | "main": "./dist/secure-ls.js",
5 | "browser": "./dist/secure-ls.js",
6 | "typings": "./types/secure-ls.d.ts",
7 | "scripts": {
8 | "build": "yarn build-dev && yarn build-prod",
9 | "build-dev": "webpack --mode=development",
10 | "build-prod": "webpack --mode=production",
11 | "build:patch": "yarn build-dev --env type=patch npm version patch",
12 | "build:minor": "yarn build-dev --env type=minor npm version minor",
13 | "build:major": "yarn build-dev --env type=major npm version major",
14 | "lint": "eslint src/*.js --fix",
15 | "prettier": "prettier -w **/*.js *.md",
16 | "test:dev": "jest --watch --runInBand --debug --colors --errorOnDeprecated",
17 | "test:prod": "jest --runInBand --colors --errorOnDeprecated",
18 | "test:coverage": "jest --coverage --coverageDirectory=coverage && cat ./coverage/lcov.info"
19 | },
20 | "dependencies": {
21 | "crypto-js": "^4.2.0",
22 | "lz-string": "^1.5.0"
23 | },
24 | "devDependencies": {
25 | "@babel/core": "^7.24.5",
26 | "@babel/preset-env": "^7.24.5",
27 | "babel-jest": "^25.x.x",
28 | "jest": "^25.x.x",
29 | "jest-environment-jsdom": "^25.x.x",
30 | "semver": "^7.6.2"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "https://github.com/softvar/secure-ls.git"
35 | },
36 | "keywords": ["secure-ls", "localStorage", "encryption", "compression", "webpack", "es6", "umd", "commonjs"],
37 | "author": "Varun Malhotra",
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/softvar/secure-ls/issues"
41 | },
42 | "homepage": "https://github.com/softvar/secure-ls",
43 | "engines": {
44 | "node": ">=8.0"
45 | },
46 | "engineStrict": true
47 | }
48 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | rules: {
4 | // TODO Add Scope Enum Here
5 | // 'scope-enum': [2, 'always', ['yourscope', 'yourscope']],
6 | 'type-enum': [
7 | 2,
8 | 'always',
9 | ['feat', 'fix', 'docs', 'chore', 'style', 'refactor', 'ci', 'test', 'revert', 'perf', 'vercel'],
10 | ],
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import pluginJs from "@eslint/js";
3 |
4 | export default [
5 | {languageOptions: { globals: globals.browser }},
6 | pluginJs.configs.recommended,
7 | {
8 | "rules": {
9 |
10 | }
11 | }
12 | ];
13 |
--------------------------------------------------------------------------------
/example/aes-compressed-realm.js:
--------------------------------------------------------------------------------
1 | var data = { data: [{ age: 1 }, { age: '2' }] };
2 | var aesCRealm1 = new SecureLS({
3 | encodingType: 'aes',
4 | encryptionSecret: 'secret1',
5 | encryptionNamespace: 'realm1',
6 | metaKey: '__meta__',
7 | });
8 | var key1 = 'aes__compressed_1';
9 | var ae = aesCRealm1.AES.encrypt(JSON.stringify(data), '');
10 | var bde = aesCRealm1.AES.decrypt(ae.toString(), '');
11 | var de = bde.toString(aesCRealm1.enc._Utf8);
12 | var aesCRealm2 = new SecureLS({ encodingType: 'aes', encryptionSecret: 'secret2', encryptionNamespace: 'realm2' });
13 | var key2 = 'aes__compressed_2';
14 | var ae2 = aesCRealm2.AES.encrypt(JSON.stringify(data), '');
15 | var bde2 = aesCRealm2.AES.decrypt(ae2.toString(), '');
16 | var de2 = bde2.toString(aesCRealm2.enc._Utf8);
17 |
18 | aesCRealm1.set(key1, data);
19 | console.log('AES Compressed Realm1');
20 | console.log(localStorage.getItem(key1));
21 | console.log(aesCRealm1.get(key1));
22 | console.log('____________________________________');
23 |
24 | aesCRealm2.set(key2, data);
25 | console.log('AES Compressed Realm2');
26 | console.log(localStorage.getItem(key2));
27 | console.log(aesCRealm2.get(key2));
28 | console.log('____________________________________');
29 |
--------------------------------------------------------------------------------
/example/aes-compressed.js:
--------------------------------------------------------------------------------
1 | var key = 'aes__compressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var aes_c = new SecureLS({ encodingType: 'aes', encryptionSecret: '' });
4 | ae = aes_c.AES.encrypt(JSON.stringify(data), '');
5 | bde = aes_c.AES.decrypt(ae.toString(), '');
6 | de = bde.toString(aes_c.enc._Utf8);
7 |
8 | aes_c.set(key, data);
9 | console.log('AES Compressed');
10 | console.log(localStorage.getItem(key));
11 | console.log(aes_c.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/aes-uncompressed.js:
--------------------------------------------------------------------------------
1 | var key = 'aes__uncompressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var aes_u = new SecureLS({ encodingType: 'aes', isCompression: false });
4 | ae = aes_u.AES.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = aes_u.AES.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(aes_u.enc._Utf8);
7 |
8 | aes_u.set(key, data);
9 | console.log('AES NOT Compressed');
10 | console.log(localStorage.getItem(key));
11 | console.log(aes_u.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/base64-compressed.js:
--------------------------------------------------------------------------------
1 | var key = 'base64__compressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var b_c = new SecureLS();
4 | ae = b_c.AES.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = b_c.AES.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(b_c.enc._Utf8);
7 |
8 | b_c.set(key, data);
9 | console.log('Base64 Compressed');
10 | console.log(localStorage.getItem(key));
11 | console.log(b_c.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/custom-storage.js:
--------------------------------------------------------------------------------
1 | window.customSecureLsStore = {};
2 | const storage = {
3 | setItem: (key, value) => {
4 | window.customSecureLsStore[key] = value || '';
5 | },
6 | getItem: (key) => {
7 | return window.customSecureLsStore[key] || null;
8 | },
9 | removeItem: (key) => {
10 | delete window.customSecureLsStore[key];
11 | },
12 | clear: () => {
13 | window.customSecureLsStore = {};
14 | },
15 | };
16 |
17 | var key = 'custom-storage';
18 | var data = { data: [{ age: 1 }, { age: '2' }] };
19 | var a = new SecureLS({ encodingType: '', isCompression: false, storage });
20 | ae = a.AES.encrypt(JSON.stringify(data), 's3cr3t@123');
21 | bde = a.AES.decrypt(ae.toString(), 's3cr3t@123');
22 | de = bde.toString(a.enc._Utf8);
23 |
24 | a.set(key, data);
25 | console.log('____________________________________');
26 | console.log('sessionStorage Case: no compression, no encryption / encoding, storage set to sessionStorage');
27 | console.log(sessionStorage.getItem(key));
28 | console.log(a.get(key));
29 | console.log('____________________________________');
30 |
--------------------------------------------------------------------------------
/example/des-compressed.js:
--------------------------------------------------------------------------------
1 | var key = 'des__compressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var des_c = new SecureLS({ encodingType: 'des' });
4 | ae = des_c.DES.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = des_c.DES.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(des_c.enc._Utf8);
7 |
8 | des_c.set(key, data);
9 | console.log('DES Compressed');
10 | console.log(localStorage.getItem(key));
11 | console.log(des_c.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/des-uncompressed.js:
--------------------------------------------------------------------------------
1 | var key = 'des__uncompressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var des_u = new SecureLS({ encodingType: 'des', isCompression: false });
4 | ae = des_u.DES.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = des_u.DES.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(des_u.enc._Utf8);
7 |
8 | des_u.set(key, data);
9 | console.log('DES not Compressed');
10 | console.log(localStorage.getItem(key));
11 | console.log(des_u.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Example Page
5 |
6 |
9 |
10 |
11 |
12 | Open console to check localStorage data.
13 |
14 | All of the console output from various examples will be log here. Scroll within it to view all logs.
15 |
16 | Input:
17 | {"data":[{"age":1},{"age":"2"}]}
18 | Output:
19 | {"data":[{"age":1},{"age":"2"}]}
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/example/only-base64.js:
--------------------------------------------------------------------------------
1 | var key = 'only__base64';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var o_b = new SecureLS({ isCompression: false });
4 | ae = o_b.AES.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = o_b.AES.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(o_b.enc._Utf8);
7 |
8 | o_b.set(key, data);
9 | console.log('Only Base64, no compression');
10 | console.log(localStorage.getItem(key));
11 | console.log(o_b.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/only-compressed.js:
--------------------------------------------------------------------------------
1 | var key = 'only__compressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var o_c = new SecureLS({ encodingType: '' });
4 | ae = o_c.AES.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = o_c.AES.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(o_c.enc._Utf8);
7 |
8 | o_c.set(key, data);
9 | console.log('Only Compression, no encoding/encryption');
10 | console.log(localStorage.getItem(key));
11 | console.log(o_c.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/rabbit-compressed.js:
--------------------------------------------------------------------------------
1 | var key = 'rabbit__compressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var rabbit_c = new SecureLS({ encodingType: 'rabbit' });
4 | ae = rabbit_c.RABBIT.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = rabbit_c.RABBIT.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(rabbit_c.enc._Utf8);
7 |
8 | rabbit_c.set(key, data);
9 | console.log('RABBIT Compressed');
10 | console.log(localStorage.getItem(key));
11 | console.log(rabbit_c.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/rabbit-uncompressed.js:
--------------------------------------------------------------------------------
1 | var key = 'rabbit__uncompressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var rabbit_u = new SecureLS({ encodingType: 'rabbit', isCompression: false });
4 | ae = rabbit_u.RABBIT.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = rabbit_u.RABBIT.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(rabbit_u.enc._Utf8);
7 |
8 | rabbit_u.set(key, data);
9 | console.log('RABBIT not Compressed');
10 | console.log(localStorage.getItem(key));
11 | console.log(rabbit_u.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/rc4-compressed.js:
--------------------------------------------------------------------------------
1 | var key = 'rc4__compressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var rc4_c = new SecureLS({ encodingType: 'rc4' });
4 | ae = rc4_c.RC4.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = rc4_c.RC4.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(rc4_c.enc._Utf8);
7 |
8 | rc4_c.set(key, data);
9 | console.log('RC4 Compressed');
10 | console.log(localStorage.getItem(key));
11 | console.log(rc4_c.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/rc4-uncompressed.js:
--------------------------------------------------------------------------------
1 | var key = 'rc4__uncompressed';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var rc4_u = new SecureLS({ encodingType: 'rc4', isCompression: false });
4 | ae = rc4_u.RC4.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = rc4_u.RC4.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(rc4_u.enc._Utf8);
7 |
8 | rc4_u.set(key, data);
9 | console.log('RC4 not Compressed');
10 | console.log(localStorage.getItem(key));
11 | console.log(rc4_u.get(key));
12 | console.log('____________________________________');
13 |
--------------------------------------------------------------------------------
/example/sessionStorage.js:
--------------------------------------------------------------------------------
1 | var key = 'sessionStorage';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var a = new SecureLS({ encodingType: '', isCompression: false, storage: sessionStorage });
4 | ae = a.AES.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = a.AES.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(a.enc._Utf8);
7 |
8 | a.set(key, data);
9 | console.log('____________________________________');
10 | console.log('sessionStorage Case: no compression, no encryption / encoding, storage set to sessionStorage');
11 | console.log(sessionStorage.getItem(key));
12 | console.log(a.get(key));
13 | console.log('____________________________________');
14 |
--------------------------------------------------------------------------------
/example/standard.js:
--------------------------------------------------------------------------------
1 | var key = 'standard';
2 | var data = { data: [{ age: 1 }, { age: '2' }] };
3 | var a = new SecureLS({ encodingType: '', isCompression: false });
4 | ae = a.AES.encrypt(JSON.stringify(data), 's3cr3t@123');
5 | bde = a.AES.decrypt(ae.toString(), 's3cr3t@123');
6 | de = bde.toString(a.enc._Utf8);
7 |
8 | a.set(key, data);
9 | console.log('____________________________________');
10 | console.log('Standard Case: no compression, no encryption / encoding');
11 | console.log(localStorage.getItem(key));
12 | console.log(a.get(key));
13 | console.log('____________________________________');
14 |
--------------------------------------------------------------------------------
/example/vendor/screenlog.min.js:
--------------------------------------------------------------------------------
1 | /*! screenlog - v0.2.2 - 2016-07-11
2 | * https://github.com/chinchang/screenlog.js
3 | * Copyright (c) 2016 Kushagra Gour; Licensed */
4 |
5 | !function(){function a(a,b){var c=document.createElement(a);return c.style.cssText=b,c}function b(){var b=a("div","z-index:2147483647;font-family:Helvetica,Arial,sans-serif;font-size:12px;font-weight:bold;padding:10px;text-align:left;opacity:0.8;position:fixed;min-width:400px;max-height:70vh;overflow:auto;background:"+_options.bgColor+";"+_options.css);return b}function c(b){return function(){var c=a("div","line-height:18px;min-height:18px;background:"+(o.children.length%2?"rgba(255,255,255,0.1)":"")+";color:"+b),d=[].slice.call(arguments).reduce(function(a,b){return a+" "+("object"==typeof b?JSON.stringify(b):b)},"");c.textContent=d,o.appendChild(c),_options.autoScroll&&(o.scrollTop=o.scrollHeight-o.clientHeight)}}function d(){o.innerHTML=""}function e(){return c(_options.logColor).apply(null,arguments)}function f(){return c(_options.infoColor).apply(null,arguments)}function g(){return c(_options.warnColor).apply(null,arguments)}function h(){return c(_options.errorColor).apply(null,arguments)}function i(a){for(var b in a)a.hasOwnProperty(b)&&_options.hasOwnProperty(b)&&(_options[b]=a[b])}function j(a){p||(p=!0,a&&i(a),o=b(),document.body.appendChild(o),_options.freeConsole||(q.log=console.log,q.clear=console.clear,q.info=console.info,q.warn=console.warn,q.error=console.error,console.log=n(e,"log"),console.clear=n(d,"clear"),console.info=n(f,"info"),console.warn=n(g,"warn"),console.error=n(h,"error")))}function k(){p=!1,console.log=q.log,console.clear=q.clear,console.info=q.info,console.warn=q.warn,console.error=q.error,o.remove()}function l(){if(!p)throw"You need to call `screenLog.init()` first."}function m(a){return function(){return l(),a.apply(this,arguments)}}function n(a,b){return function(){a.apply(this,arguments),"function"==typeof q[b]&&q[b].apply(console,arguments)}}var o,p=!1,q={};_options={bgColor:"black",logColor:"lightgreen",infoColor:"blue",warnColor:"orange",errorColor:"red",freeConsole:!1,css:"",autoScroll:!0},window.screenLog={init:j,log:n(m(e),"log"),clear:n(m(d),"clear"),info:n(m(d),"info"),warn:n(m(g),"warn"),error:n(m(h),"error"),destroy:m(k)}}();
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {
3 | '^.+\\.jsx?$': 'babel-jest',
4 | },
5 | // This is for handling extensions, if needed
6 | moduleFileExtensions: ['js', 'jsx', 'json', 'node', 'mjs'],
7 | // This might be required if you want to ignore some files
8 | transformIgnorePatterns: ['/node_modules/'],
9 | testEnvironment: 'jest-environment-jsdom',
10 | };
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "secure-ls",
3 | "version": "2.0.0",
4 | "description": "Secure localStorage/sessionStorage data with high level of encryption and data compression",
5 | "main": "./dist/secure-ls.js",
6 | "browser": "./dist/secure-ls.js",
7 | "typings": "./types/secure-ls.d.ts",
8 | "scripts": {
9 | "build": "yarn build-dev && yarn build-prod",
10 | "build-dev": "webpack --mode=development",
11 | "build-prod": "webpack --mode=production",
12 | "build:patch": "yarn build-dev --env type=patch npm version patch",
13 | "build:minor": "yarn build-dev --env type=minor npm version minor",
14 | "build:major": "yarn build-dev --env type=major npm version major",
15 | "lint": "eslint src/*.js --fix",
16 | "prepare": "husky",
17 | "prettier": "prettier -w **/*.js *.md",
18 | "test:dev": "jest --watch --runInBand --debug --colors --errorOnDeprecated",
19 | "test:prod": "jest --runInBand --colors --errorOnDeprecated",
20 | "test:coverage": "jest --coverage --coverageDirectory=coverage && cat ./coverage/lcov.info"
21 | },
22 | "lint-staged": {
23 | "**/*.{js,json,md}": [
24 | "prettier --write"
25 | ]
26 | },
27 | "dependencies": {
28 | "crypto-js": "^4.2.0",
29 | "lz-string": "^1.5.0"
30 | },
31 | "devDependencies": {
32 | "@babel/core": "^7.24.5",
33 | "@babel/preset-env": "^7.24.5",
34 | "@commitlint/cli": "^19.3.0",
35 | "@commitlint/config-conventional": "^19.2.2",
36 | "@eslint/js": "^9.2.0",
37 | "@types/crypto-js": "^4.2.2",
38 | "@types/lz-string": "^1.5.0",
39 | "babel-jest": "^29.7.0",
40 | "babel-loader": "^9.1.3",
41 | "eslint": "^9.2.0",
42 | "globals": "^15.4.0",
43 | "husky": "^9.0.11",
44 | "jest": "^29.7.0",
45 | "jest-environment-jsdom": "^29.7.0",
46 | "lint-staged": "^15.2.7",
47 | "prettier": "^3.3.2",
48 | "semver": "^7.6.2",
49 | "webpack": "^5.92.0",
50 | "webpack-cli": "^5.1.4"
51 | },
52 | "repository": {
53 | "type": "git",
54 | "url": "https://github.com/softvar/secure-ls.git"
55 | },
56 | "keywords": [
57 | "secure-ls",
58 | "localStorage",
59 | "encryption",
60 | "compression",
61 | "webpack",
62 | "es6",
63 | "umd",
64 | "commonjs"
65 | ],
66 | "author": "Varun Malhotra",
67 | "license": "MIT",
68 | "bugs": {
69 | "url": "https://github.com/softvar/secure-ls/issues"
70 | },
71 | "homepage": "https://github.com/softvar/secure-ls",
72 | "engines": {
73 | "node": ">=8.0"
74 | },
75 | "engineStrict": true
76 | }
77 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/softvar/secure-ls/d46344dc3f7910f454ddd3fc9fd9bc19886e6538/screenshot.png
--------------------------------------------------------------------------------
/src/Base64.js:
--------------------------------------------------------------------------------
1 | const Base64 = {
2 | _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
3 | encode: function (e) {
4 | let t = '';
5 | let n, r, i, s, o, u, a;
6 | let f = 0;
7 |
8 | e = Base64._utf8Encode(e);
9 | while (f < e.length) {
10 | n = e.charCodeAt(f++);
11 | r = e.charCodeAt(f++);
12 | i = e.charCodeAt(f++);
13 | s = n >> 2;
14 | o = ((n & 3) << 4) | (r >> 4);
15 | u = ((r & 15) << 2) | (i >> 6);
16 | a = i & 63;
17 | if (isNaN(r)) {
18 | u = a = 64;
19 | } else if (isNaN(i)) {
20 | a = 64;
21 | }
22 | t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a);
23 | }
24 | return t;
25 | },
26 | decode: function (e) {
27 | let t = '';
28 | let n, r, i;
29 | let s, o, u, a;
30 | let f = 0;
31 |
32 | e = e.replace(/[^A-Za-z0-9+/=]/g, '');
33 | while (f < e.length) {
34 | s = this._keyStr.indexOf(e.charAt(f++));
35 | o = this._keyStr.indexOf(e.charAt(f++));
36 | u = this._keyStr.indexOf(e.charAt(f++));
37 | a = this._keyStr.indexOf(e.charAt(f++));
38 | n = (s << 2) | (o >> 4);
39 | r = ((o & 15) << 4) | (u >> 2);
40 | i = ((u & 3) << 6) | a;
41 | t = t + String.fromCharCode(n);
42 | if (u !== 64) {
43 | t = t + String.fromCharCode(r);
44 | }
45 | if (a !== 64) {
46 | t = t + String.fromCharCode(i);
47 | }
48 | }
49 | t = Base64._utf8Decode(t);
50 | return t;
51 | },
52 | _utf8Encode: function (e) {
53 | e = e.replace(/\r\n/g, '\n');
54 | let t = '';
55 |
56 | for (let n = 0; n < e.length; n++) {
57 | let r = e.charCodeAt(n);
58 |
59 | if (r < 128) {
60 | t += String.fromCharCode(r);
61 | } else if (r > 127 && r < 2048) {
62 | t += String.fromCharCode((r >> 6) | 192);
63 | t += String.fromCharCode((r & 63) | 128);
64 | } else {
65 | t += String.fromCharCode((r >> 12) | 224);
66 | t += String.fromCharCode(((r >> 6) & 63) | 128);
67 | t += String.fromCharCode((r & 63) | 128);
68 | }
69 | }
70 | return t;
71 | },
72 | _utf8Decode: function (e) {
73 | let t = '';
74 | let n = 0;
75 | let r, c2, c3;
76 |
77 | r = c2 = 0;
78 | while (n < e.length) {
79 | r = e.charCodeAt(n);
80 | if (r < 128) {
81 | t += String.fromCharCode(r);
82 | n++;
83 | } else if (r > 191 && r < 224) {
84 | c2 = e.charCodeAt(n + 1);
85 | t += String.fromCharCode(((r & 31) << 6) | (c2 & 63));
86 | n += 2;
87 | } else {
88 | c2 = e.charCodeAt(n + 1);
89 | c3 = e.charCodeAt(n + 2);
90 | t += String.fromCharCode(((r & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
91 | n += 3;
92 | }
93 | }
94 | return t;
95 | },
96 | };
97 |
98 | export default Base64;
99 |
--------------------------------------------------------------------------------
/src/SecureLS.js:
--------------------------------------------------------------------------------
1 | import constants from './constants';
2 | import enc from './enc-utf8';
3 | import utils from './utils';
4 |
5 | import AES from 'crypto-js/aes';
6 | import RABBIT from 'crypto-js/rabbit';
7 | import RC4 from 'crypto-js/rc4';
8 | import DES from 'crypto-js/tripledes';
9 | import { compressToUTF16, decompressFromUTF16 } from 'lz-string/libs/lz-string';
10 | import Base64 from './Base64';
11 |
12 | const encryptors = {
13 | [constants.EncrytionTypes.AES]: AES,
14 | [constants.EncrytionTypes.DES]: DES,
15 | [constants.EncrytionTypes.RABBIT]: RABBIT,
16 | [constants.EncrytionTypes.RC4]: RC4,
17 | };
18 |
19 | export class SecureLS {
20 | constructor({
21 | encryptionSecret = '',
22 | encryptionNamespace = '',
23 | isCompression = true,
24 | encodingType = constants.EncrytionTypes.BASE64,
25 | storage = localStorage,
26 | metaKey = constants.metaKey,
27 | } = {}) {
28 | // Assign libraries and utilities
29 | Object.assign(this, {
30 | _name: 'secure-ls',
31 | Base64,
32 | LZString: { compressToUTF16, decompressFromUTF16 },
33 | AES,
34 | DES,
35 | RABBIT,
36 | RC4,
37 | enc,
38 | });
39 |
40 | // Configuration and property assignment
41 | this.config = {
42 | encryptionSecret,
43 | encryptionNamespace,
44 | isCompression,
45 | encodingType: encodingType.toLowerCase(),
46 | storage,
47 | metaKey,
48 | };
49 | this.encryptionSecret = encryptionSecret;
50 | this.storage = storage;
51 | this.metaKey = metaKey;
52 |
53 | // Initialize the class
54 | this.init();
55 | }
56 |
57 | init() {
58 | let metaData = this.getMetaData();
59 |
60 | this._isBase64 = this._isBase64EncryptionType();
61 | this._isAES = this._isAESEncryptionType();
62 | this._isDES = this._isDESEncryptionType();
63 | this._isRabbit = this._isRabbitEncryptionType();
64 | this._isRC4 = this._isRC4EncryptionType();
65 | this._isCompression = this._isDataCompressionEnabled();
66 |
67 | // fill the already present keys to the list of keys being used by secure-ls
68 | this.allKeys = metaData.keys || this.resetAllKeys();
69 | }
70 |
71 | _isBase64EncryptionType() {
72 | return (
73 | Base64 &&
74 | (typeof this.config.encodingType === 'undefined' || this.config.encodingType === constants.EncrytionTypes.BASE64)
75 | );
76 | }
77 |
78 | _isAESEncryptionType() {
79 | return AES && this.config.encodingType === constants.EncrytionTypes.AES;
80 | }
81 |
82 | _isDESEncryptionType() {
83 | return DES && this.config.encodingType === constants.EncrytionTypes.DES;
84 | }
85 |
86 | _isRabbitEncryptionType() {
87 | return RABBIT && this.config.encodingType === constants.EncrytionTypes.RABBIT;
88 | }
89 |
90 | _isRC4EncryptionType() {
91 | return RC4 && this.config.encodingType === constants.EncrytionTypes.RC4;
92 | }
93 |
94 | _isDataCompressionEnabled() {
95 | return this.config.isCompression;
96 | }
97 |
98 | getEncryptionSecret(key) {
99 | let metaData = this.getMetaData();
100 | let obj = utils.getObjectFromKey(metaData.keys, key);
101 |
102 | if (!obj) {
103 | return;
104 | }
105 |
106 | if (this._isAES || this._isDES || this._isRabbit || this._isRC4) {
107 | if (typeof this.config.encryptionSecret === 'undefined') {
108 | this.encryptionSecret = obj.s;
109 |
110 | if (!this.encryptionSecret) {
111 | this.encryptionSecret = utils.generateSecretKey();
112 | this.setMetaData();
113 | }
114 | } else {
115 | this.encryptionSecret = this.config.encryptionSecret || obj.s || '';
116 | }
117 | }
118 | }
119 |
120 | getEncryptionType() {
121 | const encodingType = this.config.encodingType;
122 | return encodingType ? encodingType.toLowerCase() : constants.EncrytionTypes.BASE64;
123 | }
124 |
125 | getDataFromLocalStorage(key) {
126 | return this.storage.getItem(key, true);
127 | }
128 |
129 | setDataToLocalStorage(key, data) {
130 | this.storage.setItem(key, data);
131 | }
132 |
133 | setMetaData() {
134 | let dataToStore = this.processData(
135 | {
136 | keys: this.allKeys,
137 | },
138 | true,
139 | );
140 |
141 | // Store the data to localStorage
142 | this.setDataToLocalStorage(this.getMetaKey(), dataToStore);
143 | }
144 |
145 | getMetaData() {
146 | return this.get(this.getMetaKey(), true) || {};
147 | }
148 |
149 | getMetaKey() {
150 | return this.metaKey + (this.config.encryptionNamespace ? '__' + this.config.encryptionNamespace : '');
151 | }
152 |
153 | resetAllKeys() {
154 | this.allKeys = [];
155 | return [];
156 | }
157 |
158 | processData(data, isAllKeysData) {
159 | if (data === null || data === undefined || data === '') {
160 | return '';
161 | }
162 |
163 | let jsonData;
164 |
165 | try {
166 | jsonData = JSON.stringify(data);
167 | } catch (err) {
168 | throw new Error('Could not stringify data', err);
169 | }
170 |
171 | // Encode Based on encoding type
172 | // If not set, default to Base64 for securing data
173 | let encodedData = jsonData;
174 |
175 | if (this._isBase64 || isAllKeysData) {
176 | encodedData = Base64.encode(jsonData);
177 | } else {
178 | const encryptor = encryptors[this.getEncryptionType()];
179 | if (encryptor) {
180 | encodedData = encryptor.encrypt(jsonData, this.encryptionSecret);
181 | }
182 |
183 | encodedData = encodedData && encodedData.toString();
184 | }
185 |
186 | // Compress data if set to true
187 | let compressedData = encodedData;
188 | if (this._isCompression || isAllKeysData) {
189 | compressedData = this.LZString.compressToUTF16(encodedData);
190 | }
191 |
192 | return compressedData;
193 | }
194 |
195 | // PUBLIC APIs
196 | getAllKeys() {
197 | let data = this.getMetaData();
198 |
199 | return utils.extractKeyNames(data) || [];
200 | }
201 |
202 | get(key, isAllKeysData) {
203 | let decodedData = '';
204 | let jsonData = '';
205 |
206 | if (!utils.is(key)) {
207 | utils.warn(constants.WarningEnum.KEY_NOT_PROVIDED);
208 | return jsonData;
209 | }
210 |
211 | let data = this.getDataFromLocalStorage(key);
212 |
213 | if (!data) {
214 | return jsonData;
215 | }
216 |
217 | let deCompressedData = data; // saves else
218 | if (this._isCompression || isAllKeysData) {
219 | // meta data always compressed
220 | deCompressedData = this.LZString.decompressFromUTF16(data);
221 | }
222 |
223 | decodedData = deCompressedData; // saves else
224 | if (this._isBase64 || isAllKeysData) {
225 | // meta data always Base64
226 | decodedData = Base64.decode(deCompressedData);
227 | } else {
228 | this.getEncryptionSecret(key);
229 | const encryptor = encryptors[this.getEncryptionType()];
230 |
231 | if (encryptor) {
232 | const bytes = encryptor.decrypt(deCompressedData.toString(), this.encryptionSecret);
233 |
234 | if (bytes) {
235 | decodedData = bytes.toString(enc._Utf8);
236 | }
237 | }
238 | }
239 |
240 | try {
241 | jsonData = JSON.parse(decodedData);
242 | } catch (err) {
243 | throw new Error('Could not parse JSON', err);
244 | }
245 |
246 | return jsonData;
247 | }
248 |
249 | set(key, data) {
250 | let dataToStore = '';
251 |
252 | if (!utils.is(key)) {
253 | utils.warn(constants.WarningEnum.KEY_NOT_PROVIDED);
254 | return;
255 | }
256 |
257 | this.getEncryptionSecret(key);
258 |
259 | // add key(s) to Array if not already added, only for keys other than meta key
260 | if (!(String(key) === String(this.metaKey))) {
261 | if (!utils.isKeyPresent(this.allKeys, key)) {
262 | this.allKeys.push({
263 | k: key,
264 | s: this.encryptionSecret,
265 | });
266 | this.setMetaData();
267 | }
268 | }
269 |
270 | dataToStore = this.processData(data);
271 | // Store the data to localStorage
272 | this.setDataToLocalStorage(key, dataToStore);
273 | }
274 |
275 | remove(key) {
276 | if (!utils.is(key)) {
277 | utils.warn(constants.WarningEnum.KEY_NOT_PROVIDED);
278 | return;
279 | }
280 |
281 | if (key === this.metaKey && this.getAllKeys().length) {
282 | utils.warn(constants.WarningEnum.META_KEY_REMOVE);
283 | return;
284 | }
285 |
286 | if (utils.isKeyPresent(this.allKeys, key)) {
287 | utils.removeFromKeysList(this.allKeys, key);
288 | this.setMetaData();
289 | }
290 | this.storage.removeItem(key);
291 | }
292 |
293 | removeAll() {
294 | let keys = this.getAllKeys();
295 |
296 | for (let i = 0; i < keys.length; i++) {
297 | this.storage.removeItem(keys[i]);
298 | }
299 |
300 | this.storage.removeItem(this.metaKey);
301 | this.resetAllKeys();
302 | }
303 |
304 | clear() {
305 | this.storage.clear();
306 | this.resetAllKeys();
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/src/WordArray.js:
--------------------------------------------------------------------------------
1 | /*
2 | ES6 compatible port of CryptoJS - WordArray for PBKDF2 password key generation
3 |
4 | Source: https://github.com/brix/crypto-js
5 | LICENSE: MIT
6 | */
7 |
8 | let CryptoJSWordArray = {
9 | random: function (nBytes) {
10 | let words = [];
11 | let r = function (mw) {
12 | let mz = 0x3ade68b1;
13 | let mask = 0xffffffff;
14 |
15 | return function () {
16 | mz = (0x9069 * (mz & 0xffff) + (mz >> 0x10)) & mask;
17 | mw = (0x4650 * (mw & 0xffff) + (mw >> 0x10)) & mask;
18 | let result = ((mz << 0x10) + mw) & mask;
19 |
20 | result /= 0x100000000;
21 | result += 0.5;
22 | return result * (Math.random() > 0.5 ? 1 : -1);
23 | };
24 | };
25 |
26 | for (let i = 0, rcache; i < nBytes; i += 4) {
27 | let _r = r((rcache || Math.random()) * 0x100000000);
28 |
29 | rcache = _r() * 0x3ade67b7;
30 | words.push((_r() * 0x100000000) | 0);
31 | }
32 |
33 | return new CryptoJSWordArray.Set(words, nBytes);
34 | },
35 |
36 | Set: function (words, sigBytes) {
37 | words = this.words = words || [];
38 |
39 | if (sigBytes !== undefined) {
40 | this.sigBytes = sigBytes;
41 | } else {
42 | this.sigBytes = words.length * 8;
43 | }
44 | },
45 | };
46 |
47 | export default CryptoJSWordArray;
48 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | const WarningEnum = {
2 | KEY_NOT_PROVIDED: 'keyNotProvided',
3 | META_KEY_REMOVE: 'metaKeyRemove',
4 | DEFAULT_TEXT: 'defaultText',
5 | };
6 |
7 | const WarningTypes = {};
8 |
9 | WarningTypes[WarningEnum.KEY_NOT_PROVIDED] = 'Secure LS: Key not provided. Aborting operation!';
10 | WarningTypes[WarningEnum.META_KEY_REMOVE] = `Secure LS: Meta key can not be removed
11 | unless all keys created by Secure LS are removed!`;
12 | WarningTypes[WarningEnum.DEFAULT_TEXT] = `Unexpected output`;
13 |
14 | const constants = {
15 | WarningEnum: WarningEnum,
16 | WarningTypes: WarningTypes,
17 | EncrytionTypes: {
18 | BASE64: 'base64',
19 | AES: 'aes',
20 | DES: 'des',
21 | RABBIT: 'rabbit',
22 | RC4: 'rc4',
23 | },
24 | metaKey: '_secure__ls__metadata',
25 | secretPhrase: 's3cr3t$#@135^&*246',
26 | };
27 |
28 | export default constants;
29 |
--------------------------------------------------------------------------------
/src/enc-utf8.js:
--------------------------------------------------------------------------------
1 | /*
2 | ES6 compatible port of CryptoJS - encoding
3 |
4 | Source: https://github.com/brix/crypto-js
5 | LICENSE: MIT
6 | */
7 | const enc = {
8 | Latin1: {
9 | stringify: (wordArray) => {
10 | // Shortcuts
11 | let words = wordArray.words;
12 | let sigBytes = wordArray.sigBytes;
13 | let latin1Chars = [],
14 | i,
15 | bite;
16 |
17 | // Convert
18 | for (i = 0; i < sigBytes; i++) {
19 | bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
20 | latin1Chars.push(String.fromCharCode(bite));
21 | }
22 |
23 | return latin1Chars.join('');
24 | },
25 | },
26 |
27 | _Utf8: {
28 | stringify: (wordArray) => {
29 | try {
30 | return decodeURIComponent(escape(enc.Latin1.stringify(wordArray)));
31 | } catch (err) {
32 | throw new Error('Malformed UTF-8 data', err);
33 | }
34 | },
35 | },
36 | };
37 |
38 | export default enc;
39 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { SecureLS } from './SecureLS';
2 |
3 | export default SecureLS;
4 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import PBKDF2 from 'crypto-js/pbkdf2';
2 | import constants from './constants';
3 | import CryptoJSWordArray from './WordArray';
4 |
5 | const utils = {
6 | is: (key) => !!key,
7 |
8 | warn: (reason = constants.WarningEnum.DEFAULT_TEXT) => {
9 | console.warn(constants.WarningTypes[reason]);
10 | },
11 |
12 | generateSecretKey: () => {
13 | const salt = CryptoJSWordArray.random(128 / 8);
14 | const key128Bits = PBKDF2(constants.secretPhrase, salt, { keySize: 128 / 32 });
15 | return key128Bits.toString();
16 | },
17 |
18 | getObjectFromKey: (data = [], key) => {
19 | return data.find((item) => item.k === key) || {};
20 | },
21 |
22 | extractKeyNames: ({ keys = [] } = {}) => {
23 | return keys.map(({ k }) => k);
24 | },
25 |
26 | isKeyPresent: (allKeys = [], key) => {
27 | return allKeys.some((item) => String(item.k) === String(key));
28 | },
29 |
30 | removeFromKeysList: (allKeys = [], key) => {
31 | const index = allKeys.findIndex((item) => item.k === key);
32 | if (index !== -1) {
33 | allKeys.splice(index, 1);
34 | }
35 | return index;
36 | },
37 | };
38 |
39 | export default utils;
40 |
--------------------------------------------------------------------------------
/test/basic.test.js:
--------------------------------------------------------------------------------
1 | import SecureLS from '../src/index';
2 |
3 | let lib;
4 |
5 | var localStorageMock = (function () {
6 | var store = {};
7 | return {
8 | getItem: function (key) {
9 | return store[key];
10 | },
11 | setItem: function (key, value) {
12 | store[key] = value.toString();
13 | },
14 | clear: function () {
15 | store = {};
16 | },
17 | removeItem: function (key) {
18 | delete store[key];
19 | },
20 | };
21 | })();
22 | Object.defineProperty(window, 'localStorage', { value: localStorageMock });
23 |
24 | describe('Basic suites', () => {
25 | beforeAll(() => {});
26 |
27 | describe('instance creation', () => {
28 | lib = new SecureLS();
29 |
30 | test('should check correct instance creation', () => {
31 | expect(lib).toBeInstanceOf(SecureLS);
32 | });
33 |
34 | test('should return the name', () => {
35 | expect(lib._name).toBe('secure-ls');
36 | });
37 | });
38 |
39 | describe('constructor', () => {
40 | lib = new SecureLS();
41 |
42 | test('should be called on instance creation', () => {
43 | expect(lib._name).toBeDefined();
44 | expect(lib.Base64).toBeDefined();
45 | expect(lib.LZString).toBeDefined();
46 | expect(lib.AES).toBeDefined();
47 | expect(lib.DES).toBeDefined();
48 | expect(lib.RABBIT).toBeDefined();
49 | expect(lib.RC4).toBeDefined();
50 | expect(lib.enc).toBeDefined();
51 | expect(lib.storage).toBeDefined();
52 | expect(lib.config).toBeDefined();
53 | expect(lib.config).toBeInstanceOf(Object);
54 | expect(lib.config).toHaveProperty('encodingType');
55 | expect(lib.config).toHaveProperty('isCompression');
56 | });
57 |
58 | test('should call init method', () => {
59 | const spy = jest.spyOn(lib, 'init');
60 |
61 | // mock as if new instance is created but actually not
62 | // Can't expect otherwise Object reference would be lost
63 | expect(spy).not.toHaveBeenCalled();
64 | lib.init();
65 | expect(spy).toHaveBeenCalled();
66 | spy.mockRestore(); // Reset spy after test
67 | });
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/test/functional.test.js:
--------------------------------------------------------------------------------
1 | import SecureLS from '../src/index';
2 | import utils from '../src/utils';
3 |
4 | let lib;
5 |
6 | describe('Functional tests', () => {
7 | beforeEach(() => {
8 | lib = new SecureLS();
9 | });
10 |
11 | afterEach(() => {
12 | lib.removeAll();
13 | jest.restoreAllMocks(); // Clear all mocks after each test
14 | });
15 |
16 | describe('Config test: is Base64 encoding', () => {
17 | it('should verify encryption type with data encryption', () => {
18 | lib = new SecureLS();
19 | expect(lib._isBase64EncryptionType()).toBe(true);
20 | expect(lib._isDataCompressionEnabled()).toBe(true);
21 | });
22 |
23 | it('should verify encryption type with no data compression', () => {
24 | lib = new SecureLS({ isCompression: false });
25 | expect(lib._isBase64EncryptionType()).toBe(true);
26 | expect(lib._isDataCompressionEnabled()).toBe(false);
27 | });
28 | });
29 |
30 | describe('Config test: is AES encryption', () => {
31 | it('should verify encryption type with data encryption', () => {
32 | lib = new SecureLS({ encodingType: 'aes' });
33 | expect(lib._isAESEncryptionType()).toBe(true);
34 | expect(lib._isDataCompressionEnabled()).toBe(true);
35 | });
36 |
37 | it('should verify encryption type with no data compression', () => {
38 | lib = new SecureLS({ encodingType: 'aes', isCompression: false });
39 | expect(lib._isAESEncryptionType()).toBe(true);
40 | expect(lib._isDataCompressionEnabled()).toBe(false);
41 | });
42 | });
43 |
44 | describe('Config test: is DES encryption', () => {
45 | it('should verify encryption type with data encryption', () => {
46 | lib = new SecureLS({ encodingType: 'des' });
47 | expect(lib._isDESEncryptionType()).toBe(true);
48 | expect(lib._isDataCompressionEnabled()).toBe(true);
49 | });
50 |
51 | it('should verify encryption type with no data compression', () => {
52 | lib = new SecureLS({ encodingType: 'des', isCompression: false });
53 | expect(lib._isDESEncryptionType()).toBe(true);
54 | expect(lib._isDataCompressionEnabled()).toBe(false);
55 | });
56 | });
57 |
58 | describe('Config test: is RABBIT encryption', () => {
59 | it('should verify encryption type with data encryption', () => {
60 | lib = new SecureLS({ encodingType: 'rabbit' });
61 | expect(lib._isRabbitEncryptionType()).toBe(true);
62 | expect(lib._isDataCompressionEnabled()).toBe(true);
63 | });
64 |
65 | it('should verify encryption type with no data compression', () => {
66 | lib = new SecureLS({ encodingType: 'rabbit', isCompression: false });
67 | expect(lib._isRabbitEncryptionType()).toBe(true);
68 | expect(lib._isDataCompressionEnabled()).toBe(false);
69 | });
70 | });
71 |
72 | describe('Config test: is RC4 encryption', () => {
73 | it('should verify encryption type with data encryption', () => {
74 | lib = new SecureLS({ encodingType: 'rc4' });
75 | expect(lib._isRC4EncryptionType()).toBe(true);
76 | expect(lib._isDataCompressionEnabled()).toBe(true);
77 | });
78 |
79 | it('should verify encryption type with no data compression', () => {
80 | lib = new SecureLS({ encodingType: 'rc4', isCompression: false });
81 | expect(lib._isRC4EncryptionType()).toBe(true);
82 | expect(lib._isDataCompressionEnabled()).toBe(false);
83 | });
84 | });
85 |
86 | describe('processData: method', () => {
87 | it('should return if no data provided', () => {
88 | const spyOnLZStringCompress = jest.spyOn(lib.LZString, 'compressToUTF16').mockImplementation(() => {});
89 |
90 | lib.processData();
91 | expect(spyOnLZStringCompress).not.toHaveBeenCalled();
92 |
93 | spyOnLZStringCompress.mockRestore();
94 | });
95 |
96 | it('should call AES encrypt if encoding is AES', () => {
97 | lib = new SecureLS({ encodingType: 'aes' });
98 |
99 | const spyOnLZStringCompress = jest.spyOn(lib.LZString, 'compressToUTF16').mockImplementation(() => {});
100 | const spyOnAESEncrypt = jest.spyOn(lib.AES, 'encrypt').mockImplementation(() => {});
101 | const spyOnRABBITEncrypt = jest.spyOn(lib.RABBIT, 'encrypt').mockImplementation(() => {});
102 | const data = {
103 | username: 'softvar',
104 | module: 'secure-ls',
105 | stars: 10000,
106 | };
107 |
108 | lib.encryptionSecret = utils.generateSecretKey();
109 | lib.processData(data);
110 | expect(spyOnLZStringCompress).toHaveBeenCalled();
111 | expect(spyOnAESEncrypt).toHaveBeenCalled();
112 | expect(spyOnRABBITEncrypt).not.toHaveBeenCalled();
113 |
114 | spyOnLZStringCompress.mockRestore();
115 | spyOnAESEncrypt.mockRestore();
116 | spyOnRABBITEncrypt.mockRestore();
117 | });
118 |
119 | it('should call DES encrypt if encoding is DES', () => {
120 | lib = new SecureLS({ encodingType: 'DES' });
121 |
122 | const spyOnLZStringCompress = jest.spyOn(lib.LZString, 'compressToUTF16').mockImplementation(() => {});
123 | const spyOnDESEncrypt = jest.spyOn(lib.DES, 'encrypt').mockImplementation(() => {});
124 | const spyOnRABBITEncrypt = jest.spyOn(lib.RABBIT, 'encrypt').mockImplementation(() => {});
125 | const data = {
126 | username: 'softvar',
127 | module: 'secure-ls',
128 | stars: 10000,
129 | };
130 |
131 | lib.encryptionSecret = utils.generateSecretKey();
132 | lib.processData(data);
133 | expect(spyOnLZStringCompress).toHaveBeenCalled();
134 | expect(spyOnDESEncrypt).toHaveBeenCalled();
135 | expect(spyOnRABBITEncrypt).not.toHaveBeenCalled();
136 |
137 | spyOnLZStringCompress.mockRestore();
138 | spyOnDESEncrypt.mockRestore();
139 | spyOnRABBITEncrypt.mockRestore();
140 | });
141 |
142 | it('should call RABBIT encrypt if encoding is RABBIT', () => {
143 | lib = new SecureLS({ encodingType: 'RABBIT' });
144 |
145 | const spyOnLZStringCompress = jest.spyOn(lib.LZString, 'compressToUTF16').mockImplementation(() => {});
146 | const spyOnRABBITEncrypt = jest.spyOn(lib.RABBIT, 'encrypt').mockImplementation(() => {});
147 | const spyOnAESEncrypt = jest.spyOn(lib.AES, 'encrypt').mockImplementation(() => {});
148 | const data = {
149 | username: 'softvar',
150 | module: 'secure-ls',
151 | stars: 10000,
152 | };
153 |
154 | lib.encryptionSecret = utils.generateSecretKey();
155 | lib.processData(data);
156 | expect(spyOnLZStringCompress).toHaveBeenCalled();
157 | expect(spyOnRABBITEncrypt).toHaveBeenCalled();
158 | expect(spyOnAESEncrypt).not.toHaveBeenCalled();
159 |
160 | spyOnLZStringCompress.mockRestore();
161 | spyOnRABBITEncrypt.mockRestore();
162 | spyOnAESEncrypt.mockRestore();
163 | });
164 |
165 | it('should call RC4 encrypt if encoding is RC4', () => {
166 | lib = new SecureLS({ encodingType: 'RC4' });
167 |
168 | const spyOnLZStringCompress = jest.spyOn(lib.LZString, 'compressToUTF16').mockImplementation(() => {});
169 | const spyOnRC4Encrypt = jest.spyOn(lib.RC4, 'encrypt').mockImplementation(() => {});
170 | const spyOnRABBITEncrypt = jest.spyOn(lib.RABBIT, 'encrypt').mockImplementation(() => {});
171 | const data = {
172 | username: 'softvar',
173 | module: 'secure-ls',
174 | stars: 10000,
175 | };
176 |
177 | lib.encryptionSecret = utils.generateSecretKey();
178 | lib.processData(data);
179 | expect(spyOnLZStringCompress).toHaveBeenCalled();
180 | expect(spyOnRC4Encrypt).toHaveBeenCalled();
181 | expect(spyOnRABBITEncrypt).not.toHaveBeenCalled();
182 |
183 | spyOnLZStringCompress.mockRestore();
184 | spyOnRC4Encrypt.mockRestore();
185 | spyOnRABBITEncrypt.mockRestore();
186 | });
187 |
188 | it('should not call LZString compress if compression OFF', () => {
189 | lib = new SecureLS({ encodingType: 'aes', isCompression: false });
190 |
191 | const spyOnLZStringCompress = jest.spyOn(lib.LZString, 'compressToUTF16').mockImplementation(() => {});
192 | const data = {
193 | username: 'softvar',
194 | module: 'secure-ls',
195 | stars: 10000,
196 | };
197 |
198 | lib.encryptionSecret = utils.generateSecretKey();
199 | lib.processData(data);
200 | expect(spyOnLZStringCompress).not.toHaveBeenCalled();
201 |
202 | spyOnLZStringCompress.mockRestore();
203 | });
204 |
205 | it('should call LZString compress if compression in ON', () => {
206 | lib = new SecureLS({ encodingType: 'aes', isCompression: true });
207 |
208 | const spyOnLZStringCompress = jest.spyOn(lib.LZString, 'compressToUTF16').mockImplementation(() => {});
209 | const data = {
210 | username: 'softvar',
211 | module: 'secure-ls',
212 | stars: 10000,
213 | };
214 |
215 | lib.encryptionSecret = utils.generateSecretKey();
216 | lib.processData(data);
217 | expect(spyOnLZStringCompress).toHaveBeenCalled();
218 |
219 | spyOnLZStringCompress.mockRestore();
220 | });
221 | });
222 | });
223 |
--------------------------------------------------------------------------------
/test/localStorage.test.js:
--------------------------------------------------------------------------------
1 | import SecureLS from '../src/index';
2 | import mockLS from './mock/ls';
3 |
4 | let lib;
5 | let mockStorage;
6 |
7 | describe('LocalStorage API Tests ->', () => {
8 | beforeEach(() => {
9 | mockStorage = mockLS.storageMock();
10 |
11 | lib = new SecureLS();
12 | lib.storage = mockStorage;
13 | });
14 |
15 | afterEach(() => {
16 | lib.removeAll();
17 | });
18 |
19 | describe('setItem method', () => {
20 | it('should set the value on key', () => {
21 | const data = [1, 2, 3];
22 | const key = 'key-1';
23 |
24 | lib.set(key, data);
25 |
26 | expect(mockStorage.storage[key]).toBeDefined();
27 | expect(typeof mockStorage.storage[key]).toBe('string');
28 | });
29 | });
30 |
31 | describe('getItem method', () => {
32 | it('should return the value stored', () => {
33 | const data = [1, 2, 3];
34 | const key = 'key-1';
35 |
36 | lib.set(key, data);
37 |
38 | expect(mockStorage.storage[key]).toBeDefined();
39 | expect(typeof mockStorage.storage[key]).toBe('string');
40 |
41 | const value = lib.get(key);
42 |
43 | expect(Array.isArray(value)).toBe(true);
44 | expect(value.length).toBe(3);
45 | expect(value.toString()).toBe(data.toString());
46 | });
47 | });
48 |
49 | describe('removeItem method', () => {
50 | it('should remove the key-value, if stored', () => {
51 | const data = [1, 2, 3];
52 | const key1 = 'key-1';
53 | const key2 = 'key-2';
54 |
55 | lib.set(key1, data);
56 | lib.set(key2, data);
57 |
58 | lib.remove(key1);
59 | let value1 = lib.get(key1);
60 | expect(mockStorage.storage[key1]).toBeUndefined();
61 | expect(Array.isArray(value1)).toBe(false);
62 |
63 | let value2 = lib.get(key2);
64 | expect(mockStorage.storage[key2]).toBeDefined();
65 | expect(Array.isArray(value2)).toBe(true);
66 |
67 | lib.remove(key2);
68 | value1 = lib.get(key1);
69 | expect(mockStorage.storage[key1]).toBeUndefined();
70 | expect(Array.isArray(value1)).toBe(false);
71 |
72 | value2 = lib.get(key2);
73 | expect(mockStorage.storage[key2]).toBeUndefined();
74 | expect(Array.isArray(value2)).toBe(false);
75 | });
76 | });
77 |
78 | describe('setItem, getItem and removeItem in one go', () => {
79 | it('should set, get and remove', () => {
80 | const data = [1, 2, 3];
81 | const key1 = 'key-1';
82 | const key2 = 'key-2';
83 |
84 | lib.set(key1, data);
85 | expect(mockStorage.storage[key1]).toBeDefined();
86 | expect(typeof mockStorage.storage[key1]).toBe('string');
87 |
88 | lib.set(key2, data);
89 | expect(mockStorage.storage[key2]).toBeDefined();
90 | expect(typeof mockStorage.storage[key2]).toBe('string');
91 |
92 | let value1 = lib.get(key1);
93 | expect(Array.isArray(value1)).toBe(true);
94 | expect(value1.length).toBe(3);
95 | expect(value1.toString()).toBe(data.toString());
96 |
97 | let value2 = lib.get(key2);
98 | expect(Array.isArray(value2)).toBe(true);
99 | expect(value2.length).toBe(3);
100 | expect(value2.toString()).toBe(data.toString());
101 |
102 | lib.remove(key1);
103 | value1 = lib.get(key1);
104 | expect(mockStorage.storage[key1]).toBeUndefined();
105 | expect(Array.isArray(value1)).toBe(false);
106 |
107 | value2 = lib.get(key2);
108 | expect(Array.isArray(value2)).toBe(true);
109 | expect(value2.length).toBe(3);
110 | expect(value2.toString()).toBe(data.toString());
111 |
112 | lib.remove(key2);
113 | value1 = lib.get(key1);
114 | expect(mockStorage.storage[key1]).toBeUndefined();
115 | expect(Array.isArray(value1)).toBe(false);
116 |
117 | value2 = lib.get(key2);
118 | expect(mockStorage.storage[key2]).toBeUndefined();
119 | expect(Array.isArray(value2)).toBe(false);
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/test/ls-data-compression.test.js:
--------------------------------------------------------------------------------
1 | import SecureLS from '../src/index';
2 | import mockLS from './mock/ls';
3 |
4 | let lib;
5 |
6 | describe('Data Compression Tests', () => {
7 | let mockStorage = mockLS.storageMock();
8 |
9 | beforeEach(() => {
10 | mockStorage = mockLS.storageMock();
11 | lib = new SecureLS();
12 | lib.storage = mockStorage;
13 | });
14 |
15 | afterEach(() => {
16 | lib.removeAll();
17 | jest.restoreAllMocks(); // Clear all mocks after each test
18 | });
19 |
20 | describe('no data compression but Base64 encoded', () => {
21 | test('should not compress data before storing to localStorage', () => {
22 | let valueStored;
23 | let data = [1, 2, 3];
24 | let key = 'key-1';
25 |
26 | lib = new SecureLS({ isCompression: false });
27 | lib.storage = mockStorage;
28 | lib.set(key, data);
29 |
30 | // corresponding to [1, 2, 3] => WzEsMiwzXQ== i.e. Base64 encoded
31 | valueStored = lib.LZString.compressToUTF16(lib.Base64.encode(JSON.stringify(data)));
32 |
33 | expect(mockStorage.storage[key]).toBeDefined();
34 | expect(typeof mockStorage.storage[key]).toBe('string');
35 | // important
36 | expect(mockStorage.storage[key]).not.toEqual(valueStored);
37 | });
38 | });
39 |
40 | describe('no data compression and no encoding', () => {
41 | test('should not compress data before storing to localStorage', () => {
42 | let valueStored;
43 | let data = [1, 2, 3];
44 | let key = 'key-1';
45 |
46 | lib = new SecureLS({ encodingType: '', isCompression: false });
47 | lib.storage = mockStorage;
48 | lib.set(key, data);
49 |
50 | // corresponding to [1, 2, 3] => "[1, 2, 3]"
51 | valueStored = JSON.stringify(data);
52 |
53 | expect(mockStorage.storage[key]).toBeDefined();
54 | expect(typeof mockStorage.storage[key]).toBe('string');
55 | // important
56 | expect(mockStorage.storage[key]).toEqual(valueStored);
57 | });
58 | });
59 |
60 | describe('data compression', () => {
61 | test('should compress data before storing to localStorage', () => {
62 | let valueStored;
63 | let data = [1, 2, 3];
64 | let key = 'key-1';
65 |
66 | lib.set(key, data);
67 |
68 | // corresponding to [1, 2, 3] => 㪂ೠ눉惮 脔ொꀀ
69 | valueStored = lib.LZString.compressToUTF16(lib.Base64.encode(JSON.stringify(data)));
70 |
71 | expect(mockStorage.storage[key]).toBeDefined();
72 | expect(typeof mockStorage.storage[key]).toBe('string');
73 | // important
74 | expect(mockStorage.storage[key]).toEqual(valueStored);
75 | });
76 |
77 | test('should compress data before storing to localStorage', () => {
78 | let valueStored;
79 | let data = {
80 | username: 'softvar',
81 | contact: 1234567890,
82 | hobbies: ['x', 'y', 'z'],
83 | };
84 | let key = 'key-1';
85 |
86 | lib.set(key, data);
87 |
88 | // corresponding to [1, 2, 3] => ⦄ࣀ옄쁪‑腬ؠᜁ栙䂒ͥ쀻äʹ좀鑠ፀ൜Ұـ愰ʴ䘁堀斠ᵄ뽜鰃�ଠ՚䰀ι〈怜䀧ፚ저�舀郰Y馮ހ㎱्蠀
89 | valueStored = lib.LZString.compressToUTF16(lib.Base64.encode(JSON.stringify(data)));
90 |
91 | expect(mockStorage.storage[key]).toBeDefined();
92 | expect(typeof mockStorage.storage[key]).toBe('string');
93 | // important
94 | expect(mockStorage.storage[key]).toEqual(valueStored);
95 | });
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/test/ls-data-enc-dec.test.js:
--------------------------------------------------------------------------------
1 | import SecureLS from '../src/index';
2 | import mockLS from './mock/ls';
3 | import enc from '../src/enc-utf8';
4 |
5 | let mockStorage;
6 | let lib;
7 | let Base64, AES, DES, RABBIT, RC4, LZString;
8 |
9 | describe('Encryption / Decryption Tests', () => {
10 | beforeEach(() => {
11 | mockStorage = mockLS.storageMock();
12 | lib = new SecureLS();
13 | lib.storage = mockStorage;
14 |
15 | ({ Base64, AES, DES, RABBIT, RC4, LZString } = lib);
16 | });
17 |
18 | afterEach(() => {
19 | lib.removeAll();
20 | });
21 |
22 | describe('Base64 encoded and no data compression', () => {
23 | it('should Base64 encode data before storing to localStorage', () => {
24 | let valueStored;
25 | let data = [1, 2, 3];
26 | let key = 'key-1';
27 |
28 | lib = new SecureLS({ isCompression: false });
29 | lib.storage = mockStorage;
30 | lib.set(key, data);
31 |
32 | valueStored = Base64.encode(JSON.stringify(data));
33 |
34 | expect(mockStorage.storage[key]).toBeDefined();
35 | expect(typeof mockStorage.storage[key]).toBe('string');
36 | expect(mockStorage.storage[key]).toEqual(valueStored);
37 | });
38 | });
39 |
40 | describe('Base64 encoded and data compression', () => {
41 | it('should Base64 encode data before storing to localStorage', () => {
42 | let valueStored;
43 | let data = [1, 2, 3];
44 | let key = 'key-1';
45 |
46 | lib = new SecureLS();
47 | lib.storage = mockStorage;
48 | lib.set(key, data);
49 |
50 | valueStored = LZString.compressToUTF16(Base64.encode(JSON.stringify(data)));
51 |
52 | expect(mockStorage.storage[key]).toBeDefined();
53 | expect(typeof mockStorage.storage[key]).toBe('string');
54 | expect(mockStorage.storage[key]).toEqual(valueStored);
55 | });
56 | });
57 |
58 | describe('AES encryption and no data compression', () => {
59 | it('should encrypt data with AES before storing to localStorage', () => {
60 | let valueStored, valueRetrieved;
61 | let data = [1, 2, 3];
62 | let key = 'key-1';
63 |
64 | lib = new SecureLS({ encodingType: 'aes', isCompression: false });
65 | lib.storage = mockStorage;
66 | lib.set(key, data);
67 |
68 | valueStored = AES.encrypt(JSON.stringify(data), lib.encryptionSecret).toString();
69 |
70 | expect(mockStorage.storage[key]).toBeDefined();
71 | expect(typeof mockStorage.storage[key]).toBe('string');
72 |
73 | valueRetrieved = JSON.parse(AES.decrypt(valueStored, lib.encryptionSecret).toString(enc._Utf8));
74 | expect(data.toString()).toEqual(valueRetrieved.toString());
75 | });
76 | });
77 |
78 | describe('AES encryption and data compression', () => {
79 | it('should encrypt data with AES before storing to localStorage', () => {
80 | let valueStored, valueRetrieved;
81 | let data = [1, 2, 3];
82 | let key = 'key-1';
83 |
84 | lib = new SecureLS({ encodingType: 'aes', isCompression: true });
85 | lib.storage = mockStorage;
86 | lib.set(key, data);
87 |
88 | valueStored = LZString.compressToUTF16(AES.encrypt(JSON.stringify(data), lib.encryptionSecret).toString());
89 |
90 | expect(mockStorage.storage[key]).toBeDefined();
91 | expect(typeof mockStorage.storage[key]).toBe('string');
92 |
93 | valueRetrieved = JSON.parse(
94 | AES.decrypt(LZString.decompressFromUTF16(valueStored), lib.encryptionSecret).toString(enc._Utf8),
95 | );
96 | expect(data.toString()).toEqual(valueRetrieved.toString());
97 | });
98 | });
99 |
100 | describe('AES encryption, data compression, and custom secret key', () => {
101 | it('should encrypt data with AES with custom key before storing to localStorage', () => {
102 | let valueStored, valueRetrieved;
103 | let data = [1, 2, 3];
104 | let key = 'key-1';
105 |
106 | lib = new SecureLS({
107 | encodingType: 'aes',
108 | isCompression: true,
109 | encryptionSecret: 'mySecretKey123',
110 | });
111 | lib.storage = mockStorage;
112 | lib.set(key, data);
113 |
114 | expect(lib.config.encryptionSecret).toEqual('mySecretKey123');
115 | expect(lib.encryptionSecret).toEqual('mySecretKey123');
116 |
117 | valueStored = LZString.compressToUTF16(AES.encrypt(JSON.stringify(data), lib.encryptionSecret).toString());
118 |
119 | expect(mockStorage.storage[key]).toBeDefined();
120 | expect(typeof mockStorage.storage[key]).toBe('string');
121 |
122 | valueRetrieved = JSON.parse(
123 | AES.decrypt(LZString.decompressFromUTF16(valueStored), lib.encryptionSecret).toString(enc._Utf8),
124 | );
125 | expect(data.toString()).toEqual(valueRetrieved.toString());
126 | });
127 | });
128 |
129 | describe('DES encryption and no data compression', () => {
130 | it('should encrypt data with DES before storing to localStorage', () => {
131 | let valueStored, valueRetrieved;
132 | let data = [1, 2, 3];
133 | let key = 'key-1';
134 |
135 | lib = new SecureLS({ encodingType: 'DES', isCompression: false });
136 | lib.storage = mockStorage;
137 | lib.set(key, data);
138 |
139 | valueStored = DES.encrypt(JSON.stringify(data), lib.encryptionSecret).toString();
140 |
141 | expect(mockStorage.storage[key]).toBeDefined();
142 | expect(typeof mockStorage.storage[key]).toBe('string');
143 |
144 | valueRetrieved = JSON.parse(DES.decrypt(valueStored, lib.encryptionSecret).toString(enc._Utf8));
145 | expect(data.toString()).toEqual(valueRetrieved.toString());
146 | });
147 | });
148 |
149 | describe('DES encryption, no data compression, and custom secret key', () => {
150 | it('should encrypt data with DES before storing to localStorage', () => {
151 | let valueStored, valueRetrieved;
152 | let data = [1, 2, 3];
153 | let key = 'key-1';
154 |
155 | lib = new SecureLS({
156 | encodingType: 'DES',
157 | isCompression: false,
158 | encryptionSecret: 'mySecretKey123',
159 | });
160 | lib.storage = mockStorage;
161 | lib.set(key, data);
162 |
163 | expect(lib.config.encryptionSecret).toEqual('mySecretKey123');
164 | expect(lib.encryptionSecret).toEqual('mySecretKey123');
165 |
166 | valueStored = DES.encrypt(JSON.stringify(data), lib.encryptionSecret).toString();
167 |
168 | expect(mockStorage.storage[key]).toBeDefined();
169 | expect(typeof mockStorage.storage[key]).toBe('string');
170 |
171 | valueRetrieved = JSON.parse(DES.decrypt(valueStored, lib.encryptionSecret).toString(enc._Utf8));
172 | expect(data.toString()).toEqual(valueRetrieved.toString());
173 | });
174 | });
175 |
176 | describe('DES encryption and data compression', () => {
177 | it('should encrypt data with DES before storing to localStorage', () => {
178 | let valueStored, valueRetrieved;
179 | let data = [1, 2, 3];
180 | let key = 'key-1';
181 |
182 | lib = new SecureLS({ encodingType: 'DES', isCompression: true });
183 | lib.storage = mockStorage;
184 | lib.set(key, data);
185 |
186 | valueStored = LZString.compressToUTF16(DES.encrypt(JSON.stringify(data), lib.encryptionSecret).toString());
187 |
188 | expect(mockStorage.storage[key]).toBeDefined();
189 | expect(typeof mockStorage.storage[key]).toBe('string');
190 |
191 | valueRetrieved = JSON.parse(
192 | DES.decrypt(LZString.decompressFromUTF16(valueStored), lib.encryptionSecret).toString(enc._Utf8),
193 | );
194 | expect(data.toString()).toEqual(valueRetrieved.toString());
195 | });
196 | });
197 |
198 | describe('RABBIT encryption and no data compression', () => {
199 | it('should encrypt data with RABBIT before storing to localStorage', () => {
200 | let valueStored, valueRetrieved;
201 | let data = [1, 2, 3];
202 | let key = 'key-1';
203 |
204 | lib = new SecureLS({ encodingType: 'RABBIT', isCompression: false });
205 | lib.storage = mockStorage;
206 | lib.set(key, data);
207 |
208 | valueStored = RABBIT.encrypt(JSON.stringify(data), lib.encryptionSecret).toString();
209 |
210 | expect(mockStorage.storage[key]).toBeDefined();
211 | expect(typeof mockStorage.storage[key]).toBe('string');
212 |
213 | valueRetrieved = JSON.parse(RABBIT.decrypt(valueStored, lib.encryptionSecret).toString(enc._Utf8));
214 | expect(data.toString()).toEqual(valueRetrieved.toString());
215 | });
216 | });
217 |
218 | describe('RABBIT encryption and data compression', () => {
219 | it('should encrypt data with RABBIT before storing to localStorage', () => {
220 | let valueStored, valueRetrieved;
221 | let data = [1, 2, 3];
222 | let key = 'key-1';
223 |
224 | lib = new SecureLS({ encodingType: 'RABBIT', isCompression: true });
225 | lib.storage = mockStorage;
226 | lib.set(key, data);
227 |
228 | valueStored = LZString.compressToUTF16(RABBIT.encrypt(JSON.stringify(data), lib.encryptionSecret).toString());
229 |
230 | expect(mockStorage.storage[key]).toBeDefined();
231 | expect(typeof mockStorage.storage[key]).toBe('string');
232 |
233 | valueRetrieved = JSON.parse(
234 | RABBIT.decrypt(LZString.decompressFromUTF16(valueStored), lib.encryptionSecret).toString(enc._Utf8),
235 | );
236 | expect(data.toString()).toEqual(valueRetrieved.toString());
237 | });
238 | });
239 |
240 | describe('RABBIT encryption and no data compression', () => {
241 | it('should encrypt data with RABBIT before storing to localStorage', () => {
242 | let lib;
243 | const data = [1, 2, 3];
244 | const key = 'key-1';
245 |
246 | lib = new SecureLS({ encodingType: 'RABBIT', isCompression: false });
247 | lib.storage = mockStorage;
248 | lib.set(key, data);
249 |
250 | // Encrypt data using RABBIT algorithm
251 | const valueStored = RABBIT.encrypt(JSON.stringify(data), lib.encryptionSecret).toString();
252 |
253 | // Assertions
254 | expect(mockStorage.storage[key]).toBeDefined();
255 | expect(typeof mockStorage.storage[key]).toBe('string');
256 |
257 | const decryptedData = JSON.parse(RABBIT.decrypt(valueStored, lib.encryptionSecret).toString(enc._Utf8));
258 | expect(data.toString()).toEqual(decryptedData.toString());
259 | });
260 | });
261 |
262 | describe('RABBIT encryption and data compression', () => {
263 | it('should encrypt data with RABBIT and compress before storing to localStorage', () => {
264 | let lib;
265 | const data = [1, 2, 3];
266 | const key = 'key-1';
267 |
268 | lib = new SecureLS({ encodingType: 'RABBIT', isCompression: true });
269 | lib.storage = mockStorage;
270 | lib.set(key, data);
271 |
272 | // Encrypt and compress data using RABBIT algorithm
273 | const encryptedData = RABBIT.encrypt(JSON.stringify(data), lib.encryptionSecret).toString();
274 | const valueStored = LZString.compressToUTF16(encryptedData);
275 |
276 | // Assertions
277 | expect(mockStorage.storage[key]).toBeDefined();
278 | expect(typeof mockStorage.storage[key]).toBe('string');
279 |
280 | const decompressedData = RABBIT.decrypt(LZString.decompressFromUTF16(valueStored), lib.encryptionSecret).toString(
281 | enc._Utf8,
282 | );
283 | const decryptedData = JSON.parse(decompressedData);
284 | expect(data.toString()).toEqual(decryptedData.toString());
285 | });
286 | });
287 |
288 | describe('RABBIT encryption, data compression but no secret key', () => {
289 | it('should encrypt data and store', () => {
290 | let lib;
291 | const data = [1, 2, 3];
292 | const key = 'key-1';
293 |
294 | lib = new SecureLS({
295 | encodingType: 'RABBIT',
296 | isCompression: true,
297 | encryptionSecret: undefined,
298 | });
299 | lib.storage = mockStorage;
300 | lib.set(key, data);
301 |
302 | const encryptedData = RABBIT.encrypt(JSON.stringify(data), lib.encryptionSecret).toString();
303 | const valueStored = LZString.compressToUTF16(encryptedData);
304 |
305 | // Without encryption secret, data should not be encrypted
306 | expect(mockStorage.storage[key]).toBeDefined();
307 | expect(typeof mockStorage.storage[key]).toBe('string');
308 |
309 | const decompressedData = RABBIT.decrypt(LZString.decompressFromUTF16(valueStored), lib.encryptionSecret).toString(
310 | enc._Utf8,
311 | );
312 | const decryptedData = JSON.parse(decompressedData);
313 | expect(data.toString()).toEqual(decryptedData.toString());
314 | });
315 | });
316 |
317 | describe('RC4 encryption and no data compression', () => {
318 | it('should encrypt data with RC4 before storing to localStorage', () => {
319 | let lib;
320 | const data = [1, 2, 3];
321 | const key = 'key-1';
322 |
323 | lib = new SecureLS({ encodingType: 'RC4', isCompression: false });
324 | lib.storage = mockStorage;
325 | lib.set(key, data);
326 |
327 | // Encrypt data using RC4 algorithm
328 | const valueStored = RC4.encrypt(JSON.stringify(data), lib.encryptionSecret).toString();
329 |
330 | // Assertions
331 | expect(mockStorage.storage[key]).toBeDefined();
332 | expect(typeof mockStorage.storage[key]).toBe('string');
333 |
334 | const decryptedData = JSON.parse(RC4.decrypt(valueStored, lib.encryptionSecret).toString(enc._Utf8));
335 | expect(data.toString()).toEqual(decryptedData.toString());
336 | });
337 | });
338 |
339 | describe('RC4 encryption and data compression', () => {
340 | it('should encrypt data with RC4 and compress before storing to localStorage', () => {
341 | let lib;
342 | const data = [1, 2, 3];
343 | const key = 'key-1';
344 |
345 | lib = new SecureLS({ encodingType: 'RC4', isCompression: true });
346 | lib.storage = mockStorage;
347 | lib.set(key, data);
348 |
349 | // Encrypt and compress data using RC4 algorithm
350 | const encryptedData = RC4.encrypt(JSON.stringify(data), lib.encryptionSecret).toString();
351 | const valueStored = LZString.compressToUTF16(encryptedData);
352 |
353 | // Assertions
354 | expect(mockStorage.storage[key]).toBeDefined();
355 | expect(typeof mockStorage.storage[key]).toBe('string');
356 |
357 | const decompressedData = RC4.decrypt(LZString.decompressFromUTF16(valueStored), lib.encryptionSecret).toString(
358 | enc._Utf8,
359 | );
360 | const decryptedData = JSON.parse(decompressedData);
361 | expect(data.toString()).toEqual(decryptedData.toString());
362 | });
363 | });
364 |
365 | describe('AES encryption and compression and multiple storages', () => {
366 | it('should manage two parallel storages with different encryption settings', () => {
367 | const data1 = [1, 2, 3];
368 | const data2 = [3, 4, 5];
369 | const secret1 = 'secret1';
370 | const secret2 = 'secret2';
371 | const realm1 = 'realm1';
372 | const realm2 = 'realm2';
373 | const key1 = 'key-1';
374 | const key2 = 'key-2';
375 |
376 | const lib1 = new SecureLS({
377 | encodingType: 'RC4',
378 | isCompression: true,
379 | encryptionSecret: secret1,
380 | encryptionNamespace: realm1,
381 | });
382 | const lib2 = new SecureLS({
383 | encodingType: 'RC4',
384 | isCompression: true,
385 | encryptionSecret: secret2,
386 | encryptionNamespace: realm2,
387 | });
388 |
389 | lib1.ls = mockStorage;
390 | lib2.ls = mockStorage;
391 |
392 | lib1.set(key1, data1);
393 | lib2.set(key2, data2);
394 |
395 | // Assertions
396 | expect(lib1.get(key1)).toEqual(data1);
397 | expect(lib2.get(key2)).toEqual(data2);
398 |
399 | let error = null;
400 | try {
401 | lib1.get(key2);
402 | } catch (e) {
403 | error = e;
404 | }
405 | expect(error).not.toBeNull();
406 |
407 | lib1.removeAll();
408 | lib2.removeAll();
409 | });
410 | });
411 | });
412 |
--------------------------------------------------------------------------------
/test/mock/ls.js:
--------------------------------------------------------------------------------
1 | // Storage Mock
2 | const mockLS = {
3 | storageMock: function () {
4 | const storage = {};
5 |
6 | return {
7 | storage,
8 | setItem: (key, value) => {
9 | storage[key] = value || '';
10 | },
11 | getItem: (key) => {
12 | return storage[key] || null;
13 | },
14 | removeItem: (key) => {
15 | delete storage[key];
16 | },
17 | };
18 | },
19 | };
20 |
21 | module.exports = mockLS;
22 |
--------------------------------------------------------------------------------
/test/standard.test.js:
--------------------------------------------------------------------------------
1 | import SecureLS from '../src/index';
2 |
3 | let lib;
4 |
5 | describe('Standard SecureLS API Tests ->', () => {
6 | beforeEach(() => {
7 | jest.spyOn(console, 'warn').mockImplementation(() => {});
8 | lib = new SecureLS();
9 | });
10 |
11 | afterEach(() => {
12 | console.warn.mockRestore();
13 | lib.removeAll();
14 | });
15 |
16 | describe('secure-ls: set method', () => {
17 | it('should warn if no key is provided', () => {
18 | expect(console.warn).not.toHaveBeenCalled();
19 | lib.set();
20 | expect(console.warn).toHaveBeenCalled();
21 | });
22 |
23 | it('should add key to list of stored keys', () => {
24 | const spyProcessData = jest.spyOn(lib, 'processData');
25 | const spySetData = jest.spyOn(lib, 'setDataToLocalStorage');
26 |
27 | lib.set('test123');
28 |
29 | expect(lib.allKeys).toBeDefined();
30 | expect(Array.isArray(lib.allKeys)).toBe(true);
31 | expect(lib.allKeys.length).toBe(1);
32 |
33 | expect(spyProcessData).toHaveBeenCalled();
34 | expect(spySetData).toHaveBeenCalled();
35 | });
36 | });
37 |
38 | describe('secure-ls: get method', () => {
39 | it('should warn if no key is provided', () => {
40 | expect(console.warn).not.toHaveBeenCalled();
41 | lib.get();
42 | expect(console.warn).toHaveBeenCalled();
43 | });
44 |
45 | it('should add key to list of stored keys', () => {
46 | const spyGetData = jest.spyOn(lib, 'getDataFromLocalStorage');
47 |
48 | lib.get('test123');
49 | expect(spyGetData).toHaveBeenCalled();
50 | });
51 | });
52 |
53 | describe('secure-ls: getAllKeys method', () => {
54 | it('should return [] if nothing set', () => {
55 | const keys = lib.getAllKeys();
56 | expect(Array.isArray(keys)).toBe(true);
57 | expect(keys.length).toBe(0);
58 | });
59 |
60 | it('should return keys when there are', () => {
61 | let keys = lib.getAllKeys();
62 | expect(keys.length).toBe(0);
63 |
64 | lib.set('key-1');
65 |
66 | keys = lib.getAllKeys();
67 | expect(Array.isArray(keys)).toBe(true);
68 | expect(keys.length).toBe(1);
69 |
70 | lib.set('key-2');
71 |
72 | keys = lib.getAllKeys();
73 | expect(Array.isArray(keys)).toBe(true);
74 | expect(keys.length).toBe(2);
75 | });
76 | });
77 |
78 | describe('secure-ls: remove method', () => {
79 | it('should warn if no key is provided', () => {
80 | expect(console.warn).not.toHaveBeenCalled();
81 | lib.remove();
82 | expect(console.warn).toHaveBeenCalled();
83 | });
84 |
85 | it('should warn if key is metakey and keys are there', () => {
86 | lib.set('key-1');
87 | lib.remove('_secure__ls__metadata');
88 | expect(console.warn).toHaveBeenCalled();
89 | });
90 |
91 | it('should not warn if key is metadata and no other keys present', () => {
92 | lib.remove('_secure__ls__metadata');
93 | expect(console.warn).not.toHaveBeenCalled();
94 | });
95 |
96 | it('should decrement counter', () => {
97 | lib.set('key-1', {});
98 | lib.set('key-2', []);
99 | expect(lib.allKeys.length).toBe(2);
100 |
101 | lib.remove();
102 | expect(console.warn).toHaveBeenCalled();
103 |
104 | lib.remove('key-2');
105 | expect(lib.allKeys.length).toBe(1);
106 |
107 | lib.remove('key-2');
108 | expect(lib.allKeys.length).toBe(1);
109 |
110 | lib.remove('key-1');
111 | expect(lib.allKeys.length).toBe(0);
112 | });
113 |
114 | it('should update the list of stored keys', () => {
115 | const spySetMetaData = jest.spyOn(lib, 'setMetaData');
116 | lib.set('key-1');
117 | lib.remove('key-1');
118 | expect(spySetMetaData).toHaveBeenCalled();
119 | });
120 | });
121 |
122 | describe('secure-ls: removeAll method', () => {
123 | it('verify allKeys length on removal', () => {
124 | const spyGetAllKeys = jest.spyOn(lib, 'getAllKeys');
125 | lib.set('key-1', { data: 'data' });
126 | lib.set('key-2', [1, 2, 3]);
127 |
128 | expect(lib.allKeys.length).toBe(2);
129 |
130 | lib.removeAll();
131 |
132 | expect(spyGetAllKeys).toHaveBeenCalled();
133 | expect(lib.allKeys.length).toBe(0);
134 | });
135 | });
136 |
137 | describe('secure-ls: clear method', () => {
138 | it('verify allKeys length on removal', () => {
139 | lib.set('key-1', { data: 'data' });
140 | lib.set('key-2', [1, 2, 3]);
141 |
142 | expect(lib.allKeys.length).toBe(2);
143 |
144 | lib.clear();
145 | expect(lib.allKeys.length).toBe(0);
146 | });
147 | });
148 | });
149 |
--------------------------------------------------------------------------------
/test/utils.test.js:
--------------------------------------------------------------------------------
1 | import SecureLS from '../src/index';
2 | import utils from '../src/utils';
3 |
4 | let lib;
5 |
6 | describe('Utils tests', () => {
7 | beforeEach(() => {
8 | jest.spyOn(console, 'warn').mockImplementation(() => {});
9 | lib = new SecureLS();
10 | });
11 |
12 | afterEach(() => {
13 | console.warn.mockRestore();
14 | lib.removeAll();
15 | });
16 |
17 | describe('method: is', () => {
18 | test('return true if key is present', () => {
19 | const response = utils.is('new-key');
20 | expect(response).toBe(true);
21 | });
22 |
23 | test('return false if key is not present', () => {
24 | const response = utils.is();
25 | expect(response).toBe(false);
26 | });
27 | });
28 |
29 | describe('method: warn', () => {
30 | test('warn with default warning msg if no reason provided', () => {
31 | utils.warn();
32 | expect(console.warn).toHaveBeenCalled();
33 | expect(console.warn).toHaveBeenCalledWith('Unexpected output');
34 | });
35 |
36 | test('warn with undefined warning msg if wrong reason provided', () => {
37 | utils.warn('wrong');
38 | expect(console.warn).toHaveBeenCalled();
39 | expect(console.warn).not.toHaveBeenCalledWith('Unexpected output');
40 | });
41 |
42 | test('warn with warning msg as per reason provided', () => {
43 | utils.warn('keyNotProvided');
44 | expect(console.warn).toHaveBeenCalled();
45 | expect(console.warn).toHaveBeenCalledWith('Secure LS: Key not provided. Aborting operation!');
46 | });
47 | });
48 |
49 | describe('method: generateSecretKey', () => {
50 | test('validate PBKDF2 key generated', () => {
51 | const encryptionKey = utils.generateSecretKey();
52 | expect(typeof encryptionKey).toBe('string');
53 | expect(encryptionKey.length).toBeGreaterThan(30);
54 | });
55 | });
56 |
57 | describe('method: getObjectFromKey', () => {
58 | test('if no data provided, return empty object', () => {
59 | const response = utils.getObjectFromKey();
60 | expect(response).toEqual({});
61 | });
62 |
63 | test('if data provided is empty array, return empty object', () => {
64 | const response = utils.getObjectFromKey([]);
65 | expect(response).toEqual({});
66 | });
67 |
68 | test('should return obj matching the key provided', () => {
69 | const data = [
70 | { k: 'name', test: 'case1' },
71 | { k: 'stars', test: 'case2' },
72 | ];
73 | const response = utils.getObjectFromKey(data, 'name');
74 | expect(response).toBe(data[0]);
75 | });
76 | });
77 |
78 | describe('method: extractKeyNames', () => {
79 | test('should return just the `k` values', () => {
80 | const data = {
81 | keys: [
82 | { k: 'name', test: 'case1' },
83 | { k: 'stars', test: 'case2' },
84 | ],
85 | };
86 | const response = utils.extractKeyNames(data);
87 | expect(Array.isArray(response)).toBe(true);
88 | expect(response).toContain('name');
89 | expect(response).not.toContain('test');
90 | });
91 | });
92 |
93 | describe('method: isKeyPresent', () => {
94 | test('should return the boolean based on key presence', () => {
95 | const allKeys = [
96 | { k: 'name', test: 'case1' },
97 | { k: 'stars', test: 'case2' },
98 | ];
99 | expect(utils.isKeyPresent(allKeys, 'name')).toBe(true);
100 | expect(utils.isKeyPresent(allKeys, 'wrong-key')).toBe(false);
101 | });
102 | });
103 |
104 | describe('method: removeFromKeysList', () => {
105 | test('should remove object from array if key matches', () => {
106 | const allKeys = [
107 | { k: 'name', test: 'case1' },
108 | { k: 'stars', test: 'case2' },
109 | ];
110 |
111 | expect(allKeys.length).toBe(2);
112 | utils.removeFromKeysList(allKeys, 'name');
113 | expect(allKeys.length).toBe(1);
114 | utils.removeFromKeysList(allKeys, 'wrong-key');
115 | expect(allKeys.length).toBe(1);
116 | utils.removeFromKeysList(allKeys, 'stars');
117 | expect(allKeys.length).toBe(0);
118 | });
119 | });
120 | });
121 |
--------------------------------------------------------------------------------
/types/secure-ls.d.ts:
--------------------------------------------------------------------------------
1 | export = SecureLS;
2 |
3 | import * as LZString from 'lz-string';
4 | import { CipherHelper, Encoder } from 'crypto-js';
5 |
6 | declare class SecureLS {
7 | constructor(config?: {
8 | isCompression?: boolean;
9 | encodingType?: string;
10 | encryptionSecret?: string;
11 | encryptionNamespace?: string;
12 | });
13 | getEncryptionSecret(): string;
14 | get(key: string, isAllKeysData?: boolean): any;
15 | getDataFromLocalStorage(key: string): string | null;
16 | getAllKeys(): string[];
17 | set(key: string, data: any): void;
18 | setDataToLocalStorage(key: string, data: string): void;
19 | remove(key: string): void;
20 | removeAll(): void;
21 | clear(): void;
22 | resetAllKeys(): string[];
23 | processData(data: any | string, isAllKeysData: boolean): string;
24 | setMetaData(): void;
25 | getMetaData(): { keys: string[] };
26 |
27 | storage: Storage;
28 |
29 | _name: string;
30 | Base64: SecureLS.Base64;
31 | LZString: LZString.LZStringStatic;
32 | AES: CipherHelper;
33 | DES: CipherHelper;
34 | RABBIT: CipherHelper;
35 | RC4: CipherHelper;
36 | enc: {
37 | Latin1: Encoder;
38 | _Utf8: Encoder;
39 | };
40 | }
41 |
42 | interface Storage {
43 | readonly length: number;
44 | clear(): void;
45 | getItem(key: string): string | null;
46 | key(index: number): string | null;
47 | removeItem(key: string): void;
48 | setItem(key: string, value: string): void;
49 | [name: string]: any;
50 | }
51 |
52 | declare namespace SecureLS {
53 | interface Base64 {
54 | _keyStr: string;
55 | encode(e: string): string;
56 | decode(e: string): string;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const semver = require('semver');
4 |
5 | const packageFile = require('./package.json');
6 | const libVersion = packageFile.version;
7 | const libraryName = packageFile.name;
8 |
9 | const PRODUCTION = 'production';
10 |
11 | let deps = '';
12 |
13 | Object.keys(packageFile.dependencies).map((key, index) => {
14 | deps += `\n ${index + 1}. ${key} - ${packageFile.dependencies[key]}`;
15 | });
16 |
17 | let libraryHeaderComment;
18 |
19 | function addPlugins(argv) {
20 | const version = semver.inc(libVersion, argv.env.type) || libVersion;
21 |
22 | libraryHeaderComment = `${libraryName} - v${version}
23 | URL - https://github.com/softvar/secure-ls
24 |
25 | The MIT License (MIT)
26 |
27 | Copyright (c) 2016-2024 Varun Malhotra
28 |
29 | Permission is hereby granted, free of charge, to any person obtaining a copy
30 | of this software and associated documentation files (the "Software"), to deal
31 | in the Software without restriction, including without limitation the rights
32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
33 | copies of the Software, and to permit persons to whom the Software is
34 | furnished to do so, subject to the following conditions:
35 |
36 | The above copyright notice and this permission notice shall be included in all
37 | copies or substantial portions of the Software.
38 |
39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
45 | SOFTWARE.
46 |
47 |
48 | Dependencies used - ${deps}`;
49 |
50 | const plugins = [
51 | new webpack.BannerPlugin({
52 | banner: libraryHeaderComment,
53 | entryOnly: true,
54 | stage: webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
55 | }),
56 | ];
57 |
58 | return plugins;
59 | }
60 |
61 | module.exports = function (_env, argv) {
62 | return {
63 | entry: {
64 | [libraryName]: '/src/index.js',
65 | },
66 | devtool: 'source-map',
67 | mode: argv.mode === PRODUCTION ? 'production' : 'development',
68 | output: {
69 | path: path.resolve(__dirname, 'dist'),
70 | filename: () => {
71 | if (argv.mode === PRODUCTION) {
72 | return '[name].min.js';
73 | }
74 |
75 | return '[name].js';
76 | },
77 | library: 'SecureLS',
78 | libraryTarget: 'umd',
79 | globalObject: 'this',
80 | auxiliaryComment: {
81 | root: ' Root',
82 | commonjs: ' CommonJS',
83 | commonjs2: ' CommonJS2',
84 | amd: ' AMD',
85 | },
86 | },
87 | module: {
88 | rules: [
89 | {
90 | test: /\.js$/,
91 | exclude: /(node_modules|bower_components)/,
92 | use: {
93 | loader: 'babel-loader',
94 | },
95 | },
96 | ],
97 | },
98 | resolve: {
99 | extensions: ['.js'],
100 | },
101 | plugins: addPlugins(argv),
102 | };
103 | };
104 |
--------------------------------------------------------------------------------