Add Chacha20-Poly1305 encryption

This commit is contained in:
Andy Wang 2020-01-13 23:49:35 +00:00
parent ea4fbb7c2f
commit 4b90743ba8
5 changed files with 631 additions and 0 deletions

View file

@ -71,6 +71,7 @@
"AES Decrypt",
"Blowfish Encrypt",
"Blowfish Decrypt",
"Chacha20-Poly1305 Encrypt",
"DES Encrypt",
"DES Decrypt",
"Triple DES Encrypt",

228
src/core/lib/Chacha20.mjs Normal file
View file

@ -0,0 +1,228 @@
// Implemented by cbeuw (Andy Wang) based on Bubelich Myola's implementation of Salsa20
// from https://github.com/thesimj/js-salsa20
/*
* 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:
*
* 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.
*/
/**
* Construct Chacha20 instance with key and nonce
* Key should be Uint8Array with 32 bytes
* None should be Uint8Array with 8 bytes
*
*
* @throws {Error}
* @param {[number]} key
* @param {[number]} nonce
*/
const Chacha20 = function (key, nonce) {
if (key.length !== 32) {
throw new Error("Key should be 32 byte array!");
}
this.nonceLen = nonce.length;
if (this.nonceLen !== 8 && this.nonceLen !== 12) {
throw new Error("Nonce should be an 8 or 12 byte array!");
}
this.rounds = 20;
this.sigma = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574];
let ctrAndNonce = [];
if (this.nonceLen === 8) {
ctrAndNonce = [0, 0, _get32(nonce, 0), _get32(nonce, 4)];
} else {
ctrAndNonce = [0, _get32(nonce, 0), _get32(nonce, 4), _get32(nonce, 8)];
}
this.param = [
// Constant
this.sigma[0],
this.sigma[1],
this.sigma[2],
this.sigma[3],
// Key
_get32(key, 0),
_get32(key, 4),
_get32(key, 8),
_get32(key, 12),
_get32(key, 16),
_get32(key, 20),
_get32(key, 24),
_get32(key, 28),
// Counter and Nonce
ctrAndNonce[0],
ctrAndNonce[1],
ctrAndNonce[2],
ctrAndNonce[3],
];
// init block 64 bytes //
this.block = [
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;
};
/**
* Encrypt or Decrypt data with key and nonce
*
* @param {[number]} data
* @return {Uint8Array}
* @private
*/
Chacha20.prototype._update = function (data) {
const output = new Uint8Array(data.length);
// core function, build block and xor with input data //
for (let i = 0; i < data.length; i++) {
if (this.byteCounter === 0 || this.byteCounter === 64) {
this._chacha();
this._counterIncrement();
this.byteCounter = 0;
}
output[i] = data[i] ^ this.block[this.byteCounter++];
}
return output;
};
/**
* Encrypt data with key and nonce
*
* @param {[number]} data
* @return {Uint8Array}
*/
Chacha20.prototype.encrypt = function (data) {
return this._update(data);
};
/**
* Decrypt data with key and nonce
*
* @param {[number]} data
* @return {Uint8Array}
*/
Chacha20.prototype.decrypt = function (data) {
return this._update(data);
};
Chacha20.prototype._counterIncrement = function () {
// Max possible blocks is 2^64 with 8 byte nonce or 2^32 with 12 byte nonce
this.param[12] = (this.param[12] + 1) >>> 0;
if (this.nonceLen === 8 && this.param[12] === 0) {
this.param[13] = (this.param[13] + 1) >>> 0;
}
};
Chacha20.prototype._chacha = function () {
const mix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let i = 0;
let b = 0;
const qr = function (a, b, c, d) {
mix[a] = (mix[a] + mix[b]) >>> 0;
mix[d] ^= mix[a];
mix[d] = _rotl(mix[d], 16) >>> 0;
mix[c] = (mix[c] + mix[d]) >>> 0;
mix[b] ^= mix[c];
mix[b] = _rotl(mix[b], 12) >>> 0;
mix[a] = (mix[a] + mix[b]) >>> 0;
mix[d] ^= mix[a];
mix[d] = _rotl(mix[d], 8) >>> 0;
mix[c] = (mix[c] + mix[d]) >>> 0;
mix[b] ^= mix[c];
mix[b] = _rotl(mix[b], 7) >>> 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) {
qr(0, 4, 8, 12);
qr(1, 5, 9, 13);
qr(2, 6, 10, 14);
qr(3, 7, 11, 15);
qr(0, 5, 10, 15);
qr(1, 6, 11, 12);
qr(2, 7, 8, 13);
qr(3, 4, 9, 14);
}
for (i = 0; i < 16; i++) {
// add
mix[i] += this.param[i];
// store
this.block[b++] = mix[i] & 0xFF;
this.block[b++] = (mix[i] >>> 8) & 0xFF;
this.block[b++] = (mix[i] >>> 16) & 0xFF;
this.block[b++] = (mix[i] >>> 24) & 0xFF;
}
};
/**
* Little-endian to uint 32 bytes
*
* @param {Uint8Array|[number]} data
* @param {number} index
* @return {number}
* @private
*/
const _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
*/
const _rotl = function (data, shift) {
return ((data << shift) | (data >>> (32 - shift)));
};
export default Chacha20;

View file

@ -0,0 +1,78 @@
import Poly1305 from "./Poly1305.mjs";
import Chacha20 from "./Chacha20.mjs";
// https://github.com/devi/chacha20poly1305/blob/master/chacha20poly1305.js
// Written in 2014 by Devi Mandiri. Public domain.
// Refactored by cbeuw (Andy Wang)
const Chacha20Poly1305 = function (key, nonce) {
this.chachaKey = key;
this.nonce = nonce;
this.chacha20 = new Chacha20(key, nonce);
this.polykey = this._genPolyKey(key, nonce);
this.poly1305 = new Poly1305(this.polykey);
};
const _constructAEAD = function (data, ciphertext) {
const dlen = data.length;
const clen = ciphertext.length;
const dpad = dlen % 16;
const cpad = clen % 16;
const m = [];
let i;
for (i = 0; i < dlen; i++) m.push(data[i]);
if (dpad !== 0) {
for (i = (16 - dpad); i--;) m.push(0);
}
for (i = 0; i < clen; i++) m.push(ciphertext[i]);
if (cpad !== 0) {
for (i = (16 - cpad); i--;) m.push(0);
}
const lens = new Buffer(16);
lens.fill(0);
lens.writeUInt32LE(dlen, 0);
lens.writeUInt32LE(clen, 8);
m.push(...lens);
return m;
};
Chacha20Poly1305.prototype._genPolyKey = function() {
const nullBlock = new Uint8Array(Array(64).fill(0));
const keystream = this.chacha20.encrypt(nullBlock);
return keystream.slice(0, 32);
};
Chacha20Poly1305.prototype.seal = function(plaintext, data) {
const ciphertext = this.chacha20.encrypt(plaintext);
this.poly1305.update(_constructAEAD(data, ciphertext));
return [ciphertext, this.poly1305.finish()];
};
const _bufEqual = function(mac1, mac2) {
let dif = 0;
for (let i = 0; i < 16; i++) {
dif |= (mac1[i] ^ mac2[i]);
}
dif = (dif - 1) >>> 31;
return (dif & 1) === 1;
};
Chacha20Poly1305.prototype.open = function(ciphertext, data, mac) {
const plaintext = this.chacha20.decrypt(ciphertext);
this.poly1305.update(_constructAEAD(data, ciphertext));
const tag = this.poly1305.finish();
if (!_bufEqual(tag, mac)) return false;
return plaintext;
};
export default Chacha20Poly1305
;

209
src/core/lib/Poly1305.mjs Normal file
View file

@ -0,0 +1,209 @@
// https://github.com/devi/chacha20poly1305/blob/master/chacha20poly1305.js
// Written in 2014 by Devi Mandiri. Public domain.
//
// Implementation derived from poly1305-donna-16.h
// See for details: https://github.com/floodyberry/poly1305-donna
//
// Refactored by cbeuw (Andy Wang)
const Poly1305 = function(key) {
if (key.length !== 32) {
throw new Error("Key should be 32 byte array!");
}
this.buffer = new Uint8Array(16);
this.leftover = 0;
this.r = new Uint16Array(10);
this.h = new Uint16Array(10);
this.pad = new Uint16Array(8);
this.finished = 0;
const t = new Uint16Array(8);
let i;
for (i = 8; i--;) t[i] = U8TO16_LE(key, i*2);
this.r[0] = t[0] & 0x1fff;
this.r[1] = ((t[0] >>> 13) | (t[1] << 3)) & 0x1fff;
this.r[2] = ((t[1] >>> 10) | (t[2] << 6)) & 0x1f03;
this.r[3] = ((t[2] >>> 7) | (t[3] << 9)) & 0x1fff;
this.r[4] = ((t[3] >>> 4) | (t[4] << 12)) & 0x00ff;
this.r[5] = (t[4] >>> 1) & 0x1ffe;
this.r[6] = ((t[4] >>> 14) | (t[5] << 2)) & 0x1fff;
this.r[7] = ((t[5] >>> 11) | (t[6] << 5)) & 0x1f81;
this.r[8] = ((t[6] >>> 8) | (t[7] << 8)) & 0x1fff;
this.r[9] = (t[7] >>> 5) & 0x007f;
for (i = 8; i--;) {
this.h[i] = 0;
this.pad[i] = U8TO16_LE(key, 16+(2*i));
}
this.h[8] = 0;
this.h[9] = 0;
this.leftover = 0;
this.finished = 0;
};
const U8TO16_LE = function (p, pos) {
return (p[pos] & 0xff) | ((p[pos+1] & 0xff) << 8);
};
const U16TO8_LE = function (p, pos, v) {
p[pos] = v;
p[pos+1] = v >>> 8;
};
Poly1305.prototype.blocks = function(m, mpos, bytes) {
const hibit = this.finished ? 0 : (1 << 11);
const t = new Uint16Array(8);
const d = new Uint32Array(10);
let c = 0, i = 0, j = 0;
while (bytes >= 16) {
for (i = 8; i--;) t[i] = U8TO16_LE(m, i*2+mpos);
this.h[0] += t[0] & 0x1fff;
this.h[1] += ((t[0] >>> 13) | (t[1] << 3)) & 0x1fff;
this.h[2] += ((t[1] >>> 10) | (t[2] << 6)) & 0x1fff;
this.h[3] += ((t[2] >>> 7) | (t[3] << 9)) & 0x1fff;
this.h[4] += ((t[3] >>> 4) | (t[4] << 12)) & 0x1fff;
this.h[5] += (t[4] >>> 1) & 0x1fff;
this.h[6] += ((t[4] >>> 14) | (t[5] << 2)) & 0x1fff;
this.h[7] += ((t[5] >>> 11) | (t[6] << 5)) & 0x1fff;
this.h[8] += ((t[6] >>> 8) | (t[7] << 8)) & 0x1fff;
this.h[9] += (t[7] >>> 5) | hibit;
for (i = 0, c = 0; i < 10; i++) {
d[i] = c;
for (j = 0; j < 10; j++) {
d[i] += (this.h[j] & 0xffffffff) * ((j <= i) ? this.r[i-j] : (5 * this.r[i+10-j]));
if (j === 4) {
c = (d[i] >>> 13);
d[i] &= 0x1fff;
}
}
c += (d[i] >>> 13);
d[i] &= 0x1fff;
}
c = ((c << 2) + c);
c += d[0];
d[0] = ((c & 0xffff) & 0x1fff);
c = (c >>> 13);
d[1] += c;
for (i = 10; i--;) this.h[i] = d[i];
mpos += 16;
bytes -= 16;
}
};
Poly1305.prototype.update = function(m) {
let bytes = m.length;
let want = 0, i = 0, mpos = 0;
if (this.leftover) {
want = 16 - this.leftover;
if (want > bytes)
want = bytes;
for (i = want; i--;) {
this.buffer[this.leftover+i] = m[i+mpos];
}
bytes -= want;
mpos += want;
this.leftover += want;
if (this.leftover < 16)
return;
this.blocks(this.buffer, 0, 16);
this.leftover = 0;
}
if (bytes >= 16) {
want = (bytes & ~(16 - 1));
this.blocks(m, mpos, want);
mpos += want;
bytes -= want;
}
if (bytes) {
for (i = bytes; i--;) {
this.buffer[this.leftover+i] = m[i+mpos];
}
this.leftover += bytes;
}
};
Poly1305.prototype.finish = function() {
const mac = new Uint8Array(16);
const g = new Uint16Array(10);
let c = 0, mask = 0, f = 0, i = 0;
if (this.leftover) {
i = this.leftover;
this.buffer[i++] = 1;
for (; i < 16; i++) {
this.buffer[i] = 0;
}
this.finished = 1;
this.blocks(this.buffer, 0, 16);
}
c = this.h[1] >>> 13;
this.h[1] &= 0x1fff;
for (i = 2; i < 10; i++) {
this.h[i] += c;
c = this.h[i] >>> 13;
this.h[i] &= 0x1fff;
}
this.h[0] += (c * 5);
c = this.h[0] >>> 13;
this.h[0] &= 0x1fff;
this.h[1] += c;
c = this.h[1] >>> 13;
this.h[1] &= 0x1fff;
this.h[2] += c;
g[0] = this.h[0] + 5;
c = g[0] >>> 13;
g[0] &= 0x1fff;
for (i = 1; i < 10; i++) {
g[i] = this.h[i] + c;
c = g[i] >>> 13;
g[i] &= 0x1fff;
}
g[9] -= (1 << 13);
mask = (g[9] >>> 15) - 1;
for (i = 10; i--;) g[i] &= mask;
mask = ~mask;
for (i = 10; i--;) {
this.h[i] = (this.h[i] & mask) | g[i];
}
this.h[0] = (this.h[0]) | (this.h[1] << 13);
this.h[1] = (this.h[1] >> 3) | (this.h[2] << 10);
this.h[2] = (this.h[2] >> 6) | (this.h[3] << 7);
this.h[3] = (this.h[3] >> 9) | (this.h[4] << 4);
this.h[4] = (this.h[4] >> 12) | (this.h[5] << 1) | (this.h[6] << 14);
this.h[5] = (this.h[6] >> 2) | (this.h[7] << 11);
this.h[6] = (this.h[7] >> 5) | (this.h[8] << 8);
this.h[7] = (this.h[8] >> 8) | (this.h[9] << 5);
f = (this.h[0] & 0xffffffff) + this.pad[0];
this.h[0] = f;
for (i = 1; i < 8; i++) {
f = (this.h[i] & 0xffffffff) + this.pad[i] + (f >>> 16);
this.h[i] = f;
}
for (i = 8; i--;) {
U16TO8_LE(mac, i*2, this.h[i]);
this.pad[i] = 0;
}
for (i = 10; i--;) {
this.h[i] = 0;
this.r[i] = 0;
}
return mac;
};
export default Poly1305;

View file

@ -0,0 +1,115 @@
/**
* @author cbeuw [cbeuw.andy@gmail.com]
* @copyright Crown Copyright 2020
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import Chacha20 from "../lib/Chacha20";
import Chacha20Poly1305 from "../lib/Chacha20Poly1305";
/**
* Chacha20-Poly1305 Encrypt operation
*/
class Chacha20Poly1305Encrypt extends Operation {
/**
* Chacha20Poly1305Encrypt constructor
*/
constructor() {
super();
this.name = "Chacha20-Poly1305 Encrypt";
this.module = "Crypto";
this.description = "Chacha20 is a stream cipher developed by Daniel Bernstein based on Salsa20. The cipher and the massage authentication code Poly1305 are defined by RFC8439. Chacha20 and Poly1305 are frequently used together for authenticated encryption, and has been included in TLS 1.3 protocol.<br><br><b>Key:</b> Key length should be 32 bytes (256 bits).<br><br><b>Nonce:</b> The one-time nonce should be 8 or 12 bytes long (64 or 96 bits).";
this.infoURL = "https://tools.ietf.org/html/rfc8439";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Key",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
{
"name": "Nonce",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
{
"name": "Authenticated Encryption",
"type": "boolean",
"value": "true"
},
{
"name": "Additional Authentication Data",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
{
"name": "Input",
"type": "option",
"value": ["Raw", "Hex"]
},
{
"name": "Output",
"type": "option",
"value": ["Hex", "Raw"]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const key = Utils.convertToByteArray(args[0].string, args[0].option),
nonce = Utils.convertToByteArray(args[1].string, args[1].option),
useAEAD = args[2],
aad = Utils.convertToByteArray(args[3].string, args[3].option),
inputType = args[4],
outputType = args[5];
if (key.length !== 32) {
throw new OperationError(`Invalid key length: ${key.length} bytes
Chacha20 requires a key length of 32 bytes`);
}
if (nonce.length !== 8 && nonce.length !== 12) {
throw new OperationError(`Invalid nonce length: ${nonce.length} bytes
Chacha20 requires a nonce length of 8 or 12 bytes`);
}
input = Utils.convertToByteArray(input, inputType);
if (useAEAD) {
const aead = new Chacha20Poly1305(key, nonce);
const ret = aead.seal(input, aad);
if (outputType === "Hex") {
return Buffer.from(ret[0]).toString("hex") + "\n\n" +
"Tag: " + Buffer.from(ret[1]).toString("hex");
} else {
return Buffer.from(ret[0]).toString() + "\n\n" +
"Tag: " + Buffer.from(ret[1]).toString();
}
} else {
const cipher = new Chacha20(key, nonce);
const output = cipher.encrypt(input);
if (outputType === "Hex") {
return Buffer.from(output).toString("hex");
} else {
return Buffer.from(output).toString();
}
}
}
}
export default Chacha20Poly1305Encrypt;