diff --git a/package-lock.json b/package-lock.json index 9b24b981..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", @@ -2069,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", diff --git a/package.json b/package.json index a1470dee..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", 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/JSONToArrayBuffer.mjs b/src/core/operations/JSONToArrayBuffer.mjs new file mode 100644 index 00000000..4e1c41de --- /dev/null +++ b/src/core/operations/JSONToArrayBuffer.mjs @@ -0,0 +1,46 @@ +/** + * @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 + */ + +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 new file mode 100644 index 00000000..7b5ea53c --- /dev/null +++ b/src/core/operations/LibHydrogenCurve25519Sign.mjs @@ -0,0 +1,303 @@ +/** + * @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 + */ + +import Operation from "../Operation.mjs"; + +import * as hydro from "@configuredthings/libhydrogen-wasm/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 = "ArrayBuffer"; + this.outputType = "JSON"; + this.args = [ + { + name: "Context", + type: "string", + value: "" + }, + { + name: "Sender's private key", + type: "byteArray", + value: "" + } + ]; + } + + /* eslint-disable camelcase */ + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {JSON} + */ + async run(input, args) { + const [context, privateKey] = args; + 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() { + 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); + } + /* eslint-enable camelcase */ + +} + +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 + * @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; + offset.value = newOffset; + return a; +} + +/** + * 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 +*/ + +/** + * 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 {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 + // 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 contextArr = reserve(offset, hydro.hash_CONTEXTBYTES); + const contextAb = textEncoder.encode(context); + + for (let i = 0; i < hydro.hash_CONTEXTBYTES; i++) { + contextArr.set([contextAb.at(i)], i); + } + + const messageTypedArr = new Uint8Array(input); + const messageArr = reserve(offset, messageTypedArr.length); + + for (let i = 0; i < messageTypedArr.length; i++) { + messageArr.set([messageTypedArr.at(i)], i); + } + + // Generate a key pair + const privateKeyArr = reserve(offset, hydro.sign_SECRETKEYBYTES); + for (let i = 0; i < privateKey.length; i++) { + privateKeyArr.set([privateKey.at(i)], i); + } + + // Reserving memory for the signature + const signature = reserve(offset, hydro.sign_BYTES); + + // Creating signature of message with secret key + hydro_sign_create( + signature.byteOffset, + messageArr.byteOffset, + messageArr.byteLength, + contextArr.byteOffset, + privateKeyArr.byteOffset, + ); + + return { + context: contextArr, + input: messageArr, + signature + }; +} + +export default LibHydrogenCurve25519Signing; diff --git a/src/core/operations/MQTTPublish.mjs b/src/core/operations/MQTTPublish.mjs index af2de3bf..fdabf119 100644 --- a/src/core/operations/MQTTPublish.mjs +++ b/src/core/operations/MQTTPublish.mjs @@ -1,6 +1,8 @@ /** - * @author Configured Things Ltd. [getconfigured@configuredthings.com] - * @copyright Crown Copyright 2024 + * @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 */ @@ -24,8 +26,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 +43,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}`); diff --git a/src/core/operations/SignedObjectToArrayBuffer.mjs b/src/core/operations/SignedObjectToArrayBuffer.mjs new file mode 100644 index 00000000..35a1fd98 --- /dev/null +++ b/src/core/operations/SignedObjectToArrayBuffer.mjs @@ -0,0 +1,54 @@ +/** + * @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 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Signed Object to ArrayBuffer operation + */ +class SignedObjectToArrayBuffer extends Operation { + + /** + * SignedJSONToArrayBuffer constructor + */ + constructor() { + super(); + + this.name = "Signed Object to ArrayBuffer"; + this.module = "Default"; + 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"; + 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 SignedObjectToArrayBuffer; diff --git a/webpack.config.js b/webpack.config.js index e77f2ab9..62931a84 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/@configuredthings/libhydrogen-wasm/", + from: "libhydrogen.wasm", + to: "assets/libhydrogen-wasm" + }, ] }), new ModifySourcePlugin({