Search code examples
javascriptencryptionencryption-symmetriclibsodium

Simple Javascript encryption using Libsodium.js in this sandbox demo


I've spent an embarrasing number of hours trying to get Libsodium.js to work.

See my fiddle demo (and code pasted below too).

I keep getting Error: wrong secret key for the given ciphertext.

What I would prefer is to replicate this PHP example of function simpleEncrypt($message, $key) into Libsodium.js.

But as a starter, I'd be happy even getting the basic sample from the Libsodium.js repo to work.

Any hints?


Here is the code (also shown in the working fiddle):

const _sodium = require("libsodium-wrappers");
const concatTypedArray = require("concat-typed-array");
(async () => {
    await _sodium.ready;
    const sodium = _sodium;
    const utf8 = "utf-8";
    const td = new TextDecoder(utf8);
    const te = new TextEncoder(utf8);
    const nonceBytes = sodium.crypto_secretbox_NONCEBYTES;
    const macBytes = sodium.crypto_secretbox_MACBYTES;

    let key = sodium.from_hex("724b092810ec86d7e35c9d067702b31ef90bc43a7b598626749914d6a3e033ed");

    function encrypt_and_prepend_nonce(message, key) {
        let nonce = sodium.randombytes_buf(nonceBytes);
        var encrypted = sodium.crypto_secretbox_easy(message, nonce, key);
        var combined2 = concatTypedArray(Uint8Array, nonce, encrypted);
        return combined2;
    }

    function decrypt_after_extracting_nonce(nonce_and_ciphertext, key) {
        if (nonce_and_ciphertext.length < nonceBytes + macBytes) {
            throw "Short message";
        }
        let nonce = nonce_and_ciphertext.slice(0, nonceBytes);
        let ciphertext = nonce_and_ciphertext.slice(nonceBytes);
        return sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
    }

    function encrypt(message, key) {
        var x = encrypt_and_prepend_nonce(message, key);
        return td.decode(x);
    }

    function decrypt(nonce_and_ciphertext_str, key) {
        var nonce_and_ciphertext = te.encode(nonce_and_ciphertext_str);
        return decrypt_after_extracting_nonce(nonce_and_ciphertext, key);
    }

    var inputStr = "shhh this is a secret";
    var garbledStr = encrypt(inputStr, key);
    try {
        var decryptedStr = decrypt(garbledStr, key);
        console.log("Recovered input string:", decryptedStr);
        console.log("Check whether the following text matches the original:", decryptedStr === inputStr);
    } catch (e) {
        console.error(e);
    }
})();

Solution

  • Wow, I finally got it working!

    The parts that really helped me were:

    Here is the working fiddle sandbox.


    And in case that ever disappears, here are the important parts:

    const nonceBytes = sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
    let key = sodium.from_hex("724b092810ec86d7e35c9d067702b31ef90bc43a7b598626749914d6a3e033ed");
    var nonceTest;
    
    /**
     * @param {string} message
     * @param {string} key
     * @returns {Uint8Array}
     */
    function encrypt_and_prepend_nonce(message, key) {
        let nonce = sodium.randombytes_buf(nonceBytes);
        nonceTest = nonce.toString();
        var encrypted = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(message, null, nonce, nonce, key);
        var nonce_and_ciphertext = concatTypedArray(Uint8Array, nonce, encrypted); //https://github.com/jedisct1/libsodium.js/issues/130#issuecomment-361399594     
        return nonce_and_ciphertext;
    }
    
    /**
     * @param {Uint8Array} nonce_and_ciphertext
     * @param {string} key
     * @returns {string}
     */
    function decrypt_after_extracting_nonce(nonce_and_ciphertext, key) {
        let nonce = nonce_and_ciphertext.slice(0, nonceBytes); //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice      
        let ciphertext = nonce_and_ciphertext.slice(nonceBytes);
        var result = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(nonce, ciphertext, null, nonce, key, "text");
        return result;
    }
    
    /**
     * @param {string} message
     * @param {string} key
     * @returns {string}
     */
    function encrypt(message, key) {
        var uint8ArrayMsg = encrypt_and_prepend_nonce(message, key);
        return u_btoa(uint8ArrayMsg); //returns ascii string of garbled text
    }
    
    /**
     * @param {string} nonce_and_ciphertext_str
     * @param {string} key
     * @returns {string}
     */
    function decrypt(nonce_and_ciphertext_str, key) {
        var nonce_and_ciphertext = u_atob(nonce_and_ciphertext_str); //converts ascii string of garbled text into binary
        return decrypt_after_extracting_nonce(nonce_and_ciphertext, key);
    }
    
    function u_atob(ascii) {        //https://stackoverflow.com/a/43271130/
        return Uint8Array.from(atob(ascii), c => c.charCodeAt(0));
    }
    
    function u_btoa(buffer) {       //https://stackoverflow.com/a/43271130/
        var binary = [];
        var bytes = new Uint8Array(buffer);
        for (var i = 0, il = bytes.byteLength; i < il; i++) {
            binary.push(String.fromCharCode(bytes[i]));
        }
        return btoa(binary.join(""));
    }