diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 53ca796d..8b0ed081 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -79,6 +79,8 @@ "RC2 Decrypt", "RC4", "RC4 Drop", + "Salsa20 Encrypt", + "Salsa20 Decrypt", "ROT13", "ROT47", "XOR", diff --git a/src/core/lib/Salsa20.mjs b/src/core/lib/Salsa20.mjs new file mode 100644 index 00000000..59d6005b --- /dev/null +++ b/src/core/lib/Salsa20.mjs @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2017, Bubelich Mykola + * https://www.bubelich.com + * + * (。◕‿‿◕。) + * + * Modified by cbeuw (Andy Wang) + * + * 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. + * + * General information + * Salsa20 is a stream cipher submitted to eSTREAM by Daniel J. Bernstein. + * It is built on a pseudorandom function based on add-rotate-xor (ARX) operations — 32-bit addition, + * bitwise addition (XOR) and rotation operations. Salsa20 maps a 256-bit key, a 64-bit nonce, + * and a 64-bit stream position to a 512-bit block of the key stream (a version with a 128-bit key also exists). + * This gives Salsa20 the unusual advantage that the user can efficiently seek to any position in the key + * stream in constant time. It offers speeds of around 4–14 cycles per byte in software on modern x86 processors, + * and reasonable hardware performance. It is not patented, and Bernstein has written several + * public domain implementations optimized for common architectures. + */ + +/** + * Construct SalSa20 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 Salsa20 = function (key, nonce) { + if (key.length !== 16 && key.length !== 32) { + throw new Error("Key should be a 16 or 32 byte array!"); + } + + if (nonce.length !== 8) { + throw new Error("Nonce should be 8 byte array!"); + } + + this.rounds = 20; + const sigma = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; + const tau = [0x61707865, 0x3120646e, 0x79622d36, 0x6b206574]; + + let constant, k; + if (key.length === 32) { + constant = sigma; + k = key; + } else { + constant = tau; + k = key.concat(k); + } + + this.param = [ + // Constant + constant[0], + // Key + _get32(k, 0), + _get32(k, 4), + _get32(k, 8), + _get32(k, 12), + constant[1], + // Nonce + _get32(nonce, 0), + _get32(nonce, 4), + // Counter + 0, + 0, + // Constant + constant[2], + // Key + _get32(k, 16), + _get32(k, 20), + _get32(k, 24), + _get32(k, 28), + // Const + constant[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 + */ +Salsa20.prototype._update = function (data) { + /* + if (data.length === 0) { + throw new Error("Data should be type of bytes (Uint8Array) and not empty!"); + } + */ + + 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._salsa(); + this._counterIncrement(); + this.byteCounter = 0; + } + + output[i] = data[i] ^ this.block[this.byteCounter++]; + } + + return output; +}; +/** + * Encrypt data with key and nonce + * + * @param {Uint8Array} data + * @return {Uint8Array} + */ +Salsa20.prototype.encrypt = function (data) { + return this._update(data); +}; + +/** + * Decrypt data with key and nonce + * + * @param {Uint8Array} data + * @return {Uint8Array} + */ +Salsa20.prototype.decrypt = function (data) { + return this._update(data); +}; + +Salsa20.prototype._counterIncrement = function () { + // Max possible blocks is 2^64 + this.param[8] = (this.param[8] + 1) >>> 0; + if (this.param[8] === 0) { + this.param[9] = (this.param[9] + 1) >>> 0; + } +}; + +Salsa20.prototype._salsa = 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[b] ^= _rotl(mix[a] + mix[d], 7) >>> 0; + mix[c] ^= _rotl(mix[b] + mix[a], 9) >>> 0; + mix[d] ^= _rotl(mix[c] + mix[b], 13) >>> 0; + mix[a] ^= _rotl(mix[d] + mix[c], 18) >>> 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(5, 9, 13, 1); + qr(10, 14, 2, 6); + qr(15, 3, 7, 11); + qr(0, 1, 2, 3); + qr(5, 6, 7, 4); + qr(10, 11, 8, 9); + qr(15, 12, 13, 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 Salsa20 +; diff --git a/src/core/operations/Salsa20Decrypt.mjs b/src/core/operations/Salsa20Decrypt.mjs new file mode 100644 index 00000000..1c23ab36 --- /dev/null +++ b/src/core/operations/Salsa20Decrypt.mjs @@ -0,0 +1,90 @@ +/** + * @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 Salsa20 from "../lib/Salsa20"; + +/** + * Salsa20 Decrypt operation + */ +class Salsa20Decrypt extends Operation { + + /** + * Salsa20Decrypt constructor + */ + constructor() { + super(); + + this.name = "Salsa20 Decrypt"; + this.module = "Crypto"; + this.description = "Salsa20 is a stream cipher developed by Daniel Bernstein in 2005.

Key: Key length should be 16 or 32 bytes (128 or 256 bits).

Nonce: The one-time nonce should be 8 bytes long."; + this.infoURL = "https://en.wikipedia.org/wiki/Salsa20"; + 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": "Input", + "type": "option", + "value": ["Hex", "Raw"] + }, + { + "name": "Output", + "type": "option", + "value": ["Raw", "Hex"] + } + ]; + } + + /** + * @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), + inputType = args[2], + outputType = args[3]; + + if (key.length !== 16 && key.length !== 32) { + throw new OperationError(`Invalid key length: ${key.length} bytes + +Salsa20 requires a key length of either 16 bytes or 32 bytes`); + } + if (nonce.length !== 8) { + throw new OperationError(`Invalid nonce length: ${nonce.length} bytes + +Salsa20 requires a nonce length of 8 bytes`); + } + input = Utils.convertToByteArray(input, inputType); + + const cipher = new Salsa20(key, nonce); + const output = cipher.decrypt(input); + + if (outputType === "Hex") { + return Buffer.from(output).toString("hex"); + } else { + return Buffer.from(output).toString(); + } + } + +} + +export default Salsa20Decrypt; diff --git a/src/core/operations/Salsa20Encrypt.mjs b/src/core/operations/Salsa20Encrypt.mjs new file mode 100644 index 00000000..18b1ce92 --- /dev/null +++ b/src/core/operations/Salsa20Encrypt.mjs @@ -0,0 +1,89 @@ +/** + * @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 Salsa20 from "../lib/Salsa20"; + +/** + * Salsa20 Encrypt operation + */ +class Salsa20Encrypt extends Operation { + + /** + * Salsa20Encrypt constructor + */ + constructor() { + super(); + + this.name = "Salsa20 Encrypt"; + this.module = "Crypto"; + this.description = "Salsa20 is a stream cipher developed by Daniel Bernstein in 2005.

Key: Key length should be 16 or 32 bytes (128 or 256 bits).

Nonce: The one-time nonce should be 8 bytes long."; + this.infoURL = "https://en.wikipedia.org/wiki/Salsa20"; + 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": "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), + inputType = args[2], + outputType = args[3]; + + if (key.length !== 16 && key.length !== 32) { + throw new OperationError(`Invalid key length: ${key.length} bytes + +Salsa20 requires a key length of either 16 bytes or 32 bytes`); + } + if (nonce.length !== 8) { + throw new OperationError(`Invalid nonce length: ${nonce.length} bytes + +Salsa20 requires a nonce length of 8 bytes`); + } + input = Utils.convertToByteArray(input, inputType); + + const cipher = new Salsa20(key, nonce); + const output = cipher.encrypt(input); + + if (outputType === "Hex") { + return Buffer.from(output).toString("hex"); + } else { + return Buffer.from(output).toString(); + } + } +} + +export default Salsa20Encrypt;