Simple Javascript encryption using Libsodium.js in this sandbox demo
Asked Answered
C

5

5

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);
    }
})();
Citify answered 12/8, 2018 at 21:25 Comment(0)
C
5

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(""));
}
Citify answered 13/8, 2018 at 1:51 Comment(2)
Note: I then realized that I'd need to replace null with nonce in the functions above so that the results would be interchangeable with Sodium-PHP. Here is a related question: security.stackexchange.com/questions/191472/… I also added padding.Citify
Do you have some sample PHP code to share also? I am encrypting in JS and decrypting in PHP as a way of avoiding storing client data in audit trails and error logs. I'm trying to wrap my head around Sodium, but it seems exceptionally complicated for my use case. Your JS code seems to be the most straight forward I've found so far. If you have the equivalent for the server/PHP side, that would be great.Myrna
H
2

This is what I do in https://emberclear.io :

tests: https://gitlab.com/NullVoxPopuli/emberclear/blob/master/packages/frontend/src/utils/nacl/unit-test.ts#L19

implementation: https://gitlab.com/NullVoxPopuli/emberclear/blob/master/packages/frontend/src/utils/nacl/utils.ts#L48

Snippet of implementation (in typescript):

import libsodiumWrapper, { ISodium } from 'libsodium-wrappers';

import { concat } from 'emberclear/src/utils/arrays/utils';

export async function libsodium(): Promise<ISodium> {
  const sodium = libsodiumWrapper.sodium;
  await sodium.ready;

  return sodium;
}


export async function encryptFor(
  message: Uint8Array,
  recipientPublicKey: Uint8Array,
  senderPrivateKey: Uint8Array): Promise<Uint8Array> {

  const sodium = await libsodium();
  const nonce = await generateNonce();

  const ciphertext = sodium.crypto_box_easy(
    message, nonce,
    recipientPublicKey, senderPrivateKey
  );

  return concat(nonce, ciphertext);
}

export async function decryptFrom(
  ciphertextWithNonce: Uint8Array,
  senderPublicKey: Uint8Array,
  recipientPrivateKey: Uint8Array): Promise<Uint8Array> {

  const sodium = await libsodium();

  const [nonce, ciphertext] = await splitNonceFromMessage(ciphertextWithNonce);
  const decrypted = sodium.crypto_box_open_easy(
    ciphertext, nonce,
    senderPublicKey, recipientPrivateKey
  );

  return decrypted;
}

export async function splitNonceFromMessage(messageWithNonce: Uint8Array): Promise<[Uint8Array, Uint8Array]> {
  const sodium = await libsodium();
  const bytes = sodium.crypto_box_NONCEBYTES;

  const nonce = messageWithNonce.slice(0, bytes);
  const message = messageWithNonce.slice(bytes, messageWithNonce.length);

  return [nonce, message];
}

export async function generateNonce(): Promise<Uint8Array> {
  const sodium = await libsodium();

  return await randomBytes(sodium.crypto_box_NONCEBYTES);
}

export async function randomBytes(length: number): Promise<Uint8Array> {
  const sodium = await libsodium();

  return sodium.randombytes_buf(length);
}

Snippet of tests:

import * as nacl from './utils';
import { module, test } from 'qunit';

module('Unit | Utility | nacl', function() {
  test('libsodium uses wasm', async function(assert) {
    const sodium = await nacl.libsodium();
    const isUsingWasm = sodium.libsodium.usingWasm;

    assert.ok(isUsingWasm);
  });

  test('generateAsymmetricKeys | works', async function(assert) {
    const boxKeys = await nacl.generateAsymmetricKeys();

    assert.ok(boxKeys.publicKey);
    assert.ok(boxKeys.privateKey);
  });

  test('encryptFor/decryptFrom | works with Uint8Array', async function(assert) {
    const receiver = await nacl.generateAsymmetricKeys();
    const sender = await nacl.generateAsymmetricKeys();

    const msgAsUint8 = Uint8Array.from([104, 101, 108, 108, 111]); // hello
    const ciphertext = await nacl.encryptFor(msgAsUint8, receiver.publicKey, sender.privateKey);
    const decrypted = await nacl.decryptFrom(ciphertext, sender.publicKey, receiver.privateKey);

    assert.deepEqual(msgAsUint8, decrypted);
  });
Hypothermal answered 12/8, 2018 at 21:59 Comment(2)
Thanks. The answer to your question at gitlab.com/NullVoxPopuli/emberclear/blob/master/packages/… is here: #14071963Citify
Extra points for clean, easily understandable code with reasonable function names!Buckskin
K
1

I think you're making this harder than it needs to be. For your typescript encryption for example, all you need to do is this:

private async encrypt(obj: any): Promise<string> {
    await Sodium.ready;

    const json = JSON.stringify(obj);
    const key = Sodium.from_hex(this.hexKey);

    const nonce = Sodium.randombytes_buf(Sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES);
    const encrypted = Sodium.crypto_aead_chacha20poly1305_ietf_encrypt(json, '', null, nonce, key);

    // Merge the two together
    const nonceAndCipherText = new Uint8Array(Sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES + encrypted.byteLength);
    nonceAndCipherText.set(nonce);
    nonceAndCipherText.set(encrypted, Sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES);

    return btoa(String.fromCharCode(...nonceAndCipherText));
}

You don't need all the extra libraries you're using. And on your PHP side, to decrypt you'd just do this:

function decode($encrypted, $key)
{
    $decoded = base64_decode($encrypted); // Should be using sodium_base642bin?
    if ($decoded === false) {
        throw new Exception('Scream bloody murder, the decoding failed');
    }

    $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, '8bit');
    $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, null, '8bit');

    $plain = sodium_crypto_aead_chacha20poly1305_ietf_decrypt($ciphertext, '', $nonce, sodium_hex2bin($key));

    sodium_memzero($ciphertext);
    sodium_memzero($key);

    if ($plain === false) {
        throw new Exception('the message was tampered with in transit');
    }

    return $plain;
}

You don't need to set the nonce multiple times. That second parameter to the encryption is the "additional data" parameter, and it can just be an empty string if it's an empty string on the decryption side as well.

Krystalkrystalle answered 23/12, 2018 at 21:51 Comment(0)
M
1

I've been experimenting with @Ryan's answer, and found that while it is working, a far simpler solution is to use sodium-plus. An example of a sodium-plus script can be found here. In short, the encryption side looks like this:

<script type='text/javascript' src='sodium-plus.min.js'></script>
<script>
async function encryptString(clearText) {
    if (!window.sodium) window.sodium = await SodiumPlus.auto();
    let publicKey = await X25519PublicKey.from('[Place your 64-char public key hex or variable name here]','hex');
    let cipherText = await sodium.crypto_box_seal(clearText, publicKey);
    return cipherText.toString('hex');
}

(async function () {
    let clearText = "String that contains secret.";
    console.log(await encryptString(clearText));
})();
</script>

A lot simpler. On the PHP side, all you'll need to do is use the sodium methods to handle encryption/decryption of strings.

The only downside with sodium-plus is that I haven't found a CDN for the browser version yet.

Myrna answered 24/5, 2020 at 14:44 Comment(2)
sodium-plus looks unmaintained as of 2022 sadly :(Ta
Perhaps, but it still works, at least for now!Myrna
A
0

my working example, based on Ryan's answer. Encode in PHP, decode in javascript. Data is encoded in hex or base64. In console, call work() or work_b64().

<?php
    // source https://github.com/jedisct1/libsodium.js
    // put sodium.js in root folder
    $key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen();
    $nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
    $message = 'Hello World';
    $encrypted_text = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($message, '', $nonce, $key);

    $message_hex = bin2hex( $message );
    $encrypted_text_hex = bin2hex( $encrypted_text );
    $nonce_hex = bin2hex($nonce);
    $key_hex = bin2hex($key);
    
    $message_b64 = base64_encode( $message );
    $encrypted_text_b64 = base64_encode( $encrypted_text );
    $nonce_b64 = base64_encode($nonce);
    $key_b64 = base64_encode($key);

    $str = [];
    $str['message']=$message;
    $str['encrypted_text_hex']=$encrypted_text_hex;
    $str['encrypted_text_b64']=$encrypted_text_b64;
    $str['nonce_hex']=$nonce_hex;
    $str['nonce_b64']=$nonce_b64;
    $str['key_hex']=$key_hex;
    $str['key_b64']=$key_b64;

    foreach( $str as $key => $val){
        printf( "%s=%s<br>\n", $key, $val);
    }
?>

<title>Libsodium.js test</title>
<script src="sodium.js" async></script>
<script>
    window.sodium = {
        onload: function (sodium) {
            let h = sodium.crypto_generichash(64, sodium.from_string('test'));
            console.log(sodium.to_hex(h));
        }
    };
</script>

<script>
<?php
    foreach( $str as $key => $val){
        printf( "%s='%s'\n", $key, $val);
    }
?> 

function work(){
    key = sodium.from_hex(key_hex);
    msg_enc = sodium.from_hex(encrypted_text_hex);
    nonce = sodium.from_hex(nonce_hex);
    try {
        var decryptedStr = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(nonce, msg_enc, null, nonce, key, "text");
        console.log("Recovered input string:", decryptedStr);
        console.log("Check whether the following text matches the original:", decryptedStr === message);
    } catch (e) {
        console.error(e);
    }
}

function work_b64(){
    key = sodium.from_base64(key_b64, sodium.base64_variants.ORIGINAL);
    msg_enc = sodium.from_base64(encrypted_text_b64, sodium.base64_variants.ORIGINAL);
    nonce = sodium.from_base64(nonce_b64, sodium.base64_variants.ORIGINAL);
    try {
        var decryptedStr = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(nonce, msg_enc, null, nonce, key, "text");
        console.log("Recovered input string:", decryptedStr);
        console.log("Check whether the following text matches the original:", decryptedStr === message);
    } catch (e) {
        console.error(e);
    }
}
</script>
Antung answered 10/7, 2022 at 17:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.