├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .npmignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── ci-package.json ├── commitlint.config.js ├── dist ├── secure-ls.js ├── secure-ls.js.map ├── secure-ls.min.js └── secure-ls.min.js.map ├── eslint.config.mjs ├── example ├── aes-compressed-realm.js ├── aes-compressed.js ├── aes-uncompressed.js ├── base64-compressed.js ├── custom-storage.js ├── des-compressed.js ├── des-uncompressed.js ├── index.html ├── only-base64.js ├── only-compressed.js ├── rabbit-compressed.js ├── rabbit-uncompressed.js ├── rc4-compressed.js ├── rc4-uncompressed.js ├── sessionStorage.js ├── standard.js └── vendor │ └── screenlog.min.js ├── jest.config.js ├── package-lock.json ├── package.json ├── screenshot.png ├── src ├── Base64.js ├── SecureLS.js ├── WordArray.js ├── constants.js ├── enc-utf8.js ├── index.js └── utils.js ├── test ├── basic.test.js ├── functional.test.js ├── localStorage.test.js ├── ls-data-compression.test.js ├── ls-data-enc-dec.test.js ├── mock │ └── ls.js ├── standard.test.js └── utils.test.js ├── types └── secure-ls.d.ts ├── webpack.config.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "globalReturn": true, 4 | "jsx": true, 5 | "modules": true 6 | }, 7 | 8 | "env": { 9 | "browser": true, 10 | "es6": true, 11 | "node": true 12 | }, 13 | 14 | "globals": { 15 | "document": false, 16 | "escape": false, 17 | "navigator": false, 18 | "unescape": false, 19 | "window": false, 20 | "describe": true, 21 | "before": true, 22 | "it": true, 23 | "expect": true, 24 | "sinon": true 25 | }, 26 | 27 | "parser": "babel-eslint", 28 | 29 | "plugins": [ 30 | 31 | ], 32 | 33 | "rules": { 34 | "block-scoped-var": 2, 35 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 36 | "camelcase": [2, { "properties": "always" }], 37 | "comma-dangle": [2, "never"], 38 | "comma-spacing": [2, { "before": false, "after": true }], 39 | "comma-style": [2, "last"], 40 | "complexity": 0, 41 | "consistent-return": 2, 42 | "consistent-this": 0, 43 | "curly": [2, "multi-line"], 44 | "default-case": 0, 45 | "dot-location": [2, "property"], 46 | "dot-notation": 0, 47 | "eol-last": 2, 48 | "eqeqeq": [2, "allow-null"], 49 | "func-names": 0, 50 | "func-style": 0, 51 | "generator-star-spacing": [2, "both"], 52 | "guard-for-in": 0, 53 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 54 | "indent": [2, 2, { "SwitchCase": 1 }], 55 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 56 | "linebreak-style": 0, 57 | "max-depth": 0, 58 | "max-len": [2, 120, 4], 59 | "max-nested-callbacks": 0, 60 | "max-params": 0, 61 | "max-statements": 0, 62 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 63 | "newline-after-var": [2, "always"], 64 | "new-parens": 2, 65 | "no-alert": 0, 66 | "no-array-constructor": 2, 67 | "no-bitwise": 0, 68 | "no-caller": 2, 69 | "no-catch-shadow": 0, 70 | "no-cond-assign": 2, 71 | "no-console": 0, 72 | "no-constant-condition": 0, 73 | "no-continue": 0, 74 | "no-control-regex": 2, 75 | "no-debugger": 2, 76 | "no-delete-var": 2, 77 | "no-div-regex": 0, 78 | "no-dupe-args": 2, 79 | "no-dupe-keys": 2, 80 | "no-duplicate-case": 2, 81 | "no-else-return": 2, 82 | "no-empty": 0, 83 | "no-empty-character-class": 2, 84 | "no-empty-label": 2, 85 | "no-eq-null": 0, 86 | "no-eval": 2, 87 | "no-ex-assign": 2, 88 | "no-extend-native": 2, 89 | "no-extra-bind": 2, 90 | "no-extra-boolean-cast": 2, 91 | "no-extra-parens": 0, 92 | "no-extra-semi": 0, 93 | "no-extra-strict": 0, 94 | "no-fallthrough": 2, 95 | "no-floating-decimal": 2, 96 | "no-func-assign": 2, 97 | "no-implied-eval": 2, 98 | "no-inline-comments": 0, 99 | "no-inner-declarations": [2, "functions"], 100 | "no-invalid-regexp": 2, 101 | "no-irregular-whitespace": 2, 102 | "no-iterator": 2, 103 | "no-label-var": 2, 104 | "no-labels": 2, 105 | "no-lone-blocks": 0, 106 | "no-lonely-if": 0, 107 | "no-loop-func": 0, 108 | "no-mixed-requires": 0, 109 | "no-mixed-spaces-and-tabs": [2, false], 110 | "no-multi-spaces": 2, 111 | "no-multi-str": 2, 112 | "no-multiple-empty-lines": [2, { "max": 1 }], 113 | "no-native-reassign": 2, 114 | "no-negated-in-lhs": 2, 115 | "no-nested-ternary": 0, 116 | "no-new": 2, 117 | "no-new-func": 2, 118 | "no-new-object": 2, 119 | "no-new-require": 2, 120 | "no-new-wrappers": 2, 121 | "no-obj-calls": 2, 122 | "no-octal": 2, 123 | "no-octal-escape": 2, 124 | "no-path-concat": 0, 125 | "no-plusplus": 0, 126 | "no-process-env": 0, 127 | "no-process-exit": 0, 128 | "no-proto": 2, 129 | "no-redeclare": 2, 130 | "no-regex-spaces": 2, 131 | "no-reserved-keys": 0, 132 | "no-restricted-modules": 0, 133 | "no-return-assign": 2, 134 | "no-script-url": 0, 135 | "no-self-compare": 2, 136 | "no-sequences": 2, 137 | "no-shadow": 0, 138 | "no-shadow-restricted-names": 2, 139 | "no-spaced-func": 2, 140 | "no-sparse-arrays": 2, 141 | "no-sync": 0, 142 | "no-ternary": 0, 143 | "no-throw-literal": 2, 144 | "no-trailing-spaces": 2, 145 | "no-undef": 2, 146 | "no-undef-init": 2, 147 | "no-undefined": 0, 148 | "no-underscore-dangle": 0, 149 | "no-unneeded-ternary": 2, 150 | "no-unreachable": 2, 151 | "no-unused-expressions": 0, 152 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 153 | "no-use-before-define": 2, 154 | "no-var": 0, 155 | "no-void": 0, 156 | "no-warning-comments": 0, 157 | "no-with": 2, 158 | "one-var": 0, 159 | "operator-assignment": 0, 160 | "operator-linebreak": [2, "after"], 161 | "padded-blocks": 0, 162 | "quote-props": 0, 163 | "quotes": [2, "single", "avoid-escape"], 164 | "radix": 2, 165 | "semi": [2, "always"], 166 | "semi-spacing": 0, 167 | "sort-vars": 0, 168 | "space-after-keywords": [2, "always"], 169 | "space-before-blocks": [2, "always"], 170 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 171 | "space-in-brackets": 0, 172 | "space-in-parens": [2, "never"], 173 | "space-infix-ops": 2, 174 | "space-return-throw-case": 2, 175 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 176 | "spaced-comment": [2, "always"], 177 | "strict": 0, 178 | "use-isnan": 2, 179 | "valid-jsdoc": 0, 180 | "valid-typeof": 2, 181 | "vars-on-top": 2, 182 | "wrap-iife": [2, "any"], 183 | "wrap-regex": 0, 184 | "yoda": [2, "never"] 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [softvar] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: softvar # Replace with a single Patreon username 5 | open_collective: softvar # Replace with a single Open Collective username 6 | custom: ['https://paypal.me/softvar'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master, github-action, develop] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | if: "!contains(toJSON(github.event.commits.*.message), '[skip-ci]')" 12 | name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: 18 | - ubuntu-latest 19 | # - macos-latest 20 | # - windows-latest 21 | node_version: [ 10.x, 12.x, 13.x, 14.x, 15.x, 16.x, 18.x, 19.x, 20.x, 21.x, 22.x ] 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node_version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node_version }} 29 | 30 | - name: Restore packages 31 | uses: actions/cache@v2 32 | with: 33 | path: | 34 | node_modules 35 | */*/node_modules 36 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 37 | 38 | - name: Copy package.json compatible with node version 39 | run: | 40 | if [[ ${{ matrix.node_version }} == 10* ]]; then 41 | cp ci-package.json package.json 42 | elif [[ ${{ matrix.node_version }} == 12* ]]; then 43 | cp ci-package.json package.json 44 | elif [[ ${{ matrix.node_version }} == 13* ]]; then 45 | cp ci-package.json package.json 46 | elif [[ ${{ matrix.node_version }} == 14* ]]; then 47 | cp ci-package.json package.json 48 | elif [[ ${{ matrix.node_version }} == 15* ]]; then 49 | cp ci-package.json package.json 50 | elif [[ ${{ matrix.node_version }} == 16* ]]; then 51 | cp ci-package.json package.json 52 | elif [[ ${{ matrix.node_version }} == 19* ]]; then 53 | cp ci-package.json package.json 54 | fi 55 | 56 | - name: Install dependencies and run tests with coverage 57 | run: | 58 | yarn install 59 | yarn test:coverage 60 | 61 | - name: Upload coverage to Codecov 62 | uses: codecov/codecov-action@v4.0.1 63 | with: 64 | token: ${{ secrets.CODECOV_TOKEN }} 65 | flags: ${{ runner.os }} 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | # Logs 3 | logs 4 | *.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Coverage directory used by tools like istanbul 12 | coverage 13 | .coveralls.yml 14 | 15 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 16 | .grunt 17 | 18 | # node-waf configuration 19 | .lock-wscript 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 26 | node_modules 27 | .vscode 28 | .DS_STORE 29 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit "$1" 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | yarn lint 2 | yarn test:prod 3 | yarn build 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | node_modules 4 | webpack.config.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 120, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "singleQuote": true, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dev@wingify.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to Secure LS 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | 10 | ### We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 11 | 12 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 13 | 14 | 1. Fork the repo and create your branch from `master`. 15 | 2. If you've added code that should be tested, add tests. 16 | 3. Ensure the test suite passes. 17 | 4. Make sure your code lints. 18 | 5. Open a pull request! 19 | 20 | ### Any contributions you make will be under the MIT Software License 21 | 22 | When you submit code changes, your submissions are understood to be under the same [MIT License](https://opensource.org/license/mit) that covers the project. Feel free to contact the maintainers if that's a concern. 23 | 24 | ### Report bugs using GitHub issues 25 | 26 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/softvar/secure-ls/issues); it's that easy! 27 | 28 | **Note**: Write bug reports with detail, background, and sample code 29 | 30 | **Great Bug Reports** tend to have: 31 | 32 | - A quick summary and/or background 33 | - Steps to reproduce 34 | - Be specific! 35 | - Give sample code if you can. 36 | - What you expected would happen 37 | - What actually happens 38 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 39 | 40 | ### Use a Consistent Coding Style 41 | 42 | We use [prettier](https://prettier.io/) for auto-formatting and consistent coding style among developers. 43 | 44 | ### License 45 | 46 | By contributing, you agree that your contributions will be licensed under its MIT License. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2024 Varun Malhotra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secure-ls 2 | 3 | Secure localStorage/sessionStorage data with high level of encryption and data compression. 4 | 5 | [![npm version](https://img.shields.io/npm/v/secure-ls?style=for-the-badge&color=grey&logo=npm)](https://www.npmjs.com/package/secure-ls) [![npm](https://img.shields.io/npm/dt/secure-ls.svg?style=for-the-badge&color=blue)](https://www.npmjs.com/package/secure-ls) 6 | 7 | [![CI](https://img.shields.io/github/actions/workflow/status/softvar/secure-ls/test.yml?style=for-the-badge&logo=github)](https://github.com/softvar/secure-ls/actions?query=workflow%3ACI) 8 | [![codecov](https://img.shields.io/codecov/c/github/softvar/secure-ls?token=HxMmlpGn3k&style=for-the-badge&logo=codecov)](https://codecov.io/gh/softvar/secure-ls) 9 | 10 | **[LIVE DEMO](http://softvar.github.io/secure-ls#live-demo)** 11 | 12 | ## Features 13 | 14 | - Secure data with various types of encryption including `AES`, `DES`, `Rabbit` and `RC4`. (defaults to `Base64` encoding). 15 | - Compress data before storing it to `localStorage` to save extra bytes (defaults to `true`). 16 | - Advanced API wrapper over `localStorage` API, providing other basic utilities. 17 | - Save data in multiple keys inside `localStorage` and `secure-ls` will always remember it's creation. 18 | 19 | ## Installation 20 | 21 | Via NPM 22 | 23 | ```bash 24 | npm install secure-ls 25 | ``` 26 | 27 | Via yarn 28 | 29 | ```bash 30 | yarn add secure-ls 31 | ``` 32 | 33 | ## Libraries used 34 | 35 | - **Encryption / Decryption** using [The Cipher Algorithms](https://code.google.com/archive/p/crypto-js) 36 | 37 | It requires secret-key for encrypting and decrypting data securely. If custom secret-key is provided as mentioned below in APIs, then the library will pick that otherwise it will generate yet another very `secure` unique password key using [PBKDF2](https://code.google.com/archive/p/crypto-js/#PBKDF2), which will be further used for future API requests. 38 | 39 | `PBKDF2` is a password-based key derivation function. In many applications of cryptography, user security is ultimately dependent on a password, and because a password usually can't be used directly as a cryptographic key, some processing is required. 40 | 41 | A salt provides a large set of keys for any given password, and an iteration count increases the cost of producing keys from a password, thereby also increasing the difficulty of attack. 42 | 43 | Eg: `55e8f5585789191d350329b9ebcf2b11` and `db51d35aad96610683d5a40a70b20c39`. 44 | 45 | For the generation of such strings, `secretPhrase` is being used and can be found in code easily but that won't make it unsecure, `PBKDF2`'s layer on top of that will handle security. 46 | 47 | - **Compresion / Decompression** using [lz-string](https://github.com/pieroxy/lz-string) 48 | 49 | ## Usage 50 | 51 | - Example 1: With `default` settings i.e. `Base64` Encoding and Data Compression 52 | 53 | ```javascript 54 | const ls = new SecureLS(); 55 | 56 | ls.set('key1', { data: 'test' }); // set key1 57 | ls.get('key1'); // print data 58 | // {data: 'test'} 59 | ``` 60 | 61 | - Example 2: With `AES` Encryption and Data Compression 62 | 63 | ```javascript 64 | const ls = new SecureLS({ encodingType: 'aes' }); 65 | 66 | ls.set('key1', { data: 'test' }); // set key1 67 | ls.get('key1'); // print data 68 | // {data: 'test'} 69 | 70 | ls.set('key2', [1, 2, 3]); // set another key 71 | ls.getAllKeys(); // get all keys 72 | // ["key1", "key2"] 73 | ls.removeAll(); // remove all keys 74 | ``` 75 | 76 | - Example 3: With `RC4` Encryption but no Data Compression 77 | 78 | ```javascript 79 | const ls = new SecureLS({ encodingType: 'rc4', isCompression: false }); 80 | 81 | ls.set('key1', { data: 'test' }); // set key1 82 | ls.get('key1'); // print data 83 | // {data: 'test'} 84 | 85 | ls.set('key2', [1, 2, 3]); // set another key 86 | ls.getAllKeys(); // get all keys 87 | // ["key1", "key2"] 88 | ls.removeAll(); // remove all keys 89 | ``` 90 | 91 | - Example 3: With `DES` Encryption, no Data Compression and custom secret key 92 | 93 | ```javascript 94 | const ls = new SecureLS({ encodingType: 'des', isCompression: false, encryptionSecret: 'my-secret-key' }); 95 | 96 | ls.set('key1', { data: 'test' }); // set key1 97 | ls.get('key1'); // print data 98 | // {data: 'test'} 99 | 100 | ls.set('key2', [1, 2, 3]); // set another key 101 | ls.getAllKeys(); // get all keys 102 | // ["key1", "key2"] 103 | ls.removeAll(); // remove all keys 104 | ``` 105 | 106 | ## API Documentation 107 | 108 | #### Create an instance / reference before using. 109 | 110 | ```javascript 111 | const ls = new SecureLS(); 112 | ``` 113 | 114 | `Contructor` accepts a configurable `Object` with all three keys being optional. 115 | 116 | | Config Keys | default | accepts | 117 | | ----------------------- | ----------------------- | ------------------------------------------------------------------------------ | 118 | | **encodingType** | Base64 | `base64`/`aes`/`des`/`rabbit`/`rc4`/`''` | 119 | | **isCompression** | `true` | `true`/`false` | 120 | | **encryptionSecret** | PBKDF2 value | String | 121 | | **encryptionNamespace** | null | String | 122 | | **storage** | localStorage | sessionStorage/localStorage or any storage object having same methods as ls/ss | 123 | | **metaKey** | `_secure__ls__metadata` | String | 124 | 125 | **Note:** `encryptionSecret` will only be used for the Encryption and Decryption of data 126 | with `AES`, `DES`, `RC4`, `RABBIT`, and the library will discard it if no encoding / Base64 127 | encoding method is choosen. 128 | 129 | `encryptionNamespace` is used to make multiple instances with different `encryptionSecret` 130 | and/or different `encryptionSecret` possible. 131 | 132 | ```javascript 133 | const ls1 = new SecureLS({ encodingType: 'des', encryptionSecret: 'my-secret-key-1' }); 134 | 135 | const ls2 = new SecureLS({ encodingType: 'aes', encryptionSecret: 'my-secret-key-2' }); 136 | ``` 137 | 138 | **Examples:** 139 | 140 | - No config or empty Object i.e. Default **`Base64 Encoding`** and **`Data compression`** 141 | 142 | ```javascript 143 | const ls = new SecureLS(); 144 | // or 145 | const ls = new SecureLS({}); 146 | ``` 147 | 148 | - No encoding No data compression i.e. **`Normal`** way of storing data 149 | 150 | ```javascript 151 | const ls = new SecureLS({ encodingType: '', isCompression: false }); 152 | ``` 153 | 154 | - **`Base64`** encoding but **`no`** data compression 155 | 156 | ```javascript 157 | const ls = new SecureLS({ isCompression: false }); 158 | ``` 159 | 160 | - **`AES`** encryption and **`data compression`** 161 | 162 | ```javascript 163 | const ls = new SecureLS({ encodingType: 'aes' }); 164 | ``` 165 | 166 | - **`RC4`** encryption and **`no`** data compression 167 | 168 | ```javascript 169 | const ls = new SecureLS({ encodingType: 'rc4', isCompression: false }); 170 | ``` 171 | 172 | - **`RABBIT`** encryption, **`no`** data compression and `custom` encryptionSecret 173 | 174 | ```javascript 175 | const ls = new SecureLS({ encodingType: 'rc4', isCompression: false, encryptionSecret: 's3cr3tPa$$w0rd@123' }); 176 | ``` 177 | 178 | - Any other **storage** apart from localStorage 179 | 180 | - Using `sessionStorage` 181 | 182 | ```javascript 183 | const ls = new SecureLS({ encodingType: 'aes', isCompression: true, storage: sessionStorage }); 184 | ``` 185 | 186 | - Using any storage having same methods as `localStorage/sessionStorage` 187 | 188 | ```javascript 189 | window.customSecureLsStore = {}; 190 | const storage = { 191 | setItem: (key, value) => { 192 | window.customSecureLsStore[key] = value || ''; 193 | }, 194 | getItem: (key) => { 195 | return window.customSecureLsStore[key] || null; 196 | }, 197 | removeItem: (key) => { 198 | delete window.customSecureLsStore[key]; 199 | }, 200 | clear: () => { 201 | window.customSecureLsStore = {}; 202 | }, 203 | }; 204 | 205 | const ls = new SecureLS({ encodingType: 'aes', isCompression: true, storage: storage }); 206 | ``` 207 | 208 | #### Methods 209 | 210 | - **`set`** 211 | 212 | Saves `data` in specifed `key` in localStorage. If the key is not provided, the library will warn. Following types of JavaScript objects are supported: 213 | 214 | - Array 215 | - ArrayBuffer 216 | - Blob 217 | - Float32Array 218 | - Float64Array 219 | - Int8Array 220 | - Int16Array 221 | - Int32Array 222 | - Number 223 | - Object 224 | - Uint8Array 225 | - Uint8ClampedArray 226 | - Uint16Array 227 | - Uint32Array 228 | - String 229 | 230 | | Parameter | Description | 231 | | --------- | -------------------- | 232 | | key | key to store data in | 233 | | data | data to be stored | 234 | 235 | ```javascript 236 | ls.set('key-name', { test: 'secure-ls' }); 237 | ``` 238 | 239 | - **`get`** 240 | 241 | Gets `data` back from specified `key` from the localStorage library. If the key is not provided, the library will warn. 242 | 243 | | Parameter | Description | 244 | | --------- | --------------------------- | 245 | | key | key in which data is stored | 246 | 247 | ```javascript 248 | ls.get('key-name'); 249 | ``` 250 | 251 | - **`remove`** 252 | 253 | Removes the value of a key from the localStorage. If the `meta key`, which stores the list of keys, is tried to be removed even if there are other keys which were created by `secure-ls` library, the library will warn for the action. 254 | 255 | | Parameter | Description | 256 | | --------- | ---------------------------------- | 257 | | key | remove key in which data is stored | 258 | 259 | ```javascript 260 | ls.remove('key-name'); 261 | ``` 262 | 263 | - **`removeAll`** 264 | 265 | Removes all the keys that were created by the `secure-ls` library, even the `meta key`. 266 | 267 | ```javascript 268 | ls.removeAll(); 269 | ``` 270 | 271 | - **`clear`** 272 | 273 | Removes all the keys ever created for that particular domain. Remember localStorage works differently for `http` and `https` protocol; 274 | 275 | ```javascript 276 | ls.clear(); 277 | ``` 278 | 279 | - **`getAllKeys`** 280 | 281 | Gets the list of keys that were created using the `secure-ls` library. Helpful when data needs to be retrieved for all the keys or when keys name are not known(dynamically created keys). 282 | 283 | `getAllKeys()` 284 | 285 | ```javascript 286 | ls.getAllKeys(); 287 | ``` 288 | 289 | ## Screenshot 290 | 291 | 292 | 293 | ## Scripts 294 | 295 | - `npm run build` - produces production version of the library under the `dist` folder 296 | - `npm run build-dev` - produces development version of the library and runs a watcher 297 | - `npm run test` - well ... it runs the tests :) 298 | 299 | ## Contributing 300 | 301 | 1. Fork the repo on GitHub. 302 | 2. Clone the repo on machine. 303 | 3. Execute `npm install` and `npm run dev`. 304 | 4. Create a new branch `` 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 | --------------------------------------------------------------------------------