├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── index.js ├── lib └── RNCryptor.js ├── package.json └── test ├── fixtures └── Octocat.png ├── spec.js ├── test.js └── title.js /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [14.x, 16.x, 18.x] 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: git submodule init 22 | - run: git submodule update 23 | - run: npm i 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | npm-debug.log 4 | v8.log 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec"] 2 | path = spec 3 | url = https://github.com/RNCryptor/RNCryptor-Spec.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Christian Gutierrez 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSCryptor 2 | 3 | *Javascript implementation of [RNCryptor](https://github.com/RNCryptor/RNCryptor-Spec)* 4 | 5 | This implementation tries to be compatible with [Rob Napier's Objective-C implementation of RNCryptor](https://github.com/RNCryptor/RNCryptor), It supports schema version 3. 6 | This code is based on the [PHP implementation of RNCryptor](https://github.com/RNCryptor/RNCryptor-php). 7 | 8 | ## Important Recent Changes 9 | * Now a `Buffer` is returned, use `.toString()` to convert the result to whatever format you need. 10 | * `mcrypt` library not used anymore. Thanks to @b00tsy. 11 | * Support dropped for Nodejs 12 and below. 12 | 13 | ## Install 14 | ```bash 15 | npm install jscryptor 16 | ``` 17 | 18 | ## Install on Windows 19 | ### Thanks to @jimmitaker and @black-snow for pointing this 20 | 21 | VS2015+ (Community Edition works fine) is required. 22 | 23 | ## Test 24 | ```bash 25 | npm test 26 | ``` 27 | 28 | ## Example 29 | ```js 30 | // Example taken from https://github.com/RNCryptor/RNCryptor-php/blob/master/examples/decrypt.php 31 | 32 | const password = 'myPassword'; 33 | const b64string = "AwHsr+ZD87myaoHm51kZX96u4hhaTuLkEsHwpCRpDywMO1Moz35wdS6OuDgq+SIAK6BOSVKQFSbX/GiFSKhWNy1q94JidKc8hs581JwVJBrEEoxDaMwYE+a+sZeirThbfpup9WZQgp3XuZsGuZPGvy6CvHWt08vsxFAn9tiHW9EFVtdSK7kAGzpnx53OUSt451Jpy6lXl1TKek8m64RT4XPr"; 34 | 35 | const RNCryptor = require('jscryptor'); 36 | 37 | console.time('Decrypting example'); 38 | const decrypted = RNCryptor.Decrypt(b64string, password); 39 | console.timeEnd('Decrypting example'); 40 | console.log("Result:", decrypted.toString()); 41 | ``` 42 | 43 | ### A very good example, provided by @enricodeleo 44 | ```js 45 | const fs = require('fs'); 46 | const RNCryptor = require('jscryptor'); 47 | 48 | const password = 'myPassword'; 49 | 50 | const img = fs.readFileSync('./Octocat.jpg'); 51 | const enc = RNCryptor.Encrypt(img, password); 52 | 53 | // Save encrypted image to a file, for sending to anywhere 54 | fs.writeFileSync('./Octocat.enc', enc); 55 | 56 | // Now, to decrypt the image: 57 | const b64 = Buffer.from(fs.readFileSync('./Octocat.enc').toString(), 'base64'); 58 | const dec = RNCryptor.Decrypt(b64, password); 59 | 60 | fs.writeFileSync('./Octocat2.jpg', dec); // Image should open. 61 | ``` 62 | 63 | ## API 64 | ### RNCryptor() 65 | Object exposed by `require('jscryptor')`; 66 | 67 | ### RNCryptor.Encrypt 68 | * plain_text: *String* or *Buffer* 69 | * password: *String* or *Buffer* 70 | * version: *Number* (3 by default, not mandatory) 71 | 72 | ### RNCryptor.Decrypt 73 | * b64_str: *String* or *Buffer* 74 | * password: *String* or *Buffer* 75 | 76 | ### RNCryptor.EncryptWithArbitrarySalts 77 | * plain_text: *String* or *Buffer* 78 | * password: *String* or *Buffer* 79 | * encryption_salt: *String* or *Buffer* 80 | * hmac_salt: *String* or *Buffer* 81 | * iv: *String* or *Buffer* 82 | * version: *Number* (3 by default, not mandatory) 83 | 84 | ### RNCryptor.EncryptWithArbitraryKeys 85 | * plain_text: *String* or *Buffer* 86 | * encryption_key: *String* or *Buffer* 87 | * hmac_key: *String* or *Buffer* 88 | * iv: *String* or *Buffer* 89 | * version: *Number* (3 by default, not mandatory) 90 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | module.exports = require('./lib/RNCryptor'); 3 | })(); 4 | -------------------------------------------------------------------------------- /lib/RNCryptor.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const crypto = require('crypto'); 3 | 4 | let _settings = {}; 5 | 6 | const _configure_settings = function (version) { 7 | const settings = { 8 | algorithm: 'aes-256-cbc', 9 | salt_length: 8, 10 | iv_length: 16, 11 | pbkdf2: { 12 | iterations: 10000, 13 | key_length: 32 14 | }, 15 | hmac: { 16 | length: 32 17 | } 18 | }; 19 | 20 | switch (version) { 21 | case 3: 22 | settings.options = 1; 23 | settings.hmac.includes_header = true; 24 | settings.hmac.algorithm = 'sha256'; 25 | break; 26 | default: 27 | throw `Unsupported schema version ${version}` 28 | } 29 | 30 | _settings = settings; 31 | }; 32 | 33 | const _unpack_encrypted_base64_data = function (b64str) { 34 | const data = Buffer.from(b64str, 'base64'); 35 | 36 | const components = { 37 | headers: _parseHeaders(data), 38 | hmac: data.slice(data.length - _settings.hmac.length) 39 | }; 40 | 41 | const header_length = components.headers.length; 42 | const cipher_text_length = data.length - header_length - components.hmac.length; 43 | 44 | components.cipher_text = data.slice(header_length, header_length + cipher_text_length); 45 | 46 | return components; 47 | }; 48 | 49 | const _parseHeaders = function (buffer_data) { 50 | let offset = 0; 51 | 52 | const version_char = buffer_data.slice(offset, offset + 1); 53 | offset += version_char.length; 54 | 55 | _configure_settings(version_char.toString().charCodeAt(0)); 56 | 57 | const options_char = buffer_data.slice(offset, offset + 1); 58 | offset += options_char.length; 59 | 60 | const encryption_salt = buffer_data.slice(offset, offset + _settings.salt_length); 61 | offset += encryption_salt.length; 62 | 63 | const hmac_salt = buffer_data.slice(offset, offset + _settings.salt_length); 64 | offset += hmac_salt.length; 65 | 66 | const iv = buffer_data.slice(offset, offset + _settings.iv_length); 67 | offset += iv.length; 68 | 69 | return { 70 | version: version_char, 71 | options: options_char, 72 | encryption_salt: encryption_salt, 73 | hmac_salt: hmac_salt, 74 | iv: iv, 75 | length: offset 76 | }; 77 | }; 78 | 79 | const _hmac_is_valid = function (components, password) { 80 | const hmac_key = _generate_key(password, components.headers.hmac_salt); 81 | 82 | // For 0.11+ we can use Buffer.compare 83 | return components.hmac.toString('hex') === _generate_hmac(components, hmac_key).toString('hex'); 84 | }; 85 | 86 | const _generate_key = function (password, salt) { 87 | return crypto.pbkdf2Sync(password, salt, _settings.pbkdf2.iterations, _settings.pbkdf2.key_length, 'SHA1'); 88 | }; 89 | 90 | const _generate_hmac = function (components, hmac_key) { 91 | let hmac_message = Buffer.from(''); 92 | 93 | if (_settings.hmac.includes_header) { 94 | hmac_message = Buffer.concat([ 95 | hmac_message, 96 | components.headers.version, 97 | components.headers.options, 98 | components.headers.encryption_salt || Buffer.from(''), 99 | components.headers.hmac_salt || Buffer.from(''), 100 | components.headers.iv 101 | ]); 102 | } 103 | 104 | hmac_message = Buffer.concat([hmac_message, components.cipher_text]); 105 | 106 | return crypto.createHmac(_settings.hmac.algorithm, hmac_key).update(hmac_message).digest(); 107 | }; 108 | 109 | const _generate_initialized_components = function (version) { 110 | return { 111 | headers: { 112 | version: Buffer.from(String.fromCharCode(version)), 113 | options: Buffer.from(String.fromCharCode(_settings.options)) 114 | } 115 | }; 116 | }; 117 | 118 | const _generate_salt = function () { 119 | return _generate_iv(_settings.salt_length); 120 | }; 121 | 122 | const _generate_iv = function (block_size) { 123 | return crypto.randomBytes(block_size) 124 | }; 125 | 126 | const _encrypt = function (plain_text, components, encryption_key, hmac_key) { 127 | const cipher = crypto.createCipheriv(_settings.algorithm, encryption_key, components.headers.iv) 128 | components.cipher_text = Buffer.concat([cipher.update(plain_text), cipher.final()]) 129 | 130 | const data = Buffer.concat([ 131 | components.headers.version, 132 | components.headers.options, 133 | components.headers.encryption_salt || Buffer.from(''), 134 | components.headers.hmac_salt || Buffer.from(''), 135 | components.headers.iv, 136 | components.cipher_text 137 | ]); 138 | 139 | const hmac = _generate_hmac(components, hmac_key); 140 | 141 | return Buffer.concat([data, hmac]).toString('base64'); 142 | }; 143 | 144 | const RNCryptor = {}; 145 | 146 | RNCryptor.GenerateKey = _generate_key; 147 | 148 | RNCryptor.Encrypt = function (plain_text, password, version = 3) { 149 | Buffer.isBuffer(plain_text) || (plain_text = Buffer.from(plain_text, 'binary')); 150 | Buffer.isBuffer(password) || (password = Buffer.from(password, 'binary')); 151 | 152 | _configure_settings(version); 153 | 154 | const components = _generate_initialized_components(version); 155 | components.headers.encryption_salt = _generate_salt(); 156 | components.headers.hmac_salt = _generate_salt(); 157 | components.headers.iv = _generate_iv(_settings.iv_length); 158 | 159 | const encryption_key = _generate_key(password, components.headers.encryption_salt); 160 | const hmac_key = _generate_key(password, components.headers.hmac_salt); 161 | 162 | return _encrypt(plain_text, components, encryption_key, hmac_key); 163 | }; 164 | 165 | RNCryptor.EncryptWithArbitrarySalts = function (plain_text, password, encryption_salt, hmac_salt, iv, version = 3) { 166 | Buffer.isBuffer(plain_text) || (plain_text = Buffer.from(plain_text, 'binary')); 167 | Buffer.isBuffer(password) || (password = Buffer.from(password)); 168 | Buffer.isBuffer(encryption_salt) || (encryption_salt = Buffer.from(encryption_salt, 'binary')); 169 | Buffer.isBuffer(hmac_salt) || (hmac_salt = Buffer.from(hmac_salt, 'binary')); 170 | Buffer.isBuffer(iv) || (iv = Buffer.from(iv, 'binary')); 171 | 172 | _configure_settings(version); 173 | 174 | const components = _generate_initialized_components(version); 175 | components.headers.encryption_salt = encryption_salt; 176 | components.headers.hmac_salt = hmac_salt; 177 | components.headers.iv = iv; 178 | 179 | const encryption_key = _generate_key(password, encryption_salt); 180 | const hmac_key = _generate_key(password, hmac_salt); 181 | 182 | return _encrypt(plain_text, components, encryption_key, hmac_key); 183 | }; 184 | 185 | RNCryptor.EncryptWithArbitraryKeys = function (plain_text, encryption_key, hmac_key, iv, version = 3) { 186 | Buffer.isBuffer(plain_text) || (plain_text = Buffer.from(plain_text, 'binary')); 187 | Buffer.isBuffer(encryption_key) || (encryption_key = Buffer.from(encryption_key, 'binary')); 188 | Buffer.isBuffer(hmac_key) || (hmac_key = Buffer.from(hmac_key, 'binary')); 189 | Buffer.isBuffer(iv) || (iv = Buffer.from(iv, 'binary')); 190 | 191 | _settings.options = 0; 192 | 193 | const components = _generate_initialized_components(version); 194 | components.headers.iv = iv; 195 | 196 | return _encrypt(plain_text, components, encryption_key, hmac_key); 197 | }; 198 | 199 | RNCryptor.Decrypt = function (b64str, password) { 200 | const components = _unpack_encrypted_base64_data(b64str); 201 | 202 | Buffer.isBuffer(password) || (password = Buffer.from(password, 'binary')); 203 | 204 | if (!_hmac_is_valid(components, password)) { 205 | return; 206 | } 207 | 208 | const key = _generate_key(password, components.headers.encryption_salt); 209 | const decipher = crypto.createDecipheriv(_settings.algorithm, key, components.headers.iv); 210 | return Buffer.concat([decipher.update(components.cipher_text), decipher.final()]); 211 | }; 212 | 213 | module.exports = RNCryptor; 214 | })(); 215 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jscryptor", 3 | "version": "0.1.1", 4 | "description": "Javascript implementation of RNCryptor", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "devDependencies": { 10 | "chai": "^4.3.7", 11 | "mocha": "^10.2.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/chesstrian/JSCryptor.git" 16 | }, 17 | "keywords": [ 18 | "rncryptor" 19 | ], 20 | "author": { 21 | "name": "Christian Gutierrez", 22 | "email": "chesstrian@gmail.com" 23 | }, 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/chesstrian/JSCryptor/issues" 27 | }, 28 | "homepage": "https://github.com/chesstrian/JSCryptor" 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/Octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chesstrian/JSCryptor/e88ff306c8153f8ebeca5c728b9c03c56f190f56/test/fixtures/Octocat.png -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const spec_dir = path.join(__dirname, '../spec/vectors/v3/'); 6 | 7 | fs.readdirSync(spec_dir).forEach(function (file) { 8 | let aux_object; 9 | exports[file] = []; 10 | 11 | const file_content = fs.readFileSync(spec_dir + file); 12 | file_content.toString().split('\n').forEach(function (line) { 13 | if (line === '' || line[0] === '#') return; 14 | 15 | const key_value = line.split(':'); 16 | if (key_value[0] === 'title') aux_object = {}; 17 | 18 | if (['key_hex', 'ciphertext_hex', 'plaintext_hex'].indexOf(key_value[0]) >= 0) 19 | key_value[1] = key_value[1].replace(/\s+/g, ''); 20 | 21 | aux_object[key_value[0]] = key_value[1].trim() || ''; 22 | 23 | if (['key_hex', 'ciphertext_hex'].indexOf(key_value[0]) >= 0) exports[file].push(aux_object); 24 | }); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | const expect = require('chai').expect; 5 | const RNCryptor = require('../index'); 6 | 7 | const specs = require('./spec'); 8 | require('./title'); 9 | 10 | const getSha1Sum = function (file) { 11 | const hashSum = crypto.createHash('sha1'); 12 | hashSum.update(file); 13 | return hashSum.digest('hex'); 14 | }; 15 | 16 | const password = 'סיסמא'; 17 | const plain_text = 'Some plain text'; 18 | 19 | describe('RNCryptor: Supported version', function () { 20 | it('Decrypt encrypted text for version 3, expect be the same than plain text', function () { 21 | const encrypted = RNCryptor.Encrypt(plain_text, password); 22 | const decrypted = RNCryptor.Decrypt(encrypted, password); 23 | 24 | expect(plain_text).to.equal(decrypted.toString()); 25 | }); 26 | 27 | it('Decrypt encrypted image for version 3, expect be the same than plain text', function () { 28 | const image = fs.readFileSync('test/fixtures/Octocat.png'); 29 | const encrypted = RNCryptor.Encrypt(image, password); 30 | const decrypted = RNCryptor.Decrypt(encrypted, password); 31 | 32 | expect(getSha1Sum(image)).to.equal(getSha1Sum(decrypted)); 33 | }); 34 | }); 35 | 36 | for (const key in specs) { 37 | if (!specs.hasOwnProperty(key)) { 38 | continue; 39 | } 40 | 41 | describe('RNCryptor: ' + key.title() + ' spec', function (key) { 42 | return function () { 43 | if (key === 'kdf') { 44 | specs[key].forEach(function (spec) { 45 | it(spec.title, function () { 46 | const generated_key = RNCryptor.GenerateKey(Buffer.from(spec.password), Buffer.from(spec.salt_hex, 'hex')); 47 | 48 | expect(spec.key_hex).to.equal(generated_key.toString('hex')); 49 | }); 50 | }); 51 | } else if (key === 'key') { 52 | specs[key].forEach(function (spec) { 53 | it(spec.title, function () { 54 | const cipher_text = RNCryptor.EncryptWithArbitraryKeys( 55 | Buffer.from(spec.plaintext_hex, 'hex'), 56 | Buffer.from(spec.enc_key_hex, 'hex'), 57 | Buffer.from(spec.hmac_key_hex, 'hex'), 58 | Buffer.from(spec.iv_hex, 'hex'), 59 | parseInt(spec.version) 60 | ); 61 | 62 | expect(spec.ciphertext_hex).to.equal(Buffer.from(cipher_text, 'base64').toString('hex')); 63 | }); 64 | }); 65 | } else if (key === 'password') { 66 | specs[key].forEach(function (spec) { 67 | it(spec.title, function () { 68 | const cipher_text = RNCryptor.EncryptWithArbitrarySalts( 69 | Buffer.from(spec.plaintext_hex, 'hex'), 70 | Buffer.from(spec.password), 71 | Buffer.from(spec.enc_salt_hex, 'hex'), 72 | Buffer.from(spec.hmac_salt_hex, 'hex'), 73 | Buffer.from(spec.iv_hex, 'hex'), 74 | parseInt(spec.version) 75 | ); 76 | 77 | expect(spec.ciphertext_hex).to.equal(Buffer.from(cipher_text, 'base64').toString('hex')); 78 | }); 79 | }); 80 | } 81 | }; 82 | }(key)); 83 | } 84 | }).call(undefined, undefined); 85 | -------------------------------------------------------------------------------- /test/title.js: -------------------------------------------------------------------------------- 1 | String.prototype.title = function () { 2 | return this.charAt(0).toUpperCase() + this.slice(1); 3 | }; 4 | 5 | module.exports = String; 6 | --------------------------------------------------------------------------------