From 8ce8534010689a002507137c62054fe53c49e232 Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Wed, 15 Jan 2025 22:31:47 +0000 Subject: [PATCH 01/10] checkpoint --- package-lock.json | 11 + package.json | 1 + src/core/config/Categories.json | 3 +- .../operations/LibHydrogenCurve25519Sign.mjs | 806 ++++++++++++++++++ webpack.config.js | 6 +- 5 files changed, 825 insertions(+), 2 deletions(-) create mode 100644 src/core/operations/LibHydrogenCurve25519Sign.mjs diff --git a/package-lock.json b/package-lock.json index 9b24b981..c6cb06df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "jsrsasign": "^11.1.0", "kbpgp": "2.1.15", "libbzip2-wasm": "0.0.4", + "libhydrogen": "file:../dsbd-sonata/build/backend/crypt/vendor/libhydrogen/libhydrogen-0.0.0.tgz", "libyara-wasm": "^1.2.1", "lodash": "^4.17.21", "loglevel": "^1.9.1", @@ -10522,6 +10523,16 @@ "version": "0.0.4", "license": "ISC" }, + "node_modules/libhydrogen": { + "version": "0.0.0", + "resolved": "file:../dsbd-sonata/build/backend/crypt/vendor/libhydrogen/libhydrogen-0.0.0.tgz", + "integrity": "sha512-lc/7slg5dcRja8y+wrN4+2zTdaYfxr66dUg+njdNlGFH8AACN8wT4gmPiPuK1y7glrIfnOPr3Z03byP6djHL/A==", + "license": "UNLICENSED", + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, "node_modules/libyara-wasm": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/libyara-wasm/-/libyara-wasm-1.2.1.tgz", diff --git a/package.json b/package.json index a1470dee..9487f664 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "jsrsasign": "^11.1.0", "kbpgp": "2.1.15", "libbzip2-wasm": "0.0.4", + "libhydrogen": "file:../dsbd-sonata/build/backend/crypt/vendor/libhydrogen/libhydrogen-0.0.0.tgz", "libyara-wasm": "^1.2.1", "lodash": "^4.17.21", "loglevel": "^1.9.1", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 60376907..2ec6f6e7 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -157,7 +157,8 @@ "Typex", "Lorenz", "Colossus", - "SIGABA" + "SIGABA", + "LibHydrogen Curve25519 Sign" ] }, { diff --git a/src/core/operations/LibHydrogenCurve25519Sign.mjs b/src/core/operations/LibHydrogenCurve25519Sign.mjs new file mode 100644 index 00000000..926c7920 --- /dev/null +++ b/src/core/operations/LibHydrogenCurve25519Sign.mjs @@ -0,0 +1,806 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable camelcase */ +/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable no-console */ +/** + * @author Configured Things Ltd. [getconfigured@configuredthings.com] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +import * as hydro from "libhydrogen/libHydrogen.js"; + +/** + * LibHydrogen Curve25519 Sign operation + */ +class LibHydrogenCurve25519Signing extends Operation { + + /** + * LibHydrogenCurve25519Sign constructor + */ + constructor() { + super(); + + this.name = "LibHydrogen Curve25519 Sign"; + this.module = "Crypto"; + this.description = "Computes a signature for a message using the lightweight LibHydrogen cryptography library"; + this.infoURL = "https://libhydrogen.org/"; + this.inputType = "byteArray"; + this.outputType = "JSON"; + this.args = [ + { + name: "First arg", + type: "string", + value: "Don't Panic" + }, + { + name: "Second arg", + type: "number", + value: 42 + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {JSON} + */ + run(input, args) { + const [firstArg, secondArg] = args; + (async () => { + const wasm_src = await fetch(new URL(`${self.docURL}/assets/libhydrogen/libhydrogen.wasm`)); + const wasm = await WebAssembly.compileStreaming(wasm_src); + const imports = { + "wasi_snapshot_preview1": { + args_get() { + return 0; + }, + args_sizes_get() { + return 0; + }, + clock_res_get() { + return 0; + }, + clock_time_get() { + return 0; + }, + environ_sizes_get() { + return 0; + }, + environ_get() { + return 0; + }, + proc_exit() { + return 0; + }, + fd_write() { + return 0; + }, + fd_advise() { + return 0; + }, + fd_allocate() { + return 0; + }, + fd_close() { + return 0; + }, + fd_datasync() { + return 0; + }, + fd_fdstat_get() { + return 0; + }, + fd_fdstat_set_flags() { + return 0; + }, + fd_fdstat_set_rights() { + return 0; + }, + fd_filestat_get() { + return 0; + }, + fd_filestat_set_size() { + return 0; + }, + fd_filestat_set_times() { + return 0; + }, + fd_pread() { + return 0; + }, + fd_prestat_get() { + return 0; + }, + fd_prestat_dir_name() { + return 0; + }, + fd_pwrite() { + return 0; + }, + fd_read() { + return 0; + }, + fd_readdir() { + return 0; + }, + fd_renumber() { + return 0; + }, + fd_seek() { + return 0; + }, + fd_sync() { + return 0; + }, + fd_tell() { + return 0; + }, + path_create_directory() { + return 0; + }, + path_filestat_get() { + return 0; + }, + path_filestat_set_times() { + return 0; + }, + path_link() { + return 0; + }, + path_open() { + return 0; + }, + path_readlink() { + return 0; + }, + path_remove_directory() { + return 0; + }, + path_rename() { + return 0; + }, + path_symlink() { + return 0; + }, + path_unlink_file() { + return 0; + }, + poll_oneoff() { + return 0; + }, + sched_yield() { + return 0; + }, + random_get() { + return 0; + }, + sock_accept() { + return 0; + }, + sock_recv() { + return 0; + }, + sock_send() { + return 0; + }, + sock_shutdown() { + return 0; + }, + } + }; + instance = await WebAssembly.instantiate(wasm, imports); + // We must call a start method per WASI specification + // Libhydrogen's main method is one we have patched to initialise it + instance.exports._start(); + // Get the memory are used as a stack when calling into the WASI + const memory = instance.exports.memory; + // DataView takes care of our platform specific endian conversions + dataview = new DataView(memory.buffer); + // Run the various examples + random_uniform(); + hash(); + keyed_hash(); + public_key_signing(); + symmetric_encryption(); + symmetric_encryption_via_asymmetric_key_exchange(); + })(); + return {}; + } + +} + +let instance, dataview; + +// +// Helper function to reserve space in the buffer used +// as a stack between Node and the wasm. +// +// Offset must be an an object so we can update it's value +// {value: n} +// +// Designed to allow a sequence of reservations such as +// +// let offset = {value: 0}; +// buf1 = reserve (offset, 100); +// buf2 = reserve (offset, 590); +// ... +// +function reserve(offset, length) { + const a = new Uint8Array(dataview.buffer, offset.value, length); + const newOffset = a.byteOffset + a.byteLength; + offset.value = newOffset; + return a; +} + +// +// Generate a series of random numbers +// +function random_uniform() { + console.log("\n=== random_uniform ===\n"); + const { hydro_random_uniform } = instance.exports; + // Testing a simple case of passing integers and fetching integers + console.log(`generated random - ${hydro_random_uniform(20)}`); + console.log(`generated random - ${hydro_random_uniform(20)}`); + console.log(`generated random - ${hydro_random_uniform(20)}`); + console.log(`generated random - ${hydro_random_uniform(20)}`); + console.log(`generated random - ${hydro_random_uniform(20)}`); +} + +// +// Hash Generation +// +function hash() { + console.log("\n=== hash ===\n"); + const { hydro_hash_hash } = instance.exports; + const textEncoder = new TextEncoder(); + + // We have to create the stack frame to pass to libHydrogen + // in the dataview Buffer, and then pass in pointers to + // that buffer + + const offset = { value: 0 }; + const context_str = "Examples"; + const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); + const examples_ab = textEncoder.encode(context_str); + + for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { + context_arr.set([examples_ab.at(i)], i); + } + + // Our message to be hashed + const message_str = "Arbitrary data to hash"; + const message_ab = textEncoder.encode(message_str); + const message_arr = reserve(offset, message_ab.length); + + for (let i = 0; i < message_ab.length; i++) { + message_arr.set([message_ab.at(i)], i); + } + + // Buffer for libHydrogen to write the hash into + const hash = reserve(offset, hydro.hash_BYTES); + + // Call the imported function + hydro_hash_hash( + hash.byteOffset, + hash.length, + message_arr.byteOffset, + message_arr.byteLength, + context_arr.byteOffset, + null, + ); + console.log(`generated hash - ${Buffer.from(hash).toString("hex")}`); +} + +// +// Hash generation with a key +// +function keyed_hash() { + console.log("\n=== keyed_hash ===\n"); + + // Importing libhydrogen's hashing keygen and hash functions + const { hydro_hash_keygen, hydro_hash_hash } = instance.exports; + const textEncoder = new TextEncoder(); + + // We have to create the stack frame to pass to libHydrogen + // in the dataview Buffer, and then pass in pointers to + // that buffer + const offset = { value: 0 }; + const context_str = "Examples"; + const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); + const examples_ab = textEncoder.encode(context_str); + + for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { + context_arr.set([examples_ab.at(i)], i); + } + + const message_str = "Arbitrary data to hash"; + const message_ab = textEncoder.encode(message_str); + const message_arr = reserve(offset, message_ab.length); + + for (let i = 0; i < message_ab.length; i++) { + message_arr.set([message_ab.at(i)], i); + } + + // Reserve buffer space for the returned key + const key = reserve(offset, hydro.hash_KEYBYTES); + + // Reserve space for the hash result + const keyedhash = reserve(offset, hydro.hash_BYTES); + + // Generate hashing key + hydro_hash_keygen(key.byteOffset); + console.log(`generated hash key - ${key}`); + + // Create a hash with the key + hydro_hash_hash( + keyedhash.byteOffset, + keyedhash.length, + message_arr.byteOffset, + message_arr.byteLength, + context_arr.byteOffset, + key.byteOffset, + ); + const khash1 = keyedhash.toString("hex"); + console.log(`khash1 - ${khash1}`); + + keyedhash.fill(0); // Resetting output buffer (seems to pollute state otherwise) + + // Hashing message with key again + hydro_hash_hash( + keyedhash.byteOffset, + keyedhash.length, + message_arr.byteOffset, + message_arr.byteLength, + context_arr.byteOffset, + key.byteOffset, + ); + const khash2 = keyedhash.toString("hex"); + console.log(`khash2 - ${khash2}`); + keyedhash.fill(0); + console.log(`${keyedhash.toString("hex")}`); + + // Check that the same hash is generated + if (khash1 === khash2) console.log("khash1 equals khash2"); + + // Testing whether we can load a key and generate a matching hash created previously + const presetKey = "f539065185b3ce774b0c748564e804a3717ca7d0c08231076e8b7920814f0bba"; + console.log(`presetKey - ${presetKey}`); + + const historicHash = "4004516ceff97883804dbdb221baeb7283256e60165d0715d0152e4d6a6cbad4"; + console.log(`historicHash - ${historicHash}`); + + const loadedKey = new Uint8Array(presetKey.match(/../g).map(h=>parseInt(h, 16))); + for (let i = 0; i < loadedKey.length; i++) { + key.set([loadedKey.at(i)], i); + } + + // Hashing message with presetKey + hydro_hash_hash( + keyedhash.byteOffset, + keyedhash.length, + message_arr.byteOffset, + message_arr.byteLength, + context_arr.byteOffset, + key.byteOffset, + ); + + const khash3 = keyedhash.toString("hex"); + console.log(`khash3 - ${khash3}`); + + // Testing hash matches historicHash + if (khash3 === historicHash) + console.log("khash3 using old key, matches historicHash"); + + // ...and doesn't match the hashes with the latest key + if (khash1 !== khash3) + console.log("khash3 does not equal khash1"); + + // clear the buffer for the next example + context_arr.fill(0); + message_arr.fill(0); + keyedhash.fill(0); + key.fill(0); +} + +// +// Public key signing +// +function public_key_signing() { + console.log("\n=== public_key_signing ===\n"); + + // Importing libhydrogen's signing keygen and signing and verification functions + const { hydro_sign_keygen, hydro_sign_create, hydro_sign_verify } = instance.exports; + const textEncoder = new TextEncoder(); + + // We have to create the stack frame to pass to libHydrogen + // in the dataview Buffer, and then pass in pointers to + // that buffer + const offset = { value: 0 }; + const context_str = "Examples"; + const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); + const examples_ab = textEncoder.encode(context_str); + + for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { + context_arr.set([examples_ab.at(i)], i); + } + + const message_str = "Arbitrary data to hash"; + const message_ab = textEncoder.encode(message_str); + const message_arr = reserve(offset, message_ab.length); + + for (let i = 0; i < message_ab.length; i++) { + message_arr.set([message_ab.at(i)], i); + } + + // Generate a key pair + const keypair = reserve(offset, hydro.sign_PUBLICKEYBYTES + hydro.sign_SECRETKEYBYTES); + const publicKeyOffset = keypair.byteOffset; + const privateKeyOffset = keypair.byteOffset + hydro.sign_PUBLICKEYBYTES; + hydro_sign_keygen(keypair.byteOffset); + + // Reserving memory for the signature + const signature = reserve(offset, hydro.sign_BYTES); + + // Creating signature of message with secret key + hydro_sign_create( + signature.byteOffset, + message_arr.byteOffset, + message_arr.byteLength, + context_arr.byteOffset, + privateKeyOffset, + ); + + console.log(`generated signature - ${Buffer.from(signature).toString("hex")}`); + + // Verifying signature with public key + const res = hydro_sign_verify( + signature.byteOffset, + message_arr.byteOffset, + message_arr.byteLength, + context_arr.byteOffset, + publicKeyOffset, + ); + + if (res === 0) console.log("Signature is correctly valid"); + signature.set([0]); // Modifying signature + console.log(`modified signature - ${Buffer.from(signature).toString("hex")}`); + + const manipulatedRes = hydro_sign_verify( + signature.byteOffset, + message_arr.byteOffset, // Reverifying signature + message_arr.byteLength, + context_arr.byteOffset, + publicKeyOffset, + ); + if (manipulatedRes !== 0) console.log("Signature is correctly invalid"); + + message_arr.fill(0); + keypair.fill(0); + signature.fill(0); +} + +// +// Secret Key Encryption +// +function symmetric_encryption() { + console.log("\n=== symmetric_encryption ===\n"); + + // Importing libhydrogen's secretbox keygen and encrypt and decrypt functions + const { hydro_secretbox_keygen, hydro_secretbox_encrypt, hydro_secretbox_decrypt } = + instance.exports; + const textEncoder = new TextEncoder(); + + // We have to create the stack frame to pass to libHydrogen + // in the dataview Buffer, and then pass in pointers to + // that buffer + const offset = { value: 0 }; + const context_str = "Examples"; + const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); + const examples_ab = textEncoder.encode(context_str); + + for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { + context_arr.set([examples_ab.at(i)], i); + } + + // Reserving buffer for the message + const message_str = "Arbitrary data to hash"; + const message_ab = textEncoder.encode(message_str); + const message_arr = reserve(offset, message_ab.length); + + for (let i = 0; i < message_ab.length; i++) { + message_arr.set([message_ab.at(i)], i); + } + console.log(`message - ${message_arr}`); + + // Reserving buffer for the key + const key = reserve(offset, hydro.secretbox_KEYBYTES); + hydro_secretbox_keygen(key.byteOffset); + console.log(`generated key - ${key.toString("hex")}`); + + // Reserving buffer for the cipher text + const cipherTextLength = hydro.secretbox_HEADERBYTES + message_arr.length; + const ciphertext = reserve(offset, cipherTextLength); + + // Enciphering single message (thus use of msg_id 0n -- 'n' as libhydrogen expects i64) + hydro_secretbox_encrypt( + ciphertext.byteOffset, + message_arr.byteOffset, + message_arr.byteLength, + 0n, + context_arr.byteOffset, + key.byteOffset, + ); + + // Reserving buffer for the decrypted plain text + const decryptedPlaintextLength = ciphertext.byteLength - hydro.secretbox_HEADERBYTES; + const decryptedPlaintext = reserve(offset, decryptedPlaintextLength); + + // Deciphering single message (thus use of msg_id 0n -- 'n' as libhydrogen expects i64) + const res = hydro_secretbox_decrypt( + decryptedPlaintext.byteOffset, + ciphertext.byteOffset, + ciphertext.byteLength, + 0n, + context_arr.byteOffset, + key.byteOffset, + ); + if (res === 0) { + // As secretbox is an authenticated encryption (AEAD) algorithm + // we check that the ciphertext was authentic + console.log("cipherText not forged"); + // Decoding Uint8 encoded string + const textDecoder = new TextDecoder(); + console.log(`decryptedPlaintext - ${textDecoder.decode(decryptedPlaintext)}`); + } + + message_arr.fill(0); + key.fill(0); + ciphertext.fill(0); + decryptedPlaintext.fill(0); +} + +// +// Symmetric Encryption using a Key Generated +// via Asymmetric key exchange +// +function symmetric_encryption_via_asymmetric_key_exchange() { + console.log("\n=== symmetric_encryption_via_asymmetric_key_exchange ===\n"); + + const { hydro_kx_keygen, hydro_kx_kk_1, hydro_kx_kk_2, hydro_kx_kk_3 } = instance.exports; + const textEncoder = new TextEncoder(); + + // + // Reserve space in the buffer for the keypair exchange + // + // Both Alice and Bob need a keypair + // + // Both Alice and bob need a pair of session keys + // tx to encrpyt + // rx to decrypt + // + // Alice initiates the key exchange, so she also needs a kx state + // buffer + // + + const offset = { value: 0 }; + const alice = { + static: { + pk: reserve(offset, hydro.kx_PUBLICKEYBYTES), + sk: reserve(offset, hydro.kx_SECRETKEYBYTES), + }, + session: { + rx: reserve(offset, hydro.kx_SESSIONKEYBYTES), + tx: reserve(offset, hydro.kx_SESSIONKEYBYTES), + }, + }; + + const bob = { + static: { + pk: reserve(offset, hydro.kx_PUBLICKEYBYTES), + sk: reserve(offset, hydro.kx_SECRETKEYBYTES), + }, + session: { + rx: reserve(offset, hydro.kx_SESSIONKEYBYTES), + tx: reserve(offset, hydro.kx_SESSIONKEYBYTES), + }, + }; + + const keysOffset = offset.value; + alice.state = reserve(offset, hydro.kx_SESSIONKEYBYTES); + + // We need two "packets" for the messages exchanged between + // Alice and Bob during the key exchange + const packets = { + 1: reserve(offset, hydro.kx_KK_PACKET1BYTES), + 2: reserve(offset, hydro.kx_KK_PACKET2BYTES), + }; + + console.log("\n=== symmetric_encryption_via_asymmetric_key_exchange ===\n"); + // + // Generate the static keys + // + console.log("------------ALICEKEYGEN------------"); + hydro_kx_keygen(alice.static.pk.byteOffset); + console.log("---------------ALICE---------------"); + console.log(alice); + + console.log("-------------BOBKEYGEN-------------"); + hydro_kx_keygen(bob.static.pk.byteOffset); + console.log("----------------BOB----------------"); + console.log(bob); + + // + // Key exchange + // + + // Performing kk_1 (Generating alice's ephemeral keypair and packet 1) + console.log("-----------hydro_kx_kk_1-----------"); + hydro_kx_kk_1( + alice.state.byteOffset, + packets[1].byteOffset, + bob.static.pk.byteOffset, + alice.static.pk.byteOffset, + ); + console.log("---------------ALICE---------------"); + console.dir(alice, { maxArrayLength: null }); + console.log("--------------packets--------------"); + console.log(packets); + + // Performing kk_2 (Generating bob's response packet 2 and his copy of session keys) + console.log("-----------hydro_kx_kk_2-----------"); + hydro_kx_kk_2( + bob.session.rx.byteOffset, + packets[2].byteOffset, + packets[1].byteOffset, + alice.static.pk.byteOffset, + bob.static.pk.byteOffset, + ); + console.log("----------------BOB----------------"); + console.dir(bob, { maxArrayLength: null }); + console.log("--------------packets--------------"); + console.log(packets); + + // Performing kk_3 (Generating Alice's copy of session keys) + console.log("-----------hydro_kx_kk_3-----------"); + hydro_kx_kk_3( + alice.state.byteOffset, + alice.session.rx.byteOffset, + packets[2].byteOffset, + alice.static.pk.byteOffset, + ); + console.log("---------------ALICE---------------"); + console.dir(alice, { maxArrayLength: null }); + + console.log("-----------KEYS EXCHANGED----------"); + + // Tidy up the buffer space used in the key exchange + alice.state.fill(0); + packets[1].fill(0); + packets[2].fill(0); + offset.value = keysOffset; + + // + // Use the session keys to exchange messages + // + const { hydro_secretbox_encrypt, hydro_secretbox_decrypt } = instance.exports; + + const context_str = "Examples"; + const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); + const examples_ab = textEncoder.encode(context_str); + + for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { + context_arr.set([examples_ab.at(i)], i); + } + + const message1_str = "Hello Bob"; + const message1_ab = textEncoder.encode(message1_str); + const message1_arr = reserve(offset, message1_ab.length); + + for (let i = 0; i < message1_ab.length; i++) { + message1_arr.set([message1_ab.at(i)], i); + } + + console.log(`message1_arr - ${message1_arr}`); + + const ciphertext1_length = hydro.secretbox_HEADERBYTES + message1_arr.length; + const ciphertext1_arr = reserve(offset, ciphertext1_length); + + // + // Alice Encypts the message with her session tx key + // + // Enciphering single message (thus use of msg_id 0n -- 'n' as libhydrogen expects i64) + // + hydro_secretbox_encrypt( + ciphertext1_arr.byteOffset, + message1_arr.byteOffset, + message1_arr.byteLength, + 0n, + context_arr.byteOffset, + alice.session.tx.byteOffset, + ); + + // + // Bob Decrypts the mesaage with his session rx key + // + // Deciphering single message (thus use of msg_id 0n -- 'n' as libhydrogen expects i64) + // + const decryptedPlaintext1Length = ciphertext1_arr.byteLength - hydro.secretbox_HEADERBYTES; + const decryptedPlaintext1 = reserve(offset, decryptedPlaintext1Length); + + const res1 = hydro_secretbox_decrypt( + decryptedPlaintext1.byteOffset, + ciphertext1_arr.byteOffset, + ciphertext1_arr.byteLength, + 0n, + context_arr.byteOffset, + bob.session.rx.byteOffset, + ); + + // As secretbox is an authenticated encryption (AEAD) algorithm + // we check that the ciphertext was authentic + if (res1 === 0) { + console.log("ciphertext1_arr not forged"); + // Decoding Uint8 encoded string + const textDecoder = new TextDecoder(); + console.log(`decryptedPlaintext1 - ${textDecoder.decode(decryptedPlaintext1)}`); + } + + // + // Bob sends a reply to Alice + // + const message2_str = "Hello Alice"; + const message2_ab = textEncoder.encode(message2_str); + const message2_arr = reserve(offset, message2_ab.length); + + for (let i = 0; i < message2_ab.length; i++) { + message2_arr.set([message2_ab.at(i)], i); + } + + console.log(`message2_arr - ${message2_arr}`); + + const ciphertext2Length = hydro.secretbox_HEADERBYTES + message2_arr.length; + const ciphertext2_arr = reserve(offset, ciphertext2Length); + + // + // Bob encrypts the message with his session tx key + // + hydro_secretbox_encrypt( + ciphertext2_arr.byteOffset, + message2_arr.byteOffset, + message2_arr.byteLength, + 0n, + context_arr.byteOffset, + bob.session.tx.byteOffset, + ); + + // + // Alice decrypts the message with her session rx key + // + const decryptedPlaintext2Length = ciphertext2_arr.byteLength - hydro.secretbox_HEADERBYTES; + const decryptedPlaintext2_arr = reserve(offset, decryptedPlaintext2Length); + + const res2 = hydro_secretbox_decrypt( + decryptedPlaintext2_arr.byteOffset, + ciphertext2_arr.byteOffset, + ciphertext2_arr.byteLength, + 0n, + context_arr.byteOffset, + alice.session.rx.byteOffset, + ); + + // As secretbox is an authenticated encryption (AEAD) algorithm + // we check that the ciphertext was authentic + if (res2 === 0) { + console.log("ciphertext2 not forged"); + const textDecoder = new TextDecoder(); + // Decoding Uint8 encoded string + console.log(`decryptedPlaintext2 - ${textDecoder.decode(decryptedPlaintext2_arr)}`); + } +} + + +export default LibHydrogenCurve25519Signing; diff --git a/webpack.config.js b/webpack.config.js index e77f2ab9..96efc941 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -82,7 +82,11 @@ module.exports = { context: "node_modules/node-forge/dist", from: "prime.worker.min.js", to: "assets/forge/" - } + }, { + context: "node_modules/libhydrogen/", + from: "libhydrogen.wasm", + to: "assets/libhydrogen" + }, ] }), new ModifySourcePlugin({ From 609a510406a766083ee7acf95d8655558de8967a Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:53:57 +0000 Subject: [PATCH 02/10] added entropy src --- src/core/operations/LibHydrogenCurve25519Sign.mjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/core/operations/LibHydrogenCurve25519Sign.mjs b/src/core/operations/LibHydrogenCurve25519Sign.mjs index 926c7920..6b5d5152 100644 --- a/src/core/operations/LibHydrogenCurve25519Sign.mjs +++ b/src/core/operations/LibHydrogenCurve25519Sign.mjs @@ -175,7 +175,9 @@ class LibHydrogenCurve25519Signing extends Operation { sched_yield() { return 0; }, - random_get() { + random_get(buf, buf_len) { + const random_arr = new Uint8Array(dataview.buffer, buf, buf_len); + crypto.getRandomValues(random_arr); return 0; }, sock_accept() { @@ -193,13 +195,13 @@ class LibHydrogenCurve25519Signing extends Operation { } }; instance = await WebAssembly.instantiate(wasm, imports); - // We must call a start method per WASI specification - // Libhydrogen's main method is one we have patched to initialise it - instance.exports._start(); // Get the memory are used as a stack when calling into the WASI const memory = instance.exports.memory; // DataView takes care of our platform specific endian conversions dataview = new DataView(memory.buffer); + // We must call a start method per WASI specification + // Libhydrogen's main method is one we have patched to initialise it + instance.exports._start(); // Run the various examples random_uniform(); hash(); From 00184ff68aeba45188cd5ed6dc7deb9230e9e05f Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:42:52 +0200 Subject: [PATCH 03/10] utilising gh-registry's module --- package-lock.json | 22 +++++++++++----------- package.json | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6cb06df..1bbc046c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@astronautlabs/amf": "^0.0.6", "@babel/polyfill": "^7.12.1", "@blu3r4y/lzma": "^2.3.3", + "@configuredthings/libhydrogen-wasm": "0.1.0", "@wavesenterprise/crypto-gost-js": "^2.1.0-RC1", "@xmldom/xmldom": "^0.8.10", "argon2-browser": "^1.18.0", @@ -59,7 +60,6 @@ "jsrsasign": "^11.1.0", "kbpgp": "2.1.15", "libbzip2-wasm": "0.0.4", - "libhydrogen": "file:../dsbd-sonata/build/backend/crypt/vendor/libhydrogen/libhydrogen-0.0.0.tgz", "libyara-wasm": "^1.2.1", "lodash": "^4.17.21", "loglevel": "^1.9.1", @@ -2070,6 +2070,16 @@ "node": ">=0.1.90" } }, + "node_modules/@configuredthings/libhydrogen-wasm": { + "version": "0.1.0", + "resolved": "https://npm.pkg.github.com/download/@configuredthings/libhydrogen-wasm/0.1.0/09ea0d48f4acd9294909fa95028379bf02a81629", + "integrity": "sha512-kk32qNPHc5RRn35DLi/MpiNDBJZzj4NNzxpcP4LYFQGxWxqMbMSTmQWK7IMsMAA54alzDV31CbtNGDHjkjiKmQ==", + "license": "UNLICENSED", + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -10523,16 +10533,6 @@ "version": "0.0.4", "license": "ISC" }, - "node_modules/libhydrogen": { - "version": "0.0.0", - "resolved": "file:../dsbd-sonata/build/backend/crypt/vendor/libhydrogen/libhydrogen-0.0.0.tgz", - "integrity": "sha512-lc/7slg5dcRja8y+wrN4+2zTdaYfxr66dUg+njdNlGFH8AACN8wT4gmPiPuK1y7glrIfnOPr3Z03byP6djHL/A==", - "license": "UNLICENSED", - "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" - } - }, "node_modules/libyara-wasm": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/libyara-wasm/-/libyara-wasm-1.2.1.tgz", diff --git a/package.json b/package.json index 9487f664..b40b0b84 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "@astronautlabs/amf": "^0.0.6", "@babel/polyfill": "^7.12.1", "@blu3r4y/lzma": "^2.3.3", + "@configuredthings/libhydrogen-wasm": "0.1.0", "@wavesenterprise/crypto-gost-js": "^2.1.0-RC1", "@xmldom/xmldom": "^0.8.10", "argon2-browser": "^1.18.0", @@ -143,7 +144,6 @@ "jsrsasign": "^11.1.0", "kbpgp": "2.1.15", "libbzip2-wasm": "0.0.4", - "libhydrogen": "file:../dsbd-sonata/build/backend/crypt/vendor/libhydrogen/libhydrogen-0.0.0.tgz", "libyara-wasm": "^1.2.1", "lodash": "^4.17.21", "loglevel": "^1.9.1", From af465dfcfa805fb06ca0196ece46e10f85250bca Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Mon, 27 Jan 2025 19:43:01 +0200 Subject: [PATCH 04/10] checkpoint --- .../operations/LibHydrogenCurve25519Sign.mjs | 876 ++++-------------- webpack.config.js | 4 +- 2 files changed, 179 insertions(+), 701 deletions(-) diff --git a/src/core/operations/LibHydrogenCurve25519Sign.mjs b/src/core/operations/LibHydrogenCurve25519Sign.mjs index 6b5d5152..5b2ad355 100644 --- a/src/core/operations/LibHydrogenCurve25519Sign.mjs +++ b/src/core/operations/LibHydrogenCurve25519Sign.mjs @@ -10,7 +10,7 @@ import Operation from "../Operation.mjs"; -import * as hydro from "libhydrogen/libHydrogen.js"; +import * as hydro from "@configuredthings/libhydrogen-wasm/libHydrogen.js"; /** * LibHydrogen Curve25519 Sign operation @@ -27,190 +27,182 @@ class LibHydrogenCurve25519Signing extends Operation { this.module = "Crypto"; this.description = "Computes a signature for a message using the lightweight LibHydrogen cryptography library"; this.infoURL = "https://libhydrogen.org/"; - this.inputType = "byteArray"; + this.inputType = "JSON"; this.outputType = "JSON"; this.args = [ { - name: "First arg", + name: "Context", type: "string", - value: "Don't Panic" + value: "" }, { - name: "Second arg", - type: "number", - value: 42 + name: "Sender's private key", + type: "byteArray", + value: "" } ]; } /** - * @param {byteArray} input + * @param {JSON} input * @param {Object[]} args * @returns {JSON} */ - run(input, args) { - const [firstArg, secondArg] = args; - (async () => { - const wasm_src = await fetch(new URL(`${self.docURL}/assets/libhydrogen/libhydrogen.wasm`)); - const wasm = await WebAssembly.compileStreaming(wasm_src); - const imports = { - "wasi_snapshot_preview1": { - args_get() { - return 0; - }, - args_sizes_get() { - return 0; - }, - clock_res_get() { - return 0; - }, - clock_time_get() { - return 0; - }, - environ_sizes_get() { - return 0; - }, - environ_get() { - return 0; - }, - proc_exit() { - return 0; - }, - fd_write() { - return 0; - }, - fd_advise() { - return 0; - }, - fd_allocate() { - return 0; - }, - fd_close() { - return 0; - }, - fd_datasync() { - return 0; - }, - fd_fdstat_get() { - return 0; - }, - fd_fdstat_set_flags() { - return 0; - }, - fd_fdstat_set_rights() { - return 0; - }, - fd_filestat_get() { - return 0; - }, - fd_filestat_set_size() { - return 0; - }, - fd_filestat_set_times() { - return 0; - }, - fd_pread() { - return 0; - }, - fd_prestat_get() { - return 0; - }, - fd_prestat_dir_name() { - return 0; - }, - fd_pwrite() { - return 0; - }, - fd_read() { - return 0; - }, - fd_readdir() { - return 0; - }, - fd_renumber() { - return 0; - }, - fd_seek() { - return 0; - }, - fd_sync() { - return 0; - }, - fd_tell() { - return 0; - }, - path_create_directory() { - return 0; - }, - path_filestat_get() { - return 0; - }, - path_filestat_set_times() { - return 0; - }, - path_link() { - return 0; - }, - path_open() { - return 0; - }, - path_readlink() { - return 0; - }, - path_remove_directory() { - return 0; - }, - path_rename() { - return 0; - }, - path_symlink() { - return 0; - }, - path_unlink_file() { - return 0; - }, - poll_oneoff() { - return 0; - }, - sched_yield() { - return 0; - }, - random_get(buf, buf_len) { - const random_arr = new Uint8Array(dataview.buffer, buf, buf_len); - crypto.getRandomValues(random_arr); - return 0; - }, - sock_accept() { - return 0; - }, - sock_recv() { - return 0; - }, - sock_send() { - return 0; - }, - sock_shutdown() { - return 0; - }, - } - }; - instance = await WebAssembly.instantiate(wasm, imports); - // Get the memory are used as a stack when calling into the WASI - const memory = instance.exports.memory; - // DataView takes care of our platform specific endian conversions - dataview = new DataView(memory.buffer); - // We must call a start method per WASI specification - // Libhydrogen's main method is one we have patched to initialise it - instance.exports._start(); - // Run the various examples - random_uniform(); - hash(); - keyed_hash(); - public_key_signing(); - symmetric_encryption(); - symmetric_encryption_via_asymmetric_key_exchange(); - })(); - return {}; + async run(input, args) { + const [context, privateKey] = args; + const wasm_src = await fetch(new URL(`${self.docURL}/assets/libhydrogen-wasm/libhydrogen.wasm`)); + const wasm = await WebAssembly.compileStreaming(wasm_src); + const imports = { + "wasi_snapshot_preview1": { + args_get() { + return 0; + }, + args_sizes_get() { + return 0; + }, + clock_res_get() { + return 0; + }, + clock_time_get() { + return 0; + }, + environ_sizes_get() { + return 0; + }, + environ_get() { + return 0; + }, + proc_exit() { + return 0; + }, + fd_write() { + return 0; + }, + fd_advise() { + return 0; + }, + fd_allocate() { + return 0; + }, + fd_close() { + return 0; + }, + fd_datasync() { + return 0; + }, + fd_fdstat_get() { + return 0; + }, + fd_fdstat_set_flags() { + return 0; + }, + fd_fdstat_set_rights() { + return 0; + }, + fd_filestat_get() { + return 0; + }, + fd_filestat_set_size() { + return 0; + }, + fd_filestat_set_times() { + return 0; + }, + fd_pread() { + return 0; + }, + fd_prestat_get() { + return 0; + }, + fd_prestat_dir_name() { + return 0; + }, + fd_pwrite() { + return 0; + }, + fd_read() { + return 0; + }, + fd_readdir() { + return 0; + }, + fd_renumber() { + return 0; + }, + fd_seek() { + return 0; + }, + fd_sync() { + return 0; + }, + fd_tell() { + return 0; + }, + path_create_directory() { + return 0; + }, + path_filestat_get() { + return 0; + }, + path_filestat_set_times() { + return 0; + }, + path_link() { + return 0; + }, + path_open() { + return 0; + }, + path_readlink() { + return 0; + }, + path_remove_directory() { + return 0; + }, + path_rename() { + return 0; + }, + path_symlink() { + return 0; + }, + path_unlink_file() { + return 0; + }, + poll_oneoff() { + return 0; + }, + sched_yield() { + return 0; + }, + random_get(buf, buf_len) { + const random_arr = new Uint8Array(dataview.buffer, buf, buf_len); + crypto.getRandomValues(random_arr); + return 0; + }, + sock_accept() { + return 0; + }, + sock_recv() { + return 0; + }, + sock_send() { + return 0; + }, + sock_shutdown() { + return 0; + }, + } + }; + instance = await WebAssembly.instantiate(wasm, imports); + // Get the memory are used as a stack when calling into the WASI + const memory = instance.exports.memory; + // DataView takes care of our platform specific endian conversions + dataview = new DataView(memory.buffer); + // We must call a start method per WASI specification + // Libhydrogen's main method is one we have patched to initialise it + instance.exports._start(); + // Generated signature for JSON input + return await sign(input, context, privateKey); } } @@ -219,7 +211,7 @@ let instance, dataview; // // Helper function to reserve space in the buffer used -// as a stack between Node and the wasm. +// as a stack between js and the wasm. // // Offset must be an an object so we can update it's value // {value: n} @@ -238,182 +230,10 @@ function reserve(offset, length) { return a; } -// -// Generate a series of random numbers -// -function random_uniform() { - console.log("\n=== random_uniform ===\n"); - const { hydro_random_uniform } = instance.exports; - // Testing a simple case of passing integers and fetching integers - console.log(`generated random - ${hydro_random_uniform(20)}`); - console.log(`generated random - ${hydro_random_uniform(20)}`); - console.log(`generated random - ${hydro_random_uniform(20)}`); - console.log(`generated random - ${hydro_random_uniform(20)}`); - console.log(`generated random - ${hydro_random_uniform(20)}`); -} - -// -// Hash Generation -// -function hash() { - console.log("\n=== hash ===\n"); - const { hydro_hash_hash } = instance.exports; - const textEncoder = new TextEncoder(); - - // We have to create the stack frame to pass to libHydrogen - // in the dataview Buffer, and then pass in pointers to - // that buffer - - const offset = { value: 0 }; - const context_str = "Examples"; - const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); - const examples_ab = textEncoder.encode(context_str); - - for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { - context_arr.set([examples_ab.at(i)], i); - } - - // Our message to be hashed - const message_str = "Arbitrary data to hash"; - const message_ab = textEncoder.encode(message_str); - const message_arr = reserve(offset, message_ab.length); - - for (let i = 0; i < message_ab.length; i++) { - message_arr.set([message_ab.at(i)], i); - } - - // Buffer for libHydrogen to write the hash into - const hash = reserve(offset, hydro.hash_BYTES); - - // Call the imported function - hydro_hash_hash( - hash.byteOffset, - hash.length, - message_arr.byteOffset, - message_arr.byteLength, - context_arr.byteOffset, - null, - ); - console.log(`generated hash - ${Buffer.from(hash).toString("hex")}`); -} - -// -// Hash generation with a key -// -function keyed_hash() { - console.log("\n=== keyed_hash ===\n"); - - // Importing libhydrogen's hashing keygen and hash functions - const { hydro_hash_keygen, hydro_hash_hash } = instance.exports; - const textEncoder = new TextEncoder(); - - // We have to create the stack frame to pass to libHydrogen - // in the dataview Buffer, and then pass in pointers to - // that buffer - const offset = { value: 0 }; - const context_str = "Examples"; - const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); - const examples_ab = textEncoder.encode(context_str); - - for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { - context_arr.set([examples_ab.at(i)], i); - } - - const message_str = "Arbitrary data to hash"; - const message_ab = textEncoder.encode(message_str); - const message_arr = reserve(offset, message_ab.length); - - for (let i = 0; i < message_ab.length; i++) { - message_arr.set([message_ab.at(i)], i); - } - - // Reserve buffer space for the returned key - const key = reserve(offset, hydro.hash_KEYBYTES); - - // Reserve space for the hash result - const keyedhash = reserve(offset, hydro.hash_BYTES); - - // Generate hashing key - hydro_hash_keygen(key.byteOffset); - console.log(`generated hash key - ${key}`); - - // Create a hash with the key - hydro_hash_hash( - keyedhash.byteOffset, - keyedhash.length, - message_arr.byteOffset, - message_arr.byteLength, - context_arr.byteOffset, - key.byteOffset, - ); - const khash1 = keyedhash.toString("hex"); - console.log(`khash1 - ${khash1}`); - - keyedhash.fill(0); // Resetting output buffer (seems to pollute state otherwise) - - // Hashing message with key again - hydro_hash_hash( - keyedhash.byteOffset, - keyedhash.length, - message_arr.byteOffset, - message_arr.byteLength, - context_arr.byteOffset, - key.byteOffset, - ); - const khash2 = keyedhash.toString("hex"); - console.log(`khash2 - ${khash2}`); - keyedhash.fill(0); - console.log(`${keyedhash.toString("hex")}`); - - // Check that the same hash is generated - if (khash1 === khash2) console.log("khash1 equals khash2"); - - // Testing whether we can load a key and generate a matching hash created previously - const presetKey = "f539065185b3ce774b0c748564e804a3717ca7d0c08231076e8b7920814f0bba"; - console.log(`presetKey - ${presetKey}`); - - const historicHash = "4004516ceff97883804dbdb221baeb7283256e60165d0715d0152e4d6a6cbad4"; - console.log(`historicHash - ${historicHash}`); - - const loadedKey = new Uint8Array(presetKey.match(/../g).map(h=>parseInt(h, 16))); - for (let i = 0; i < loadedKey.length; i++) { - key.set([loadedKey.at(i)], i); - } - - // Hashing message with presetKey - hydro_hash_hash( - keyedhash.byteOffset, - keyedhash.length, - message_arr.byteOffset, - message_arr.byteLength, - context_arr.byteOffset, - key.byteOffset, - ); - - const khash3 = keyedhash.toString("hex"); - console.log(`khash3 - ${khash3}`); - - // Testing hash matches historicHash - if (khash3 === historicHash) - console.log("khash3 using old key, matches historicHash"); - - // ...and doesn't match the hashes with the latest key - if (khash1 !== khash3) - console.log("khash3 does not equal khash1"); - - // clear the buffer for the next example - context_arr.fill(0); - message_arr.fill(0); - keyedhash.fill(0); - key.fill(0); -} - // // Public key signing // -function public_key_signing() { - console.log("\n=== public_key_signing ===\n"); - +async function sign(input, context, privateKey) { // Importing libhydrogen's signing keygen and signing and verification functions const { hydro_sign_keygen, hydro_sign_create, hydro_sign_verify } = instance.exports; const textEncoder = new TextEncoder(); @@ -422,16 +242,14 @@ function public_key_signing() { // in the dataview Buffer, and then pass in pointers to // that buffer const offset = { value: 0 }; - const context_str = "Examples"; const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); - const examples_ab = textEncoder.encode(context_str); + const context_ab = textEncoder.encode(context); for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { - context_arr.set([examples_ab.at(i)], i); + context_arr.set([context_ab.at(i)], i); } - const message_str = "Arbitrary data to hash"; - const message_ab = textEncoder.encode(message_str); + const message_ab = textEncoder.encode(JSON.stringify(input)); const message_arr = reserve(offset, message_ab.length); for (let i = 0; i < message_ab.length; i++) { @@ -439,10 +257,10 @@ function public_key_signing() { } // Generate a key pair - const keypair = reserve(offset, hydro.sign_PUBLICKEYBYTES + hydro.sign_SECRETKEYBYTES); - const publicKeyOffset = keypair.byteOffset; - const privateKeyOffset = keypair.byteOffset + hydro.sign_PUBLICKEYBYTES; - hydro_sign_keygen(keypair.byteOffset); + const privateKey_arr = reserve(offset, hydro.sign_SECRETKEYBYTES); + for (let i = 0; i < privateKey.length; i++) { + privateKey_arr.set([privateKey.at(i)], i); + } // Reserving memory for the signature const signature = reserve(offset, hydro.sign_BYTES); @@ -453,356 +271,16 @@ function public_key_signing() { message_arr.byteOffset, message_arr.byteLength, context_arr.byteOffset, - privateKeyOffset, + privateKey_arr.byteOffset, ); console.log(`generated signature - ${Buffer.from(signature).toString("hex")}`); - // Verifying signature with public key - const res = hydro_sign_verify( - signature.byteOffset, - message_arr.byteOffset, - message_arr.byteLength, - context_arr.byteOffset, - publicKeyOffset, - ); - - if (res === 0) console.log("Signature is correctly valid"); - signature.set([0]); // Modifying signature - console.log(`modified signature - ${Buffer.from(signature).toString("hex")}`); - - const manipulatedRes = hydro_sign_verify( - signature.byteOffset, - message_arr.byteOffset, // Reverifying signature - message_arr.byteLength, - context_arr.byteOffset, - publicKeyOffset, - ); - if (manipulatedRes !== 0) console.log("Signature is correctly invalid"); - - message_arr.fill(0); - keypair.fill(0); - signature.fill(0); -} - -// -// Secret Key Encryption -// -function symmetric_encryption() { - console.log("\n=== symmetric_encryption ===\n"); - - // Importing libhydrogen's secretbox keygen and encrypt and decrypt functions - const { hydro_secretbox_keygen, hydro_secretbox_encrypt, hydro_secretbox_decrypt } = - instance.exports; - const textEncoder = new TextEncoder(); - - // We have to create the stack frame to pass to libHydrogen - // in the dataview Buffer, and then pass in pointers to - // that buffer - const offset = { value: 0 }; - const context_str = "Examples"; - const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); - const examples_ab = textEncoder.encode(context_str); - - for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { - context_arr.set([examples_ab.at(i)], i); - } - - // Reserving buffer for the message - const message_str = "Arbitrary data to hash"; - const message_ab = textEncoder.encode(message_str); - const message_arr = reserve(offset, message_ab.length); - - for (let i = 0; i < message_ab.length; i++) { - message_arr.set([message_ab.at(i)], i); - } - console.log(`message - ${message_arr}`); - - // Reserving buffer for the key - const key = reserve(offset, hydro.secretbox_KEYBYTES); - hydro_secretbox_keygen(key.byteOffset); - console.log(`generated key - ${key.toString("hex")}`); - - // Reserving buffer for the cipher text - const cipherTextLength = hydro.secretbox_HEADERBYTES + message_arr.length; - const ciphertext = reserve(offset, cipherTextLength); - - // Enciphering single message (thus use of msg_id 0n -- 'n' as libhydrogen expects i64) - hydro_secretbox_encrypt( - ciphertext.byteOffset, - message_arr.byteOffset, - message_arr.byteLength, - 0n, - context_arr.byteOffset, - key.byteOffset, - ); - - // Reserving buffer for the decrypted plain text - const decryptedPlaintextLength = ciphertext.byteLength - hydro.secretbox_HEADERBYTES; - const decryptedPlaintext = reserve(offset, decryptedPlaintextLength); - - // Deciphering single message (thus use of msg_id 0n -- 'n' as libhydrogen expects i64) - const res = hydro_secretbox_decrypt( - decryptedPlaintext.byteOffset, - ciphertext.byteOffset, - ciphertext.byteLength, - 0n, - context_arr.byteOffset, - key.byteOffset, - ); - if (res === 0) { - // As secretbox is an authenticated encryption (AEAD) algorithm - // we check that the ciphertext was authentic - console.log("cipherText not forged"); - // Decoding Uint8 encoded string - const textDecoder = new TextDecoder(); - console.log(`decryptedPlaintext - ${textDecoder.decode(decryptedPlaintext)}`); - } - - message_arr.fill(0); - key.fill(0); - ciphertext.fill(0); - decryptedPlaintext.fill(0); -} - -// -// Symmetric Encryption using a Key Generated -// via Asymmetric key exchange -// -function symmetric_encryption_via_asymmetric_key_exchange() { - console.log("\n=== symmetric_encryption_via_asymmetric_key_exchange ===\n"); - - const { hydro_kx_keygen, hydro_kx_kk_1, hydro_kx_kk_2, hydro_kx_kk_3 } = instance.exports; - const textEncoder = new TextEncoder(); - - // - // Reserve space in the buffer for the keypair exchange - // - // Both Alice and Bob need a keypair - // - // Both Alice and bob need a pair of session keys - // tx to encrpyt - // rx to decrypt - // - // Alice initiates the key exchange, so she also needs a kx state - // buffer - // - - const offset = { value: 0 }; - const alice = { - static: { - pk: reserve(offset, hydro.kx_PUBLICKEYBYTES), - sk: reserve(offset, hydro.kx_SECRETKEYBYTES), - }, - session: { - rx: reserve(offset, hydro.kx_SESSIONKEYBYTES), - tx: reserve(offset, hydro.kx_SESSIONKEYBYTES), - }, + return { + context, + input, + signature: Buffer.from(signature).toString("hex") }; - - const bob = { - static: { - pk: reserve(offset, hydro.kx_PUBLICKEYBYTES), - sk: reserve(offset, hydro.kx_SECRETKEYBYTES), - }, - session: { - rx: reserve(offset, hydro.kx_SESSIONKEYBYTES), - tx: reserve(offset, hydro.kx_SESSIONKEYBYTES), - }, - }; - - const keysOffset = offset.value; - alice.state = reserve(offset, hydro.kx_SESSIONKEYBYTES); - - // We need two "packets" for the messages exchanged between - // Alice and Bob during the key exchange - const packets = { - 1: reserve(offset, hydro.kx_KK_PACKET1BYTES), - 2: reserve(offset, hydro.kx_KK_PACKET2BYTES), - }; - - console.log("\n=== symmetric_encryption_via_asymmetric_key_exchange ===\n"); - // - // Generate the static keys - // - console.log("------------ALICEKEYGEN------------"); - hydro_kx_keygen(alice.static.pk.byteOffset); - console.log("---------------ALICE---------------"); - console.log(alice); - - console.log("-------------BOBKEYGEN-------------"); - hydro_kx_keygen(bob.static.pk.byteOffset); - console.log("----------------BOB----------------"); - console.log(bob); - - // - // Key exchange - // - - // Performing kk_1 (Generating alice's ephemeral keypair and packet 1) - console.log("-----------hydro_kx_kk_1-----------"); - hydro_kx_kk_1( - alice.state.byteOffset, - packets[1].byteOffset, - bob.static.pk.byteOffset, - alice.static.pk.byteOffset, - ); - console.log("---------------ALICE---------------"); - console.dir(alice, { maxArrayLength: null }); - console.log("--------------packets--------------"); - console.log(packets); - - // Performing kk_2 (Generating bob's response packet 2 and his copy of session keys) - console.log("-----------hydro_kx_kk_2-----------"); - hydro_kx_kk_2( - bob.session.rx.byteOffset, - packets[2].byteOffset, - packets[1].byteOffset, - alice.static.pk.byteOffset, - bob.static.pk.byteOffset, - ); - console.log("----------------BOB----------------"); - console.dir(bob, { maxArrayLength: null }); - console.log("--------------packets--------------"); - console.log(packets); - - // Performing kk_3 (Generating Alice's copy of session keys) - console.log("-----------hydro_kx_kk_3-----------"); - hydro_kx_kk_3( - alice.state.byteOffset, - alice.session.rx.byteOffset, - packets[2].byteOffset, - alice.static.pk.byteOffset, - ); - console.log("---------------ALICE---------------"); - console.dir(alice, { maxArrayLength: null }); - - console.log("-----------KEYS EXCHANGED----------"); - - // Tidy up the buffer space used in the key exchange - alice.state.fill(0); - packets[1].fill(0); - packets[2].fill(0); - offset.value = keysOffset; - - // - // Use the session keys to exchange messages - // - const { hydro_secretbox_encrypt, hydro_secretbox_decrypt } = instance.exports; - - const context_str = "Examples"; - const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); - const examples_ab = textEncoder.encode(context_str); - - for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { - context_arr.set([examples_ab.at(i)], i); - } - - const message1_str = "Hello Bob"; - const message1_ab = textEncoder.encode(message1_str); - const message1_arr = reserve(offset, message1_ab.length); - - for (let i = 0; i < message1_ab.length; i++) { - message1_arr.set([message1_ab.at(i)], i); - } - - console.log(`message1_arr - ${message1_arr}`); - - const ciphertext1_length = hydro.secretbox_HEADERBYTES + message1_arr.length; - const ciphertext1_arr = reserve(offset, ciphertext1_length); - - // - // Alice Encypts the message with her session tx key - // - // Enciphering single message (thus use of msg_id 0n -- 'n' as libhydrogen expects i64) - // - hydro_secretbox_encrypt( - ciphertext1_arr.byteOffset, - message1_arr.byteOffset, - message1_arr.byteLength, - 0n, - context_arr.byteOffset, - alice.session.tx.byteOffset, - ); - - // - // Bob Decrypts the mesaage with his session rx key - // - // Deciphering single message (thus use of msg_id 0n -- 'n' as libhydrogen expects i64) - // - const decryptedPlaintext1Length = ciphertext1_arr.byteLength - hydro.secretbox_HEADERBYTES; - const decryptedPlaintext1 = reserve(offset, decryptedPlaintext1Length); - - const res1 = hydro_secretbox_decrypt( - decryptedPlaintext1.byteOffset, - ciphertext1_arr.byteOffset, - ciphertext1_arr.byteLength, - 0n, - context_arr.byteOffset, - bob.session.rx.byteOffset, - ); - - // As secretbox is an authenticated encryption (AEAD) algorithm - // we check that the ciphertext was authentic - if (res1 === 0) { - console.log("ciphertext1_arr not forged"); - // Decoding Uint8 encoded string - const textDecoder = new TextDecoder(); - console.log(`decryptedPlaintext1 - ${textDecoder.decode(decryptedPlaintext1)}`); - } - - // - // Bob sends a reply to Alice - // - const message2_str = "Hello Alice"; - const message2_ab = textEncoder.encode(message2_str); - const message2_arr = reserve(offset, message2_ab.length); - - for (let i = 0; i < message2_ab.length; i++) { - message2_arr.set([message2_ab.at(i)], i); - } - - console.log(`message2_arr - ${message2_arr}`); - - const ciphertext2Length = hydro.secretbox_HEADERBYTES + message2_arr.length; - const ciphertext2_arr = reserve(offset, ciphertext2Length); - - // - // Bob encrypts the message with his session tx key - // - hydro_secretbox_encrypt( - ciphertext2_arr.byteOffset, - message2_arr.byteOffset, - message2_arr.byteLength, - 0n, - context_arr.byteOffset, - bob.session.tx.byteOffset, - ); - - // - // Alice decrypts the message with her session rx key - // - const decryptedPlaintext2Length = ciphertext2_arr.byteLength - hydro.secretbox_HEADERBYTES; - const decryptedPlaintext2_arr = reserve(offset, decryptedPlaintext2Length); - - const res2 = hydro_secretbox_decrypt( - decryptedPlaintext2_arr.byteOffset, - ciphertext2_arr.byteOffset, - ciphertext2_arr.byteLength, - 0n, - context_arr.byteOffset, - alice.session.rx.byteOffset, - ); - - // As secretbox is an authenticated encryption (AEAD) algorithm - // we check that the ciphertext was authentic - if (res2 === 0) { - console.log("ciphertext2 not forged"); - const textDecoder = new TextDecoder(); - // Decoding Uint8 encoded string - console.log(`decryptedPlaintext2 - ${textDecoder.decode(decryptedPlaintext2_arr)}`); - } } - export default LibHydrogenCurve25519Signing; diff --git a/webpack.config.js b/webpack.config.js index 96efc941..62931a84 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -83,9 +83,9 @@ module.exports = { from: "prime.worker.min.js", to: "assets/forge/" }, { - context: "node_modules/libhydrogen/", + context: "node_modules/@configuredthings/libhydrogen-wasm/", from: "libhydrogen.wasm", - to: "assets/libhydrogen" + to: "assets/libhydrogen-wasm" }, ] }), From 26c37ea522e7ec0f9cc1d257b50fa61c1816c897 Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:18:29 +0200 Subject: [PATCH 05/10] added marshalling operation --- ...igitalSecurityByDesignSignedJSONToText.mjs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/core/operations/ConfiguredThingsDigitalSecurityByDesignSignedJSONToText.mjs diff --git a/src/core/operations/ConfiguredThingsDigitalSecurityByDesignSignedJSONToText.mjs b/src/core/operations/ConfiguredThingsDigitalSecurityByDesignSignedJSONToText.mjs new file mode 100644 index 00000000..b0c82fe8 --- /dev/null +++ b/src/core/operations/ConfiguredThingsDigitalSecurityByDesignSignedJSONToText.mjs @@ -0,0 +1,52 @@ +/** + * @author Configured Things Ltd. [getconfigured@configuredthings.com] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Configured Things - Digital Security by Design - Signed JSON to Text operation + */ +class ConfiguredThingsDigitalSecurityByDesignSignedJSONToText extends Operation { + + /** + * ConfiguredThingsDigitalSecurityByDesignSignedJSONToText constructor + */ + constructor() { + super(); + + this.name = "Configured Things - Digital Security by Design - LibHydrogen Signed JSON to Text"; + this.module = "Default"; + this.description = "Converts signed JSON to text"; + this.infoURL = ""; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc) + this.inputType = "JSON"; + this.outputType = "ArrayBuffer"; + this.args = [ + ]; + } + + /** + * @param {JSON} input + * @param {Object[]} args + * @returns {ArrayBuffer} + */ + run(input, args) { + const properties = ["context", "signature", "input"]; + const output = properties.reduce((previousBytes, prop) => { + if (!Object.hasOwn(input, prop)) { + throw new OperationError(`Input missing '${prop}' property`); + } else { + const combinedBytes = new Uint8Array(previousBytes.byteLength + input[prop].byteLength); + combinedBytes.set(previousBytes); + combinedBytes.set(input[prop], previousBytes.byteLength); + return combinedBytes; + } + }, new Uint8Array()); + return output.buffer; + } +} + +export default ConfiguredThingsDigitalSecurityByDesignSignedJSONToText; From 5e403cf06865d5bebe75e8e53fb75f94eb6fd17f Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:19:19 +0200 Subject: [PATCH 06/10] remove lint overrides --- .../operations/LibHydrogenCurve25519Sign.mjs | 100 ++++++++++-------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/src/core/operations/LibHydrogenCurve25519Sign.mjs b/src/core/operations/LibHydrogenCurve25519Sign.mjs index 5b2ad355..d8f9ad90 100644 --- a/src/core/operations/LibHydrogenCurve25519Sign.mjs +++ b/src/core/operations/LibHydrogenCurve25519Sign.mjs @@ -1,10 +1,6 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable camelcase */ -/* eslint-disable jsdoc/require-jsdoc */ -/* eslint-disable no-console */ /** * @author Configured Things Ltd. [getconfigured@configuredthings.com] - * @copyright Crown Copyright 2024 + * @copyright Crown Copyright 2025 * @license Apache-2.0 */ @@ -43,6 +39,7 @@ class LibHydrogenCurve25519Signing extends Operation { ]; } + /* eslint-disable camelcase */ /** * @param {JSON} input * @param {Object[]} args @@ -50,8 +47,8 @@ class LibHydrogenCurve25519Signing extends Operation { */ async run(input, args) { const [context, privateKey] = args; - const wasm_src = await fetch(new URL(`${self.docURL}/assets/libhydrogen-wasm/libhydrogen.wasm`)); - const wasm = await WebAssembly.compileStreaming(wasm_src); + const wasmSrc = await fetch(new URL(`${self.docURL}/assets/libhydrogen-wasm/libhydrogen.wasm`)); + const wasm = await WebAssembly.compileStreaming(wasmSrc); const imports = { "wasi_snapshot_preview1": { args_get() { @@ -204,25 +201,28 @@ class LibHydrogenCurve25519Signing extends Operation { // Generated signature for JSON input return await sign(input, context, privateKey); } + /* eslint-enable camelcase */ } let instance, dataview; -// -// Helper function to reserve space in the buffer used -// as a stack between js and the wasm. -// -// Offset must be an an object so we can update it's value -// {value: n} -// -// Designed to allow a sequence of reservations such as -// -// let offset = {value: 0}; -// buf1 = reserve (offset, 100); -// buf2 = reserve (offset, 590); -// ... -// +/** + * Helper function to reserve space in the buffer used + * as a stack between js and the wasm. + * @param {Object} offset - Must be an an object so we can update it's value + * @param {number} offset.value + * @param {number} length + * @returns {Uint8Array} Returns a UInt8Array representation of the buffer + * + * @example + * // Designed to allow a sequence of reservations such as + * + * let offset = {value: 0}; + * buf1 = reserve (offset, 100); + * buf2 = reserve (offset, 590); + * ... + */ function reserve(offset, length) { const a = new Uint8Array(dataview.buffer, offset.value, length); const newOffset = a.byteOffset + a.byteLength; @@ -230,36 +230,52 @@ function reserve(offset, length) { return a; } -// -// Public key signing -// +/** + * A signed JSON object. + * @typedef {Object} SignedJSON + * @property {Uint8Array} context - A buffer representing the context used to define the context of the signing operation, + see {@link https://github.com/jedisct1/libhydrogen/wiki/Contexts} + * @property {Uint8Array} input - A buffer representing the stringified JSON object used as the input to the signing operation + * @property {Uint8Array} signature - A buffer representing the digital signature of the signed JSON object + */ + +/** + * Digital signing of a JSON object + * @param {JSON} input - A JSON object to be signed + * @param {string} context - A string used to define the context of the signing operation, + * see {@link https://github.com/jedisct1/libhydrogen/wiki/Contexts} + * @param {Uint8Array} privateKey - The private key to use for the digital signing operation + * @returns {SignedJSON} A signed JSON object + */ async function sign(input, context, privateKey) { // Importing libhydrogen's signing keygen and signing and verification functions - const { hydro_sign_keygen, hydro_sign_create, hydro_sign_verify } = instance.exports; + // eslint-disable-next-line camelcase + const { hydro_sign_create } = instance.exports; const textEncoder = new TextEncoder(); // We have to create the stack frame to pass to libHydrogen // in the dataview Buffer, and then pass in pointers to // that buffer const offset = { value: 0 }; - const context_arr = reserve(offset, hydro.hash_CONTEXTBYTES); - const context_ab = textEncoder.encode(context); + const contextArr = reserve(offset, hydro.hash_CONTEXTBYTES); + const contextAb = textEncoder.encode(context); for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { - context_arr.set([context_ab.at(i)], i); + contextArr.set([contextAb.at(i)], i); } - const message_ab = textEncoder.encode(JSON.stringify(input)); - const message_arr = reserve(offset, message_ab.length); + const messageStr = JSON.stringify(input); + const messageAb = textEncoder.encode(messageStr); + const messageArr = reserve(offset, messageAb.length); - for (let i = 0; i < message_ab.length; i++) { - message_arr.set([message_ab.at(i)], i); + for (let i = 0; i < messageAb.length; i++) { + messageArr.set([messageAb.at(i)], i); } // Generate a key pair - const privateKey_arr = reserve(offset, hydro.sign_SECRETKEYBYTES); + const privateKeyArr = reserve(offset, hydro.sign_SECRETKEYBYTES); for (let i = 0; i < privateKey.length; i++) { - privateKey_arr.set([privateKey.at(i)], i); + privateKeyArr.set([privateKey.at(i)], i); } // Reserving memory for the signature @@ -268,18 +284,16 @@ async function sign(input, context, privateKey) { // Creating signature of message with secret key hydro_sign_create( signature.byteOffset, - message_arr.byteOffset, - message_arr.byteLength, - context_arr.byteOffset, - privateKey_arr.byteOffset, + messageArr.byteOffset, + messageArr.byteLength, + contextArr.byteOffset, + privateKeyArr.byteOffset, ); - console.log(`generated signature - ${Buffer.from(signature).toString("hex")}`); - return { - context, - input, - signature: Buffer.from(signature).toString("hex") + context: contextArr, + input: messageArr, + signature }; } From 08d66388a7d103afd07146fe53501b23c3f96444 Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:19:40 +0200 Subject: [PATCH 07/10] corrected types to support marshalled output --- src/core/operations/MQTTPublish.mjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/operations/MQTTPublish.mjs b/src/core/operations/MQTTPublish.mjs index af2de3bf..bf9751ee 100644 --- a/src/core/operations/MQTTPublish.mjs +++ b/src/core/operations/MQTTPublish.mjs @@ -1,6 +1,6 @@ /** * @author Configured Things Ltd. [getconfigured@configuredthings.com] - * @copyright Crown Copyright 2024 + * @copyright Crown Copyright 2025 * @license Apache-2.0 */ @@ -24,8 +24,8 @@ class MQTTPublish extends Operation { this.module = "MQTT"; this.description = "Publishes a message to an MQTT broker"; this.infoURL = "https://en.wikipedia.org/wiki/MQTT"; - this.inputType = "String"; - this.outputType = "String"; + this.inputType = "ArrayBuffer"; + this.outputType = "ArrayBuffer"; this.args = [ { name: "MQTT broker URL", @@ -41,19 +41,19 @@ class MQTTPublish extends Operation { } /** - * @param {String} input + * @param {ArrayBuffer} input * @param {Object[]} args - * @returns {String} + * @returns {ArrayBuffer} */ run(input, args) { const [broker, topic] = args; - const mqttUrlRegex = /^(ws|mqtt)s?:\/\/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*|([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])(\.([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])){3}|(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])))(:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$/; + const mqttUrlRegex = /^(ws|mqtt)s?:\/\/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*|([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])(\.([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])){3}|(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])))(:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?/; const mqttTopicRegex = /^[\u0000-\uFFFF]+(\/[\u0000-\uFFFF]+)+$/; if (mqttUrlRegex.test(broker)) { if (mqttTopicRegex.test(broker)) { const client = mqtt.connect(broker); client.on("connect", () => { - client.publish(topic, input); + client.publish(topic, Buffer.from(input)); client.end(); }); } else throw new OperationError(`Invalid MQTT topic name - ${topic}`); From 0ebafdc7f20dfce251a95c437d688e6e23e1ff37 Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Wed, 29 Jan 2025 21:28:13 +0200 Subject: [PATCH 08/10] added json to ab helper op --- src/core/operations/JSONToArrayBuffer.mjs | 44 +++++++++++++++++++ .../operations/LibHydrogenCurve25519Sign.mjs | 17 ++++--- ...ToText.mjs => SignedJSONToArrayBuffer.mjs} | 8 ++-- 3 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 src/core/operations/JSONToArrayBuffer.mjs rename src/core/operations/{ConfiguredThingsDigitalSecurityByDesignSignedJSONToText.mjs => SignedJSONToArrayBuffer.mjs} (81%) diff --git a/src/core/operations/JSONToArrayBuffer.mjs b/src/core/operations/JSONToArrayBuffer.mjs new file mode 100644 index 00000000..09c21582 --- /dev/null +++ b/src/core/operations/JSONToArrayBuffer.mjs @@ -0,0 +1,44 @@ +/** + * @author Configured Things Ltd. [getconfigured@configuredthings.com] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * JSON to ArrayBuffer operation + */ +class JSONToArrayBuffer extends Operation { + + /** + * JSONToArrayBuffer constructor + */ + constructor() { + super(); + + this.name = "JSON to ArrayBuffer"; + this.module = "Default"; + this.description = "Serialises a JSON object to an ArrayBuffer"; + this.infoURL = ""; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc) + this.inputType = "JSON"; + this.outputType = "ArrayBuffer"; + this.args = [ + ]; + } + + /** + * @param {JSON} input + * @param {Object[]} args + * @returns {ArrayBuffer} + */ + run(input, args) { + const messageStr = JSON.stringify(input); + const textEncoder = new TextEncoder(); + const messageAb = textEncoder.encode(messageStr).buffer; + return messageAb; + } + +} + +export default JSONToArrayBuffer; diff --git a/src/core/operations/LibHydrogenCurve25519Sign.mjs b/src/core/operations/LibHydrogenCurve25519Sign.mjs index d8f9ad90..fd21426d 100644 --- a/src/core/operations/LibHydrogenCurve25519Sign.mjs +++ b/src/core/operations/LibHydrogenCurve25519Sign.mjs @@ -23,7 +23,7 @@ class LibHydrogenCurve25519Signing extends Operation { this.module = "Crypto"; this.description = "Computes a signature for a message using the lightweight LibHydrogen cryptography library"; this.infoURL = "https://libhydrogen.org/"; - this.inputType = "JSON"; + this.inputType = "ArrayBuffer"; this.outputType = "JSON"; this.args = [ { @@ -41,7 +41,7 @@ class LibHydrogenCurve25519Signing extends Operation { /* eslint-disable camelcase */ /** - * @param {JSON} input + * @param {ArrayBuffer} input * @param {Object[]} args * @returns {JSON} */ @@ -240,8 +240,8 @@ function reserve(offset, length) { */ /** - * Digital signing of a JSON object - * @param {JSON} input - A JSON object to be signed + * Digital signing of an ArrayBuffer's contents + * @param {ArrayBuffer} input - An ArrayBuffer to be signed * @param {string} context - A string used to define the context of the signing operation, * see {@link https://github.com/jedisct1/libhydrogen/wiki/Contexts} * @param {Uint8Array} privateKey - The private key to use for the digital signing operation @@ -264,12 +264,11 @@ async function sign(input, context, privateKey) { contextArr.set([contextAb.at(i)], i); } - const messageStr = JSON.stringify(input); - const messageAb = textEncoder.encode(messageStr); - const messageArr = reserve(offset, messageAb.length); + const messageTypedArr = new Uint8Array(input); + const messageArr = reserve(offset, messageTypedArr.length); - for (let i = 0; i < messageAb.length; i++) { - messageArr.set([messageAb.at(i)], i); + for (let i = 0; i < input.length; i++) { + messageArr.set([messageTypedArr.at(i)], i); } // Generate a key pair diff --git a/src/core/operations/ConfiguredThingsDigitalSecurityByDesignSignedJSONToText.mjs b/src/core/operations/SignedJSONToArrayBuffer.mjs similarity index 81% rename from src/core/operations/ConfiguredThingsDigitalSecurityByDesignSignedJSONToText.mjs rename to src/core/operations/SignedJSONToArrayBuffer.mjs index b0c82fe8..87d61a9d 100644 --- a/src/core/operations/ConfiguredThingsDigitalSecurityByDesignSignedJSONToText.mjs +++ b/src/core/operations/SignedJSONToArrayBuffer.mjs @@ -10,15 +10,15 @@ import OperationError from "../errors/OperationError.mjs"; /** * Configured Things - Digital Security by Design - Signed JSON to Text operation */ -class ConfiguredThingsDigitalSecurityByDesignSignedJSONToText extends Operation { +class SignedJSONToArrayBuffer extends Operation { /** - * ConfiguredThingsDigitalSecurityByDesignSignedJSONToText constructor + * SignedJSONToArrayBuffer constructor */ constructor() { super(); - this.name = "Configured Things - Digital Security by Design - LibHydrogen Signed JSON to Text"; + this.name = "Signed JSON to ArrayBuffer"; this.module = "Default"; this.description = "Converts signed JSON to text"; this.infoURL = ""; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc) @@ -49,4 +49,4 @@ class ConfiguredThingsDigitalSecurityByDesignSignedJSONToText extends Operation } } -export default ConfiguredThingsDigitalSecurityByDesignSignedJSONToText; +export default SignedJSONToArrayBuffer; From ba9127c9c885c8bf4e5333bcd34f616a0e787bc1 Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Wed, 29 Jan 2025 22:20:53 +0200 Subject: [PATCH 09/10] correcting loop --- src/core/operations/LibHydrogenCurve25519Sign.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/LibHydrogenCurve25519Sign.mjs b/src/core/operations/LibHydrogenCurve25519Sign.mjs index fd21426d..c58b4a99 100644 --- a/src/core/operations/LibHydrogenCurve25519Sign.mjs +++ b/src/core/operations/LibHydrogenCurve25519Sign.mjs @@ -267,7 +267,7 @@ async function sign(input, context, privateKey) { const messageTypedArr = new Uint8Array(input); const messageArr = reserve(offset, messageTypedArr.length); - for (let i = 0; i < input.length; i++) { + for (let i = 0; i < messageTypedArr.length; i++) { messageArr.set([messageTypedArr.at(i)], i); } From 557d0b7567dae52ded03e5446e810e323faabe55 Mon Sep 17 00:00:00 2001 From: Simon Arnell <14029547+simonarnell@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:32:39 +0200 Subject: [PATCH 10/10] added funding credit UKRI DSbD --- src/core/operations/JSONToArrayBuffer.mjs | 4 +++- .../operations/LibHydrogenCurve25519Sign.mjs | 18 +++++++++++------- src/core/operations/MQTTPublish.mjs | 4 +++- ...uffer.mjs => SignedObjectToArrayBuffer.mjs} | 14 ++++++++------ 4 files changed, 25 insertions(+), 15 deletions(-) rename src/core/operations/{SignedJSONToArrayBuffer.mjs => SignedObjectToArrayBuffer.mjs} (68%) diff --git a/src/core/operations/JSONToArrayBuffer.mjs b/src/core/operations/JSONToArrayBuffer.mjs index 09c21582..4e1c41de 100644 --- a/src/core/operations/JSONToArrayBuffer.mjs +++ b/src/core/operations/JSONToArrayBuffer.mjs @@ -1,5 +1,7 @@ /** - * @author Configured Things Ltd. [getconfigured@configuredthings.com] + * @file Developed by {@link https://configuredthings.com Configured Things} with funding from the {@link https://www.ukri.org UKRI} + * {@link https://www.dsbd.tech Digital Security by Design} program. + * @author Configured Things Ltd. * @copyright Crown Copyright 2025 * @license Apache-2.0 */ diff --git a/src/core/operations/LibHydrogenCurve25519Sign.mjs b/src/core/operations/LibHydrogenCurve25519Sign.mjs index c58b4a99..7b5ea53c 100644 --- a/src/core/operations/LibHydrogenCurve25519Sign.mjs +++ b/src/core/operations/LibHydrogenCurve25519Sign.mjs @@ -1,5 +1,7 @@ /** - * @author Configured Things Ltd. [getconfigured@configuredthings.com] + * @file Developed by {@link https://configuredthings.com Configured Things} with funding from the {@link https://www.ukri.org UKRI} + * {@link https://www.dsbd.tech Digital Security by Design} program. + * @author Configured Things Ltd. * @copyright Crown Copyright 2025 * @license Apache-2.0 */ @@ -210,6 +212,7 @@ let instance, dataview; /** * Helper function to reserve space in the buffer used * as a stack between js and the wasm. + * @private * @param {Object} offset - Must be an an object so we can update it's value * @param {number} offset.value * @param {number} length @@ -231,21 +234,22 @@ function reserve(offset, length) { } /** - * A signed JSON object. - * @typedef {Object} SignedJSON + * An object containing the signature, the ArrayBuffer used to generate the signature, and the signing operation's context + * @typedef {Object} SignedObject * @property {Uint8Array} context - A buffer representing the context used to define the context of the signing operation, see {@link https://github.com/jedisct1/libhydrogen/wiki/Contexts} - * @property {Uint8Array} input - A buffer representing the stringified JSON object used as the input to the signing operation - * @property {Uint8Array} signature - A buffer representing the digital signature of the signed JSON object - */ +* @property {Uint8Array} input - A buffer representing the stringified JSON object used as the input to the signing operation +* @property {Uint8Array} signature - A buffer representing the digital signature of the signed JSON object +*/ /** * Digital signing of an ArrayBuffer's contents + * @private * @param {ArrayBuffer} input - An ArrayBuffer to be signed * @param {string} context - A string used to define the context of the signing operation, * see {@link https://github.com/jedisct1/libhydrogen/wiki/Contexts} * @param {Uint8Array} privateKey - The private key to use for the digital signing operation - * @returns {SignedJSON} A signed JSON object + * @returns {SignedObject} An object containing the signature, the ArrayBuffer used to generate the signature, and the signing operation's context */ async function sign(input, context, privateKey) { // Importing libhydrogen's signing keygen and signing and verification functions diff --git a/src/core/operations/MQTTPublish.mjs b/src/core/operations/MQTTPublish.mjs index bf9751ee..fdabf119 100644 --- a/src/core/operations/MQTTPublish.mjs +++ b/src/core/operations/MQTTPublish.mjs @@ -1,5 +1,7 @@ /** - * @author Configured Things Ltd. [getconfigured@configuredthings.com] + * @file Developed by {@link https://configuredthings.com Configured Things} with funding from the {@link https://www.ukri.org UKRI} + * {@link https://www.dsbd.tech Digital Security by Design} program. + * @author Configured Things Ltd. * @copyright Crown Copyright 2025 * @license Apache-2.0 */ diff --git a/src/core/operations/SignedJSONToArrayBuffer.mjs b/src/core/operations/SignedObjectToArrayBuffer.mjs similarity index 68% rename from src/core/operations/SignedJSONToArrayBuffer.mjs rename to src/core/operations/SignedObjectToArrayBuffer.mjs index 87d61a9d..35a1fd98 100644 --- a/src/core/operations/SignedJSONToArrayBuffer.mjs +++ b/src/core/operations/SignedObjectToArrayBuffer.mjs @@ -1,5 +1,7 @@ /** - * @author Configured Things Ltd. [getconfigured@configuredthings.com] + * @file Developed by {@link https://configuredthings.com Configured Things} with funding from the {@link https://www.ukri.org UKRI} + * {@link https://www.dsbd.tech Digital Security by Design} program. + * @author Configured Things Ltd. * @copyright Crown Copyright 2025 * @license Apache-2.0 */ @@ -8,9 +10,9 @@ import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; /** - * Configured Things - Digital Security by Design - Signed JSON to Text operation + * Signed Object to ArrayBuffer operation */ -class SignedJSONToArrayBuffer extends Operation { +class SignedObjectToArrayBuffer extends Operation { /** * SignedJSONToArrayBuffer constructor @@ -18,9 +20,9 @@ class SignedJSONToArrayBuffer extends Operation { constructor() { super(); - this.name = "Signed JSON to ArrayBuffer"; + this.name = "Signed Object to ArrayBuffer"; this.module = "Default"; - this.description = "Converts signed JSON to text"; + this.description = "Converts a digitally signed object created by, for example, the 'LibHydrogen Curve25519 Sign' operation to an ArrayBuffer"; this.infoURL = ""; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc) this.inputType = "JSON"; this.outputType = "ArrayBuffer"; @@ -49,4 +51,4 @@ class SignedJSONToArrayBuffer extends Operation { } } -export default SignedJSONToArrayBuffer; +export default SignedObjectToArrayBuffer;