js-chacha20
Version:
Pure JavaScript ChaCha20 stream cipher
258 lines (230 loc) • 7.62 kB
JavaScript
'use strict'
/*
* Copyright (c) 2017, Bubelich Mykola
* https, 0x//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 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
}