diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index c8f83f95e6..dc3f8c2bfa 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -402,7 +402,9 @@ "CRC-8 Checksum", "CRC-16 Checksum", "CRC-32 Checksum", - "TCP/IP Checksum" + "TCP/IP Checksum", + "FNV-1", + "FNV-1a" ] }, { diff --git a/src/core/lib/FNV.mjs b/src/core/lib/FNV.mjs new file mode 100644 index 0000000000..950b3ad463 --- /dev/null +++ b/src/core/lib/FNV.mjs @@ -0,0 +1,65 @@ +/** + * FNV resources. + * + * @license CC0-1.0 + */ + +// http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-param +export const FNV_OPTIONS = { + 32: { + prime: 0x01000193n, + init: 0x811c9dc5n, + size: 32 + }, + 64: { + prime: 0x100000001b3n, + init: 0xcbf29ce484222325n, + size: 64 + }, + 128: { + prime: 0x0000000001000000000000000000013Bn, + init: 0x6c62272e07bb014262b821756295c58dn, + size: 128 + }, + 256: { + prime: 0x0000000000000000000001000000000000000000000000000000000000000163n, + init: 0xdd268dbcaac550362d98c384c4e576ccc8b1536847b6bbb31023b4c8caee0535n, + size: 256 + } +}; + +/** + * Computes a FNV-1 hash of the data + * + * @param {Uint8Array} data + * @param {Object} options + * @returns {BigInt} + */ +export function fnv1(data, options) { + let hash = options.init; + + for (let i = 0; i < data.length; i++) { + hash *= options.prime; + hash = BigInt.asUintN(options.size, hash ^ BigInt(data[i])); + } + + return hash; +} + +/** + * Computes a FNV-1a hash of the data + * + * @param {Uint8Array} data + * @param {Object} options + * @returns {BigInt} + */ +export function fnv1a(data, options) { + let hash = options.init; + + for (let i = 0; i < data.length; i++) { + hash ^= BigInt(data[i]); + hash = BigInt.asUintN(options.size, hash * options.prime); + } + + return hash; +} diff --git a/src/core/operations/FNV1.mjs b/src/core/operations/FNV1.mjs new file mode 100644 index 0000000000..83caeaf0ca --- /dev/null +++ b/src/core/operations/FNV1.mjs @@ -0,0 +1,56 @@ +/** + * @author TheIndra55 [theindra@protonmail.com] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; +import {fnv1, FNV_OPTIONS} from "../lib/FNV.mjs"; + +/** + * FNV-1 operation + */ +class FNV1 extends Operation { + + /** + * FNV1 constructor + */ + constructor() { + super(); + + this.name = "FNV-1"; + this.module = "Default"; + this.description = "Fowler-Noll-Vo (or FNV) is a non-cryptographic hash function created by Glenn Fowler, Landon Curt Noll, and Kiem-Phong Vo."; + this.infoURL = "https://wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1_hash"; + this.inputType = "ArrayBuffer"; + this.outputType = "string"; + this.args = [ + { + name: "Size", + type: "option", + value: ["32", "64", "128", "256"], + defaultIndex: 0 + } + ]; + } + + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const size = Number(args[0]), + options = FNV_OPTIONS[size]; + + input = new Uint8Array(input); + + const hash = fnv1(input, options); + + return Utils.hex(hash); + } + +} + +export default FNV1; diff --git a/src/core/operations/FNV1a.mjs b/src/core/operations/FNV1a.mjs new file mode 100644 index 0000000000..1ca098b619 --- /dev/null +++ b/src/core/operations/FNV1a.mjs @@ -0,0 +1,56 @@ +/** + * @author TheIndra55 [theindra@protonmail.com] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; +import {fnv1a, FNV_OPTIONS} from "../lib/FNV.mjs"; + +/** + * FNV-1a operation + */ +class FNV1a extends Operation { + + /** + * FNV1a constructor + */ + constructor() { + super(); + + this.name = "FNV-1a"; + this.module = "Default"; + this.description = "Fowler-Noll-Vo (or FNV) is a non-cryptographic hash function created by Glenn Fowler, Landon Curt Noll, and Kiem-Phong Vo."; + this.infoURL = "https://wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash"; + this.inputType = "ArrayBuffer"; + this.outputType = "string"; + this.args = [ + { + name: "Size", + type: "option", + value: ["32", "64", "128", "256"], + defaultIndex: 0 + } + ]; + } + + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const size = Number(args[0]), + options = FNV_OPTIONS[size]; + + input = new Uint8Array(input); + + const hash = fnv1a(input, options); + + return Utils.hex(hash); + } + +} + +export default FNV1a; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index e61f542d9e..00fdb62d34 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -136,6 +136,7 @@ import "./tests/SwapCase.mjs"; import "./tests/HKDF.mjs"; import "./tests/GenerateDeBruijnSequence.mjs"; import "./tests/GOST.mjs"; +import "./tests/FNV.mjs"; // Cannot test operations that use the File type yet // import "./tests/SplitColourChannels.mjs"; diff --git a/tests/operations/tests/FNV.mjs b/tests/operations/tests/FNV.mjs new file mode 100644 index 0000000000..eeaa17171b --- /dev/null +++ b/tests/operations/tests/FNV.mjs @@ -0,0 +1,100 @@ +/** + * FNV tests + * + * @author TheIndra55 [theindra@protonmail.com] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "FNV-1: 32-bit", + input: "Hello, World", + expectedOutput: "57c1791d", + recipeConfig: [ + { + op: "FNV-1", + args: ["32"] + } + ] + }, + { + name: "FNV-1: 64-bit", + input: "Hello, World", + expectedOutput: "7b7fdc56f6a11b3d", + recipeConfig: [ + { + op: "FNV-1", + args: ["64"] + } + ] + }, + { + name: "FNV-1: 128-bit", + input: "Hello, World", + expectedOutput: "19922cf5ab67a18bcfb7e8f3c7ccd435", + recipeConfig: [ + { + op: "FNV-1", + args: ["128"] + } + ] + }, + { + name: "FNV-1: 256-bit", + input: "Hello, World", + expectedOutput: "3e3bfd5f1d1c0be4887134fce95e52c0f33f2931081c26330bdd7780eeb2ead", + recipeConfig: [ + { + op: "FNV-1", + args: ["256"] + } + ] + }, + { + name: "FNV-1a: 32-bit", + input: "Hello, World", + expectedOutput: "66d37c5d", + recipeConfig: [ + { + op: "FNV-1a", + args: ["32"] + } + ] + }, + { + name: "FNV-1a: 64-bit", + input: "Hello, World", + expectedOutput: "a28e0387da37a07d", + recipeConfig: [ + { + op: "FNV-1a", + args: ["64"] + } + ] + }, + { + name: "FNV-1a: 128-bit", + input: "Hello, World", + expectedOutput: "5c9ecbd668e872034facb07b72b3a0c5", + recipeConfig: [ + { + op: "FNV-1a", + args: ["128"] + } + ] + }, + { + name: "FNV-1a: 256-bit", + input: "Hello, World", + expectedOutput: "f7ce17afba5b7d52d482b4fce95e52c0f3400d0d29ad18dce0651d6d1cd936d", + recipeConfig: [ + { + op: "FNV-1a", + args: ["256"] + } + ] + } +]);