Простое шифрование Javascript с использованием Libsodium.js в этой демо-песочнице

Я потратил позорное количество часов, пытаясь заставить Libsodium.js работать.

Посмотрите мою демонстрацию скрипки (и код тоже приклеено ниже).

Я продолжаю получать Error: wrong secret key for the given ciphertext.

Я бы предпочел воспроизвести этот PHP-пример function simpleEncrypt($message, $key) в Libsodium.js.

Но для начала я был бы рад даже получить базовый пример из репозитория Libsodium.js работать.

Любые подсказки?


Вот код (также показан в рабочей скрипке):

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

person Ryan    schedule 12.08.2018    source источник


Ответы (4)


Вау, у меня наконец-то все заработало!

Части, которые действительно помогли мне, были:

Вот работающая песочница для скриптов.


И на случай, если это когда-нибудь исчезнет, ​​вот важные части:

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(""));
}
person Ryan    schedule 13.08.2018
comment
Примечание. Затем я понял, что мне нужно заменить null на nonce в приведенных выше функциях, чтобы результаты были взаимозаменяемыми с Sodium-PHP. Вот связанный с этим вопрос: security.stackexchange.com/questions/191472/. Я также добавил отступы. - person Ryan; 14.08.2018
comment
У вас есть образец PHP-кода, которым вы также можете поделиться? Я шифрую в JS и расшифровываю в PHP, чтобы избежать хранения клиентских данных в журналах аудита и журналах ошибок. Я пытаюсь разобраться с натрием, но он кажется исключительно сложным для моего варианта использования. Ваш код JS кажется самым простым, который я нашел до сих пор. Если у вас есть эквивалент для сервера/PHP, это было бы здорово. - person dearsina; 23.05.2020

Вот что я делаю в https://emberclear.io:

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

реализация: https://gitlab.com/NullVoxPopuli/emberclear/blob/master/packages/frontend/src/utils/nacl/utils.ts#L48

Фрагмент реализации (в машинописи):

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

Фрагмент тестов:

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);
  });
person NullVoxPopuli    schedule 12.08.2018
comment
Спасибо. Ответ на ваш вопрос в gitlab. com/NullVoxPopuli/emberclear/blob/master/packages/ находится здесь: stackoverflow.com/questions/14071463/ - person Ryan; 13.08.2018
comment
Дополнительные баллы за чистый, понятный код с разумными именами функций! - person Master of Ducks; 18.11.2019

Я экспериментировал с ответом @Ryan и обнаружил, что, пока он работает, гораздо более простым решением является использование натрий-плюс. Пример сценария натрия плюс можно найти здесь. Вкратце, сторона шифрования выглядит так:

<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>

Намного проще. На стороне PHP все, что вам нужно сделать, это использовать sodium для обработки шифрования/дешифрования строк.

Единственным недостатком натрия-плюс является то, что я еще не нашел CDN для браузерной версии.

person dearsina    schedule 24.05.2020

Я думаю, ты делаешь это сложнее, чем нужно. Например, для шифрования вашего машинописного текста все, что вам нужно сделать, это:

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

Вам не нужны все дополнительные библиотеки, которые вы используете. А на вашей стороне PHP для расшифровки вы просто сделаете это:

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

Вам не нужно устанавливать одноразовый номер несколько раз. Этот второй параметр шифрования является параметром «дополнительные данные», и он может быть просто пустой строкой, если это также пустая строка на стороне дешифрования.

person Gargoyle    schedule 23.12.2018