4
\$\begingroup\$

I have been implementing Salsa20 by studying the design and specification. I have made sure to unit test every function along the way and match the outputs against the test vectors in the spec. I am aiming for readability/auditability of the code as a whole rather than raw speed, so I have not followed the C reference implementation approach which appears to inline all the functions and is confusing to follow in my opinion.

I was wondering if I could get some constructive feedback in regards to improving the speed, security or quality of the code?

Obviously JavaScript doesn't have access to the raw memory so a secure erase of sensitive data is ruled out. However I am interested in any suggestions to flush any data after use or make garbage collection happen sooner.

The full code is here on GitHub. The main code is in the salsa20.js file. I have pasted the output of that below too minus some of the function header comments because I've exceeded the 30000 char limit here.

'use strict';

var Salsa20 = {

    // The maximum integer size in JavaScript (9007199254740992)
    maxInteger: Math.pow(2, 53),

    // The core of Salsa20 is a hash function with 64-byte input and 64-byte output
    outputByteLength: 64,

    /**
     * Converts a hex word e.g. 0xc0a8787e or 'c0a8787e' to decimal i.e. 3232266366
     * @param {String} hexWord A hexadecimal word of 32 bits
     * @returns {Number} Returns an integer
     */
    hexWordToDecimal: function(hexWord)
    {
        return parseInt(hexWord, 16);
    },

    /**
     * Converts a decimal or integer to a hexadecimal word of 32 bits e.g. 3232266366 becomes 'c0a8787e'
     * @param {Number} decimalNum
     * @returns {String} Returns a hexadecimal word of length 32 bits (8 hexadecimal chars)
     */
    decimalToHexWord: function(decimalNum)
    {
        // Convert to hexadecimal and pad to 4 bytes (8 hexadecimal chars)
        var hexWord = decimalNum.toString(16);
        var paddedHexWord = this.leftPadding(hexWord, '0', 8);

        return paddedHexWord;
    },

    /**
     * Left pad a string with a certain character to a total number of characters
     * @param {string} inputString The string to be padded
     * @param {string} padCharacter The character that the string should be padded with
     * @param {number} totalCharacters The length of string that's required
     * @returns {string} A string with characters appended to the front of it
     */
    leftPadding: function(inputString, padCharacter, totalCharacters)
    {
        // Convert to string first, or it starts adding numbers instead of concatenating
        inputString = inputString.toString();

        // If the string is already the right length, just return it
        if (!inputString || !padCharacter || inputString.length >= totalCharacters)
        {
            return inputString;
        }

        // Work out how many extra characters we need to add to the string
        var charsToAdd = (totalCharacters - inputString.length) / padCharacter.length;

        // Add padding onto the string
        for (var i = 0; i < charsToAdd; i++)
        {
            inputString = padCharacter + inputString;
        }

        return inputString;
    },

    /**
     * Adds two hexadecimal words of length 32 bits together
     * @param {String} hexWordA A hexadecimal word of 32 bits
     * @param {String} hexWordB A hexadecimal word of 32 bits
     * @returns {String} Returns the resulting hexadecimal word of 32 bits
     */
    sumHexWords: function(hexWordA, hexWordB)
    {
        // Convert words to decimal and store in 32 bit typed array
        var wordArray = new Uint32Array(3);
        wordArray[0] = this.hexWordToDecimal(hexWordA);
        wordArray[1] = this.hexWordToDecimal(hexWordB);

        // Add the two numbers
        wordArray[2] = wordArray[0] + wordArray[1];

        // Return as hex
        return this.decimalToHexWord(wordArray[2]);
    },

    /**
     * Exclusive ORs two hex words together
     * @param {String} hexWordA A hexadecimal word of 32 bits
     * @param {String} hexWordB A hexadecimal word of 32 bits
     * @returns {String} Returns the resulting hexadecimal word of 32 bits
     */
    xorHexWords: function(hexWordA, hexWordB)
    {
        // Convert words to decimal and store in 32 bit typed array
        var wordArray = new Uint32Array(3);
        wordArray[0] = this.hexWordToDecimal(hexWordA);
        wordArray[1] = this.hexWordToDecimal(hexWordB);

        // XOR the two numbers
        wordArray[2] = wordArray[0] ^ wordArray[1];

        // Return as hex
        return this.decimalToHexWord(wordArray[2]);
    },

    /**
     * Left rotates a hexadecimal word to the specified number of bits
     * @param {String} hexWordA A hexadecimal word of 32 bits
     * @param {Number} numBitsToRotate An integer of the number of bits to rotate
     * @returns {String} Returns the resulting hexadecimal word of 32 bits
     */
    leftRotate: function(hexWordA, numBitsToRotate)
    {
        // Convert word to decimal and store in 32 bit typed array
        var wordArray = new Uint32Array(2);
        wordArray[0] = this.hexWordToDecimal(hexWordA);

        // Left rotate the number by the number of bits
        wordArray[1] = (wordArray[0] << numBitsToRotate | (wordArray[0] >>> (32 - numBitsToRotate)));

        // Return as hex
        return this.decimalToHexWord(wordArray[1]);
    },

    /**
     * The quarterround function from Section 3 of the spec
     * @param {Array} yWords An array of four 32 bit hexadecimal words
     * @returns {Array} zWords Returns a modified array of four 32 bit hexadecimal words
     */
    quarterRound: function(yWords)
    {
        var sum = null;
        var leftRotation = null;
        var zWords = [];

        // z1 = y1 ⊕ ((y0 + y3) <<< 7)
        sum = this.sumHexWords(yWords[0], yWords[3]);
        leftRotation = this.leftRotate(sum, 7);
        zWords[1] = this.xorHexWords(yWords[1], leftRotation);

        // z2 = y2 ⊕ ((z1 + y0) <<< 9)
        sum = this.sumHexWords(zWords[1], yWords[0]);
        leftRotation = this.leftRotate(sum, 9);
        zWords[2] = this.xorHexWords(yWords[2], leftRotation);

        // z3 = y3 ⊕ ((z2 + z1) <<< 13)
        sum = this.sumHexWords(zWords[2], zWords[1]);
        leftRotation = this.leftRotate(sum, 13);
        zWords[3] = this.xorHexWords(yWords[3], leftRotation);

        // z0 = y0 ⊕ ((z3 + z2) <<< 18)
        sum = this.sumHexWords(zWords[3], zWords[2]);
        leftRotation = this.leftRotate(sum, 18);
        zWords[0] = this.xorHexWords(yWords[0], leftRotation);

        return zWords;
    },

    /**
     * The rowround function from Section 4 of the spec
     * @param {Array} yWords A 16-word array consisting of 32 bit hexadecimal words
     * @returns {Array} zWords Returns a transformed 16-word array consisting of 32 bit hexadecimal words
     */
    rowRound: function(yWords)
    {
        var zWords = [];
        var transformedWords = [];

        // (z0, z1, z2, z3) = quarterround(y0, y1, y2, y3)
        transformedWords = this.quarterRound([yWords[0], yWords[1], yWords[2], yWords[3]]);
        zWords[0] = transformedWords[0];
        zWords[1] = transformedWords[1];
        zWords[2] = transformedWords[2];
        zWords[3] = transformedWords[3];

        // (z5, z6, z7, z4) = quarterround(y5, y6, y7, y4)
        transformedWords = this.quarterRound([yWords[5], yWords[6], yWords[7], yWords[4]]);
        zWords[5] = transformedWords[0];
        zWords[6] = transformedWords[1];
        zWords[7] = transformedWords[2];
        zWords[4] = transformedWords[3];

        // (z10, z11, z8, z9) = quarterround(y10, y11, y8, y9)
        transformedWords = this.quarterRound([yWords[10], yWords[11], yWords[8], yWords[9]]);
        zWords[10] = transformedWords[0];
        zWords[11] = transformedWords[1];
        zWords[8] = transformedWords[2];
        zWords[9] = transformedWords[3];

        // (z15, z12, z13, z14) = quarterround(y15, y12, y13, y14)
        transformedWords = this.quarterRound([yWords[15], yWords[12], yWords[13], yWords[14]]);
        zWords[15] = transformedWords[0];
        zWords[12] = transformedWords[1];
        zWords[13] = transformedWords[2];
        zWords[14] = transformedWords[3];

        return zWords;
    },

    /**
     * The columnround function from Section 5 of the spec
     * @param {Array} xWords A 16-word array consisting of 32 bit hexadecimal words
     * @returns {Array} yWords Returns a transformed 16-word array consisting of 32 bit hexadecimal words
     */
    columnRound: function(xWords)
    {
        var yWords = [];
        var transformedWords = [];

        // (y0, y4, y8, y12) = quarterround(x0, x4, x8, x12)
        transformedWords = this.quarterRound([xWords[0], xWords[4], xWords[8], xWords[12]]);
        yWords[0] = transformedWords[0];
        yWords[4] = transformedWords[1];
        yWords[8] = transformedWords[2];
        yWords[12] = transformedWords[3];

        // (y5, y9, y13, y1) = quarterround(x5, x9, x13, x1)
        transformedWords = this.quarterRound([xWords[5], xWords[9], xWords[13], xWords[1]]);
        yWords[5] = transformedWords[0];
        yWords[9] = transformedWords[1];
        yWords[13] = transformedWords[2];
        yWords[1] = transformedWords[3];

        // (y10, y14, y2, y6) = quarterround(x10, x14, x2, x6)
        transformedWords = this.quarterRound([xWords[10], xWords[14], xWords[2], xWords[6]]);
        yWords[10] = transformedWords[0];
        yWords[14] = transformedWords[1];
        yWords[2] = transformedWords[2];
        yWords[6] = transformedWords[3];

        // (y15, y3, y7, y11) = quarterround(x15, x3, x7, x11)
        transformedWords = this.quarterRound([xWords[15], xWords[3], xWords[7], xWords[11]]);
        yWords[15] = transformedWords[0];
        yWords[3] = transformedWords[1];
        yWords[7] = transformedWords[2];
        yWords[11] = transformedWords[3];

        return yWords;
    },

    /**
     * The doubleround function from Section 6 of the spec
     * @param {Array} xWords A 16-word array consisting of 32 bit hexadecimal words
     * @returns {Array} xWords Returns a transformed 16-word array consisting of 32 bit hexadecimal words
     */
    doubleRound: function(xWords)
    {
        return this.rowRound(this.columnRound(xWords));
    },

    /**
     * The littleendian function from Section 7 of the spec
     * @param {Array} bytes A 4-byte sequence e.g. [6, 75, 30, 9]
     * @returns {String} Returns a 32 bit hexadecimal word
     */
    littleEndian: function(bytes)
    {
        var wordArray = new Uint32Array(1);

        // Calculate b0 + 2^8 * b1 + 2^16 * b2 + 2^24 * b3  
        wordArray[0] = bytes[0] + (Math.pow(2, 8) * bytes[1]) + (Math.pow(2, 16) * bytes[2]) + (Math.pow(2, 24) * bytes[3]);

        // Convert back to hexadecimal
        return this.decimalToHexWord(wordArray[0]);
    },

    /**
     * The inverse of the littleendian function
     * @param {String} word A 32 bit hexadecimal word
     * @returns {Array} Returns a 4-byte sequence e.g. [6, 75, 30, 9]
     */
    littleEndianInverse: function(word)
    {
        // Convert the word to bytes
        var bytes = this.hexToBytes(word);

        // Perform the littleEndian function on the bytes
        var inverseLittleEndianWord = this.littleEndian(bytes);

        // Return the word back as bytes
        return this.hexToBytes(inverseLittleEndianWord);
    },

    /**
     * Converts a hexadecimal string to a byte array
     * @param {String} hexString A string of hexadecimal symbols
     * @returns {Array} Returns a byte array e.g. [34, 255, 0, 64]
     */
    hexToBytes: function(hexString)
    {
        var byteArray = [];

        for (var i = 0, length = hexString.length; i < length; i += 2)
        {
            var byteHex = hexString.substring(i, i + 2);
            var byte = parseInt(byteHex, 16);

            byteArray.push(byte);
        }

        return byteArray;
    },

    /**
     * Converts a byte array to a hexadecimal string
     * @param {Array} byteArray A byte array e.g. [34, 255, 0, 64]
     * @returns {String} Returns A string of hexadecimal symbols
     */
    bytesToHex: function(byteArray)
    {
        var hexString = '';

        for (var i = 0, length = byteArray.length; i < length; i++)
        {
            // Convert the decimal to a byte, adding padding if necessary
            var hex = byteArray[i].toString(16);
            var hexPadded = this.leftPadding(hex, '0', 2);

            hexString += hexPadded;
        }

        return hexString;
    },

    /**
     * The Salsa20 hash function from Section 8 of the spec
     * @param {Array} xBytes A 64-byte sequence e.g. [211, 159, 13, 115, ...]
     * @returns {Array} zBytes A modified 64-byte sequence
     */
    hash: function(xBytes)
    {
        var xWords = [];
        var resultBytes = [];

        // Convert bytes to littleendian words
        // x0 = littleendian(x[0], x[1], x[2], x[3]),
        // x1 = littleendian(x[4], x[5], x[6], x[7]),
        // ...
        // x15 = littleendian(x[60], x[61], x[62], x[63]).
        for (var i = 0, j = 0; i < this.outputByteLength; i += 4, j++)
        {           
            xWords[j] = this.littleEndian([xBytes[i], xBytes[i + 1], xBytes[i + 2], xBytes[i + 3]]);
        }

        // Set to be input into doubleround multiple times
        var zWords = xWords;

        // Perform doubleround 10 times
        for (var i = 0; i < 10; i++)
        {
            zWords = this.doubleRound(zWords);
        }

        // Perform littleendian −1 (z0 + x0) through to littleendian −1 (z15 + x15)
        for (var i = 0; i < 16; i++)
        {
            // Add the z and x words in the brackets first then perform the inverse littleendian
            var sumWords = this.sumHexWords(zWords[i], xWords[i]);
            var inverseEndianBytes = this.littleEndianInverse(sumWords);

            // Concatenate the bytes to the end of the array
            resultBytes = resultBytes.concat(inverseEndianBytes);
        }

        return resultBytes;
    },

    /**
     * Constants for the 16 byte (128 bit) key
     * τ0 τ1 τ2 τ3 is "expand 16-byte k" in ASCII
     */
    constants16: [
        [101, 120, 112, 97],    // τ0 "expa"
        [110, 100, 32, 49],     // τ1 "nd 1"
        [54, 45, 98, 121],      // τ2 "6-by"
        [116, 101, 32, 107]     // τ3 "te k"
    ],

    /**
     * Constants for the 32 byte (256 bit) key
     * σ0 σ1 σ2 σ3 is "expand 32-byte k" in ASCII
     */
    constants32: [
        [101, 120, 112, 97],    // σ0 "expa"
        [110, 100, 32, 51],     // σ1 "nd 3"
        [50, 45, 98, 121],      // σ2 "2-by"
        [116, 101, 32, 107]     // σ3 "te k"
    ],

    /**
     * The Salsa20 expansion function from Section 9 of the spec
     * @param {Array} nonce A 16 byte sequence for the nonce and counter
     * @param {Array} key0 A 16 byte (128 bit) sequence for the key
     * @param {Array} key1 An optional additional 16 byte (128 bit) sequence to make up a full 256 bit key
     * @returns {Array} Returns a 64 byte sequence which is the expansion of (k, n) into Salsa20 k(n)
     */
    expansion: function(nonce, key0, key1)
    {
        var inputBytes = [];

        // If using a 32 byte (256 bit) key
        if ((key1) && (key1.length !== 0))
        {           
            // Salsa20 k0,k1(n) = Salsa20(σ0, k0, σ1, n, σ2, k1, σ3)
            inputBytes = inputBytes.concat(this.constants32[0], key0, this.constants32[1], nonce, this.constants32[2], key1, this.constants32[3]);
        }
        else {
            // Otherwise use the 16 byte (128 bit key)
            // Salsa20 k(n) = Salsa20(τ0, k, τ1, n, τ2, k, τ3)
            inputBytes = inputBytes.concat(this.constants16[0], key0, this.constants16[1], nonce, this.constants16[2], key0, this.constants16[3]);
        }

        // Perform the hash
        return this.hash(inputBytes);
    },

    /**
     * Converts a nonce or counter to a fixed 8 byte (64 bit) array
     * @param {Number} num An integer e.g. nonce or counter to be converted to fixed 8 byte array
     * @returns {Array} Returns an array of bytes with a total size of 8 bytes
     */
    numToByteArray: function(num)
    {
        // Convert to hex
        var counterHex = num.toString(16);

        // Add padding to make up 64 bits (4 bits per symbol x 16)
        var paddedCounterHex = this.leftPadding(counterHex, '0', 16);

        // Convert to bytes
        return this.hexToBytes(paddedCounterHex);
    },

    /**
     * The Salsa20 encryption function from Section 10 of the spec
     * @param {Array} key A 16-byte sequence for the 128 bit key or 32-byte sequence for the 256 bit key
     * @param {Array} message An arbitrary length byte sequence (less than 2^70) for the plaintext message
     * @param {Array} nonce An 8-byte nonce / unique message number (less than 2^53)
     * @param {Number} counter An optional integer counter (less than 2^53) to start encryption/decryption from, default is usually 0
     * @returns {Array} Returns an array of bytes (the ciphertext message)
     */
    encryption: function(key, message, nonce, counter)
    {
        // Separate the key into two parts for input into the expansion function
        var key0 = key.slice(0, 16);
        var key1 = key.slice(16, 32);

        // Find number of 64 byte keystream blocks to create
        var numBlocksToGenerate = Math.ceil(message.length / this.outputByteLength);

        // Find the stop counter value to exit keystream generation
        var maxCounter = counter + numBlocksToGenerate;

        // Generate the keystream
        for (var keystreamBytes = []; counter < maxCounter; counter++)
        {
            // Add the nonce and counter to form the 16 bytes input into the expansion function
            var counterBytes = this.numToByteArray(counter);
            var counterAndNonceBytes = nonce.concat(counterBytes);

            // Generate 64 byte block keystream
            var expansionBytes = this.expansion(counterAndNonceBytes, key0, key1);

            // Build output         
            keystreamBytes = keystreamBytes.concat(expansionBytes);
        }

        // XOR each byte of the key with the corresponding plaintext byte
        for (var xoredBytes = [], i = 0, length = message.length; i < length; i++)
        {
            // XOR the two bytes together
            var byte = this.xorTwoBytes(keystreamBytes[i], message[i]);

            // Append to output
            xoredBytes.push(byte);
        }

        return xoredBytes;
    },

    /**
     * Exclusive OR two bytes together
     * @param {Number} byteA An integer from 0 - 255 inclusive
     * @param {Number} byteB An integer from 0 - 255 inclusive
     * @returns {Number} Returns an integer from 0 - 255 inclusive
     */
    xorTwoBytes: function(byteA, byteB)
    {
        // Create a typed array which will clamp the result to a single byte within the range of 0 - 255
        var resultByte = new Uint8Array(1);

        // XOR the two bytes
        resultByte[0] = byteA ^ byteB;

        return resultByte[0];
    },

    /**
     * Parses the key input by the user and normalises it to a byte array
     */
    parseKey: function(key)
    {
        try {           
            if ((key instanceof Uint8Array) || (key instanceof Array))
            {
                // Normalise the key to a regular array
                key = Array.prototype.slice.call(key);
            }
            else if (typeof key === 'string')
            {
                // Convert the key from hexadecimal string to byte array
                key = this.hexToBytes(key);
            }
            else {
                throw new Error('Incorrect parameter type for the key, it should be an array of bytes or hex string');
            }

            // Check key length is correct
            if (this.checkKeyLength(key.length))
            {
                // Key should be byte array now
                return key;
            }
            else {
                throw new Error('Incorrect key length, only 16 bytes (128 bits) or 32 bytes (256 bits) accepted');
            }
        }
        catch (exception)
        {
            console.error(exception);
        }
    },

    /**
     * Checks the key is the correct size
     */
    checkKeyLength: function(keyLength)
    {
        // If key is 16 bytes (128 bits) or 32 bytes (256 bits)
        if ((keyLength === 16) || (keyLength === 32))
        {
            return true;
        }

        return false;
    },

    /**
     * Converts a message from a hexadecimal string or UTF-8 to a byte array
     */
    parseMessage: function(message, options)
    {
        try {
            var messageBytes = [];

            // If an array of bytes
            if ((message instanceof Uint8Array) || (message instanceof Array))
            {
                // Convert the typed array to a regular array
                messageBytes = Array.prototype.slice.call(message);
            }
            else if (typeof message === 'string')
            {
                // Check it is a hex string by checking the options property
                if ((typeof options !== 'undefined') && (options.hasOwnProperty('inputTextType')) && (options.inputTextType === 'hex'))
                {
                    // Convert from hexadecimal string
                    messageBytes = this.hexToBytes(message);
                }
                else {
                    // Convert from UTF-8 string
                    messageBytes = this.utf8StringToBytes(message);
                }
            }
            else {
                throw new Error('Incorrect parameter type for the message, it should be a string, hex string or array of bytes');
            }

            // The maximum counter in the spec is 2^64, but JavaScript can't go higher than 2^53 for an integer. Multiply 
            // this max counter by the output bytes of Salsa20 function (64 bytes) which gives 576460752303424000 bytes = 512 PiB
            if (messageBytes.length > (this.maxInteger * this.outputByteLength))
            {
                throw new Error('Should not encrypt more than 512 petabytes of data under a single nonce, try splitting the data and encrypting under separate nonces');
            }

            return messageBytes;
        }
        catch (exception)
        {
            console.error(exception);
        }
    },

    /**
     * Encodes an ASCII/UTF-8 string into an array of bytes
     */
    utf8StringToBytes: function(unicodeString)
    {
        try {
            // Encode all Unicode chars
            var utf8ByteString = unescape(encodeURIComponent(unicodeString));

            for (var i = 0, utf8Bytes = [], length = utf8ByteString.length; i < length; i++)
            {
                // Get the decimal representation of each character
                var decimal = utf8ByteString.charCodeAt(i);

                // Build up the byte array
                utf8Bytes.push(decimal);
            }

            return utf8Bytes;
        }
        catch (exception)
        {
            // Catch exceptions for URIError: malformed URI sequence
            console.error(exception);
        }
    },

    /**
     * Decodes an array of bytes to an ASCII/UTF-8 string
     */
    bytesToUtf8String: function(bytes)
    {       
        // Get the char code for each byte
        for (var i = 0, string = '', length = bytes.length; i < length; i++)
        {
            string += String.fromCharCode(bytes[i]);
        }

        // Decode string back to UTF-8
        return decodeURIComponent(escape(string));
    },

    /**
     * Converts the nonce to a fixed size byte array
     */
    parseNonce: function(nonce)
    {
        try {
            // If an array of bytes
            if (((nonce instanceof Uint8Array) || (nonce instanceof Array)) && (nonce.length === 8))
            {
                // Convert the typed array to a regular array
                return Array.prototype.slice.call(nonce);
            }

            // If a hexadecimal string with 16 hex digits in length
            else if ((typeof nonce === 'string') && (nonce.length === 16))
            {
                return this.hexToBytes(nonce);
            }

            // If a positive integer
            else if ((typeof nonce === 'number') && (nonce % 1 === 0) && (nonce >= 0))
            {
                // Check that it does not exceed integer limit in JavaScript (2^53)
                if (nonce <= this.maxInteger)
                {
                    return this.numToByteArray(nonce);
                }
                else {
                    throw new Error('The nonce size has exceeded the max integer limit of JavaScript (' + this.maxInteger + ')');
                }
            }
            else {
                throw new Error('Incorrect parameter for the nonce, it should be a hex string (16 symbols), array of bytes (8 bytes) or a positive integer');
            }
        }
        catch (exception)
        {           
            console.error(exception);
        }
    },

    /**
     * Checks the counter is formulated correctly
     */
    parseCounter: function(counter, messageLength)
    {
        try {
            // Throw exception if not a positive integer
            if ((typeof counter !== 'number') || (counter % 1 !== 0) || (counter < 0))
            {
                throw new Error('The counter size should be an integer from 0 to ' + this.maxInteger);
            }

            // The maximum number of keystream bytes that can be created from this start counter
            var maxKeystreamBytes = ((this.maxInteger - counter) * this.outputByteLength) + this.outputByteLength;

            // Check that enough keystream bytes can be generated to encrypt the message from this counter position 
            // without overflowing JavaScript's maximum integer limit
            if (messageLength > maxKeystreamBytes)
            {
                throw new Error(
                    'The message is longer than the number of keystream bytes that can be generated from this start ' +
                    'counter position. Consider splitting the message into smaller parts. Start counter: ' + 
                    counter + '. Max message bytes that can be ' + 'encrypted/decrypted from this counter position: ' + 
                    maxKeystreamBytes + '. Num of message bytes: ' + messageLength
                );
            }
            else 
            {               
                return counter;
            }
        }
        catch (exception)
        {           
            console.error(exception);
        }
    },  

    /**
     * A wrapper function for the Salsa20 encryption of a message
     */
    encrypt: function(key, message, nonce, counter, options)
    {
        // If the options are unset, set to a blank object
        if (typeof options === 'undefined')
        {
            options = {};
        }

        // Normalise the various input formats to what is required for the encryption function
        key = this.parseKey(key);
        message = this.parseMessage(message, options);
        nonce = this.parseNonce(nonce);
        counter = this.parseCounter(counter, message.length);

        // Encrypt starting from the specified counter
        var encryptedBytes = this.encryption(key, message, nonce, counter);

        // If the return type requested is hex, convert the bytes to hex
        if (options.hasOwnProperty('returnType') && (options.returnType === 'hex'))
        {
            return this.bytesToHex(encryptedBytes);
        }
        else {
            // By default return a byte array
            return encryptedBytes;
        }
    },

    /**
     * A wrapper function for the Salsa20 encryption of a message
     */
    decrypt: function(key, ciphertext, nonce, counter, options)
    {
        // If the options are unset, set to a blank object
        if (typeof options === 'undefined')
        {
            options = {};
        }

        // Normalise the various input formats to what is required for the encryption function
        key = this.parseKey(key);
        ciphertext = this.parseMessage(ciphertext, options);
        nonce = this.parseNonce(nonce);
        counter = this.parseCounter(counter, ciphertext.length);

        // Decrypt starting from the specified counter
        var decryptedBytes = this.encryption(key, ciphertext, nonce, counter);

        // If the return type requested is hex, convert the bytes to hex
        if (options.hasOwnProperty('returnType') && (options.returnType === 'hex'))
        {
            return this.bytesToHex(decryptedBytes);
        }
        else {
            // Decode from bytes to UTF-8 string
            return this.bytesToUtf8String(decryptedBytes);
        }
    }
};

Update:

The updated version of this code based on the review feedback below is available here on GitHub if anyone was wanting to use it as a library.

\$\endgroup\$
1
\$\begingroup\$
  • I'd probably not export all those functions from the Salsa20 object, or maybe use a different helper object to group them together in a better way. Currently leftPadding is in the same group as decrypt, but IMO only the most necessary functions should be in the top-level Salsa20 group, basically only what you want your users to call and where you have certain guarantees for interface stability.

For performance the next few items likely won't improve performance in any significant way, however it's the most I could gather from looking at it, so YMMV and profiling is key for this.

  • In rowRound you construct and pass a couple of four-element arrays to quarterRound, where could just pass in the array and four indexes as plain parameters; somewhat the same case happens in littleEndian.
  • In littleEndian there are a couple of expressions that could be extract into variables, although I suspect that they will be treated as constants anyway.
  • encryption could be made destructive on the passed-in message array.

The rest is just style considerations.

  • parseKey and all the other functions shouldn't catch the exceptions. It's up to the user of the library to handle them. In any case console.error will not be available in some circumstances, so I suggest either not using that, or checking first.
  • checkKeyLength should just be return keylength === 16 || keyLength === 32.
  • It's probably up to taste, but if you throw an exception in an if, the else can be removed, so if (...) throw ... else return 1; can just become if (...) throw ...; return 1; instead (in parseCounter for example).
  • In encrypt I'd initialise the options via options = options || {} instead, even if you ignore some values that way (false, null, undefined), but they weren't useful here anyway.

All in all great work!

\$\endgroup\$
  • \$\begingroup\$ Thanks very much for the excellent review. I have made all the necessary changes based on your comments. I also converted it to work with typed arrays for better performance. This prevents a lot of internal conversions from hex back to arrays. You can see the latest version on GitHub. \$\endgroup\$ – x9c8v7 Mar 14 '15 at 3:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.