diff --git a/CHANGELOG.md b/CHANGELOG.md index e873a53..d82e7eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Changelog -## 1.3.3-r1 +## 1.4.0-r1 +- Switched packet encryption to XChaCha20-Poly1305 +- Allow external XChaCha20-Poly1305 encryption/decryption - Support writing base64 string data -- Allow external ChaCha20 encryption/decryption - Renamed 'delete/delete_file' to 'remove/remove_file' ## 1.3.1-r3 diff --git a/package.json b/package.json index cc1e866..bf32570 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blockstorage/repertory-js", - "version": "1.3.3-r1", + "version": "1.4.0-r1", "description": "A Node.js module for interfacing with Repertory's remote mount API", "author": "scott.e.graves@protonmail.com", "license": "MIT", @@ -41,6 +41,7 @@ "prepublish": "rollup -c --silent && ./fixup" }, "dependencies": { + "@stablelib/xchacha20poly1305": "^1.0.1", "int64-buffer": "^1.0.0", "socket-pool": "^1.2.3", "text-encoding": "^0.7.0", diff --git a/src/__tests__/packet.test.js b/src/__tests__/packet.test.js index d81da9f..bf138ee 100644 --- a/src/__tests__/packet.test.js +++ b/src/__tests__/packet.test.js @@ -7,3 +7,13 @@ test('can construct a packet', () => { expect(p.buffer).toBeNull(); expect(p.decode_offset).toEqual(0); }); + +test('can encrypt and decrypt a packet', async () => { + console.log('testing'); + const p = new packet('my password'); + p.encode_utf8('moose'); + await p.encrypt(); + await p.decrypt(); + const str = p.decode_utf8(); + expect(str).toEqual('moose'); +}); diff --git a/src/networking/packet.js b/src/networking/packet.js index 273daf5..c294450 100644 --- a/src/networking/packet.js +++ b/src/networking/packet.js @@ -3,7 +3,6 @@ import { Int64BE, Uint64BE } from 'int64-buffer'; import crypto from 'crypto'; import { TextEncoder } from 'text-encoding'; import { getCustomEncryption } from '../utils/constants'; - import { be_ui8_array_to_i16, be_ui8_array_to_i32, @@ -18,14 +17,13 @@ import { ui8_array_to_ui8, ui8_to_ui8_array, } from '../utils/byte_order'; -import JSChaCha20 from '../utils/jschacha20'; +import {XChaCha20Poly1305} from '@stablelib/xchacha20poly1305'; export default class packet { constructor(token) { this.token = token; } - static HEADER_STRING = 'repertory'; static HEADER = new TextEncoder().encode(this.HEADER_STRING); buffer = null; @@ -164,7 +162,8 @@ export default class packet { hash = hash.update(new TextEncoder().encode(this.token)); const key = Uint8Array.from(hash.digest()); - const nonce = this.buffer.slice(0, 12); + const nonce = this.buffer.slice(0, 24); + const mac = this.buffer.slice(24, 16); const customEncryption = getCustomEncryption(); if (customEncryption) { @@ -172,23 +171,18 @@ export default class packet { await customEncryption.decrypt( Buffer.from(key).toString('base64'), Buffer.from(nonce).toString('base64'), - Buffer.from(this.buffer.slice(12)).toString('base64') + Buffer.from(mac).toString('base64'), + Buffer.from(this.buffer.slice(40)).toString('base64') ), 'base64' ); } else { + const aad = ui32_to_be_ui8_array(this.buffer.length); this.buffer = Buffer.from( - new JSChaCha20(key, nonce, 0).decrypt(this.buffer.slice(12)) + new XChaCha20Poly1305(key).open(nonce, this.buffer.slice(24), aad), ); } - this.decode_offset = packet.HEADER.length; - - const header = this.buffer.slice(0, 9); - if (header.toString() !== packet.HEADER_STRING) { - return Promise.reject(new Error('Header does not match')); - } - this.buffer = new Uint8Array(this.buffer); return this.buffer; } catch (e) { @@ -284,13 +278,12 @@ export default class packet { encrypt = async (nonce) => { try { - this.push_buffer(packet.HEADER); let hash = crypto.createHash('sha256'); hash = hash.update(new TextEncoder().encode(this.token)); const key = Uint8Array.from(hash.digest()); if (!nonce) { - nonce = Uint8Array.from(randomBytes(12)); + nonce = Uint8Array.from(randomBytes(24)); } const customEncryption = getCustomEncryption(); @@ -306,9 +299,10 @@ export default class packet { ) ); } else { - this.buffer = new JSChaCha20(key, nonce, 0).encrypt(this.buffer); + const aad = ui32_to_be_ui8_array(this.buffer.length + 40) + this.buffer = new XChaCha20Poly1305(key).seal(nonce, this.buffer, aad); + this.push_buffer(nonce); } - this.push_buffer(nonce); return this.buffer; } catch (e) { diff --git a/src/utils/jschacha20.js b/src/utils/jschacha20.js deleted file mode 100644 index 1ea1933..0000000 --- a/src/utils/jschacha20.js +++ /dev/null @@ -1,322 +0,0 @@ -'use strict'; -/* - * Copyright (c) 2017, Bubelich Mykola - * https://www.bubelich.com - * - * (。◕‿‿◕。) - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met, 0x - * - * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * ChaCha20 is a stream cipher designed by D. J. Bernstein. - * It is a refinement of the Salsa20 algorithm, and it uses a 256-bit key. - * - * ChaCha20 successively calls the ChaCha20 block function, with the same key and nonce, and with successively increasing block counter parameters. - * ChaCha20 then serializes the resulting state by writing the numbers in little-endian order, creating a keystream block. - * - * Concatenating the keystream blocks from the successive blocks forms a keystream. - * The ChaCha20 function then performs an XOR of this keystream with the plaintext. - * Alternatively, each keystream block can be XORed with a plaintext block before proceeding to create_or_open the next block, saving some memory. - * There is no requirement for the plaintext to be an integral multiple of 512 bits. If there is extra keystream from the last block, it is discarded. - * - * The inputs to ChaCha20 are - * - 256-bit key - * - 32-bit initial counter - * - 96-bit nonce. In some protocols, this is known as the Initialization Vector - * - Arbitrary-length plaintext - * - * Implementation derived from chacha-ref.c version 20080118 - * See for details, 0x http, 0x//cr.yp.to/chacha/chacha-20080128.pdf - */ - -/** - * - * @param {Uint8Array} key - * @param {Uint8Array} nonce - * @param {number} counter - * @throws {Error} - * - * @constructor - */ -var JSChaCha20 = function (key, nonce, counter) { - if (typeof counter === 'undefined') { - counter = 0; - } - - if (!(key instanceof Uint8Array) || key.length !== 32) { - throw new Error('Key should be 32 byte array!'); - } - - if (!(nonce instanceof Uint8Array) || nonce.length !== 12) { - throw new Error('Nonce should be 12 byte array!'); - } - - this._rounds = 20; - // Constants - this._sigma = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; - - // param construction - this._param = [ - this._sigma[0], - this._sigma[1], - this._sigma[2], - this._sigma[3], - // key - this._get32(key, 0), - this._get32(key, 4), - this._get32(key, 8), - this._get32(key, 12), - this._get32(key, 16), - this._get32(key, 20), - this._get32(key, 24), - this._get32(key, 28), - // counter - counter, - // nonce - this._get32(nonce, 0), - this._get32(nonce, 4), - this._get32(nonce, 8), - ]; - - // init 64 byte keystream block // - this._keystream = [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ]; - - // internal byte counter // - this._byteCounter = 0; -}; - -JSChaCha20.prototype._chacha = function () { - var mix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - var i = 0; - var b = 0; - - // copy param array to mix // - for (i = 0; i < 16; i++) { - mix[i] = this._param[i]; - } - - // mix rounds // - for (i = 0; i < this._rounds; i += 2) { - this._quarterround(mix, 0, 4, 8, 12); - this._quarterround(mix, 1, 5, 9, 13); - this._quarterround(mix, 2, 6, 10, 14); - this._quarterround(mix, 3, 7, 11, 15); - - this._quarterround(mix, 0, 5, 10, 15); - this._quarterround(mix, 1, 6, 11, 12); - this._quarterround(mix, 2, 7, 8, 13); - this._quarterround(mix, 3, 4, 9, 14); - } - - for (i = 0; i < 16; i++) { - // add - mix[i] += this._param[i]; - - // store keystream - this._keystream[b++] = mix[i] & 0xff; - this._keystream[b++] = (mix[i] >>> 8) & 0xff; - this._keystream[b++] = (mix[i] >>> 16) & 0xff; - this._keystream[b++] = (mix[i] >>> 24) & 0xff; - } -}; - -/** - * The basic operation of the ChaCha algorithm is the quarter round. - * It operates on four 32-bit unsigned integers, denoted a, b, c, and d. - * - * @param {Array} output - * @param {number} a - * @param {number} b - * @param {number} c - * @param {number} d - * @private - */ -JSChaCha20.prototype._quarterround = function (output, a, b, c, d) { - output[d] = this._rotl(output[d] ^ (output[a] += output[b]), 16); - output[b] = this._rotl(output[b] ^ (output[c] += output[d]), 12); - output[d] = this._rotl(output[d] ^ (output[a] += output[b]), 8); - output[b] = this._rotl(output[b] ^ (output[c] += output[d]), 7); - - // JavaScript hack to make UINT32 :) // - output[a] >>>= 0; - output[b] >>>= 0; - output[c] >>>= 0; - output[d] >>>= 0; -}; - -/** - * Little-endian to uint 32 bytes - * - * @param {Uint8Array|[number]} data - * @param {number} index - * @return {number} - * @private - */ -JSChaCha20.prototype._get32 = function (data, index) { - return ( - data[index++] ^ - (data[index++] << 8) ^ - (data[index++] << 16) ^ - (data[index] << 24) - ); -}; - -/** - * Cyclic left rotation - * - * @param {number} data - * @param {number} shift - * @return {number} - * @private - */ -JSChaCha20.prototype._rotl = function (data, shift) { - return (data << shift) | (data >>> (32 - shift)); -}; - -/** - * Encrypt data with key and nonce - * - * @param {Uint8Array} data - * @return {Uint8Array} - */ -JSChaCha20.prototype.encrypt = function (data) { - return this._update(data); -}; - -/** - * Decrypt data with key and nonce - * - * @param {Uint8Array} data - * @return {Uint8Array} - */ -JSChaCha20.prototype.decrypt = function (data) { - return this._update(data); -}; - -/** - * Encrypt or Decrypt data with key and nonce - * - * @param {Uint8Array} data - * @return {Uint8Array} - * @private - */ -JSChaCha20.prototype._update = function (data) { - if (!(data instanceof Uint8Array) || data.length === 0) { - throw new Error('Data should be type of bytes (Uint8Array) and not empty!'); - } - - var output = new Uint8Array(data.length); - - // core function, build block and xor with input data // - for (var i = 0; i < data.length; i++) { - if (this._byteCounter === 0 || this._byteCounter === 64) { - // generate new block // - - this._chacha(); - // counter increment // - this._param[12]++; - - // reset internal counter // - this._byteCounter = 0; - } - - output[i] = data[i] ^ this._keystream[this._byteCounter++]; - } - - return output; -}; - -// EXPORT // -if (typeof module !== 'undefined' && module.exports) { - module.exports = JSChaCha20; -}