Simple Javascript encryption using Libsodium.js in this sandbox demo

拟墨画扇 提交于 2019-12-02 02:29:08

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);
  });

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(""));
}

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!