diff --git a/src/core/operations/AnalyseUUID.mjs b/src/core/operations/AnalyseUUID.mjs index b3506017..05eb5746 100644 --- a/src/core/operations/AnalyseUUID.mjs +++ b/src/core/operations/AnalyseUUID.mjs @@ -1,5 +1,5 @@ /** - * @author n1474335 [n1474335@gmail.com] + * @author ko80240 [csk.dev@proton.me] * @copyright Crown Copyright 2023 * @license Apache-2.0 */ @@ -8,6 +8,7 @@ import * as uuid from "uuid"; import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; +import { toHex } from "../lib/Hex.mjs"; /** * Analyse UUID operation @@ -22,27 +23,128 @@ class AnalyseUUID extends Operation { this.name = "Analyse UUID"; this.module = "Crypto"; - this.description = "Tries to determine information about a given UUID and suggests which version may have been used to generate it"; + this.description = "Operation for extracting metadata and detecting the version of a given UUID."; this.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier"; this.inputType = "string"; this.outputType = "string"; - this.args = []; + this.args = [ + { + name: "Include Metadata", + type: "boolean", + value: true + } + ]; } /** - * @param {string} input + * @param {string} input - Expects a valid UUID string * @param {Object[]} args * @returns {string} */ run(input, args) { + input = input.trim(); + + let uuidVersion, uuidBytes; try { - const uuidVersion = uuid.version(input); - return "UUID version: " + uuidVersion; + uuidVersion = uuid.version(input); // Re-using the uuid library to extract version + uuidBytes = uuid.parse(input); // Re-using the uuid library to parse bytes } catch (error) { throw new OperationError("Invalid UUID"); } - } + const [includeMetadata] = args; + const dv = new DataView(uuidBytes.buffer, uuidBytes.byteOffset, uuidBytes.byteLength); // Dataview helps handle the multi-byte ints + const uuidInteger = (dv.getBigUint64(0) << 64n) | dv.getBigUint64(8); + + const sections = [`Version:\n${uuidVersion}`]; + + if (includeMetadata) { + const parser = UUID_PARSERS[uuidVersion]; + const decoded = parser?.(uuidBytes, dv); + sections.push(formatDecoded(decoded)); + } + + sections.push(`UUID Integer:\n${uuidInteger}`); + + return sections.filter(Boolean).join("\n\n"); + } } export default AnalyseUUID; + +/** + * Metadata can be extracted for versions 1, 6, and 7. + * Enum-like frozen mapping of UUID version to parser function. + */ +const UUID_PARSERS = Object.freeze({ + 1: parsev1v6, + 6: parsev1v6, + 7: parsev7, +}); + +/** + * Versions 1 and 6. Note 6 is a re-order of 1. + * Version 1 == layout: timeLow(32) | timeMid(16) | timeHi(12) + * Version 6 == layout: timeHi(32) | timeMid(16) | timeLow(12) + */ +function parsev1v6(uuidBytes, dv) { + const isV1 = (uuidBytes[6] >> 4) === 1; + + const timeStamp = + isV1 ? ( + (BigInt(dv.getUint16(6) & 0x0fff) << 48n) | // mask off version bits + (BigInt(dv.getUint16(4)) << 32n) | + BigInt(dv.getUint32(0)) + ) : ( + (BigInt(dv.getUint32(0)) << 28n) | + (BigInt(dv.getUint16(4)) << 12n) | + (BigInt(dv.getUint16(6) & 0x0fff)) + ); + + // Convert to Unix time + const milliseconds = + Number( + (timeStamp - 122192928000000000n) / 10000n + ); + + return { + timestamp: milliseconds, + isoTimestamp: new Date(milliseconds).toISOString(), + clock: ((uuidBytes[8] & 0x3f) << 8) | uuidBytes[9], + node: toHex(uuidBytes.slice(10), ":").toUpperCase() + }; +} + +/** Version 7 */ +function parsev7(uuidBytes, dv) { + const milliseconds = Number((BigInt(dv.getUint32(0)) << 16n) | BigInt(dv.getUint16(4))); + + return { + timestamp: milliseconds, + isoTimestamp: new Date(milliseconds).toISOString(), + randA: ((uuidBytes[6] & 0x0f) << 8) | uuidBytes[7], + randB: toHex(uuidBytes.slice(8), "").toUpperCase() + }; +} + +/** + * Formats metadata + * + * @param {Object|undefined} decoded + * @returns {string} + */ +function formatDecoded(decoded) { + if (!decoded) return "No metadata available. Only versions 1, 6, 7 are supported."; + + return Object.entries({ + "Timestamp": decoded.timestamp, + "Timestamp (ISO)": decoded.isoTimestamp, + "Node": decoded.node, + "Clock": decoded.clock, + "Rand A": decoded.randA, + "Rand B": decoded.randB + }) + .filter(([, value]) => value !== undefined) + .map(([label, value]) => `${label}:\n${value}`) + .join("\n\n"); +} diff --git a/tatus b/tatus new file mode 100644 index 00000000..a4fb1c83 --- /dev/null +++ b/tatus @@ -0,0 +1,43 @@ +e5305a4f (HEAD -> feature/analyseUUID) Feature: Added metadata extraction for AnalyseUUID operation with tests and test index. +53583814 (origin/master, origin/HEAD, master) Regular Expression operation email address regex: Support IPv4 domains (#2167) +f646065c Rewriting fixCryptoApiImports and fixSnackbarMarkup to js to make it OS agnostic (#2298) +2eb8810b chore (deps): bump basic-ftp from 5.2.1 to 5.2.2 (#2317) +d8830e2b chore (deps): bump axios from 1.13.6 to 1.15.0 (#2316) +7b9ff751 chore (deps): bump webpack from 5.105.4 to 5.106.0 (#2315) +c68cc5a0 chore (deps): bump basic-ftp from 5.2.0 to 5.2.1 (#2313) +39f97a2a Update vulnerable dependencies (#2311) +42adf5b1 (tag: v10.23.0) Bump v10.23.0 (#2310) +167cc398 Properly escape HTML entities in sampleDelim to avoid XSS issue (#2307) +33314ac6 chore (deps): bump lodash from 4.17.23 to 4.18.1 (#2304) +e1352065 chore (deps): bump @codemirror/view from 6.40.0 to 6.41.0 (#2305) +eea8bcd2 chore (deps): bump the patch-updates group with 2 updates (#2303) +7d9fd4b2 chore (deps): bump @xmldom/xmldom from 0.8.11 to 0.8.12 (#2302) +80286f1e chore (deps): bump picomatch (#2299) +c00824a8 chore (deps): bump node-forge from 1.3.3 to 1.4.0 (#2297) +f9184d39 chore (deps): bump the patch-updates group with 3 updates (#2296) +088da9de chore (deps) bump chromedriver from 130.0.4 to 146.0.6 (#2292) +6aa98b4a ParseEthernetFrame - Fix vlan calculation (#2295) +b0fa1f8d Add pull request template with AI usage disclosure (#2279) +ff63ec97 fix: return empty output for zero-length To Modhex input (#2249) +9cf82cc1 Added tab focus to top banner and navigation to About/Support Modal (#1733) +78d40eab Add Parse Ethernet frame Operation, allow Parse IPv4 Header to cascade (#1722) +2b370b96 Selection and Deselection of autobake checkbox using keyboard (#1727) +38a0adaf chore (deps): bump @babel/runtime from 7.28.6 to 7.29.2 (#2263) +8c4a55b2 Add more helpful error for when numerical ingredient is left empty (#1540) +a1f92082 chore (deps): bump @codemirror/view from 6.39.17 to 6.40.0 (#2262) +32ff3cd5 Bump flatted from 3.3.2 to 3.4.2 (#2266) +290f824e feat: add Raw option for Jq operation (#2237) +7f4f90e4 chore (deps): bump core-js from 3.48.0 to 3.49.0 (#2261) +607acbd2 chore (deps): bump the patch-updates group with 6 updates (#2260) +f7bbb330 Add Extract Audio Metadata operation (#2170) +d9dacf6b Fix Jq issue (#2210) +19bc8169 Configure dependabot updates (#2259) +eef8d55d fix(A1Z26): return empty string instead of empty array for empty input (#2257) +4deaa0de Fix broken Docker link in README (#2250) +d195a51e Update some dependencies, including a number causing npm audit warnings (#2236) +47ba0585 Bump axios from 1.7.9 to 1.13.6 (#2234) +64224dac Bump jws from 3.2.2 to 3.2.3 (#2235) +76bf4f44 Bump pbkdf2 from 3.1.2 to 3.1.5 (#2229) +c39ae52f Bump form-data from 4.0.1 to 4.0.5 (#2228) +3f91df23 Bump basic-ftp from 5.0.5 to 5.2.0 (#2231) +a19261d5 feat: add ARM disassembler operation (#2156) diff --git a/tests/node/tests/operations.mjs b/tests/node/tests/operations.mjs index 41eddd82..a97f5ccb 100644 --- a/tests/node/tests/operations.mjs +++ b/tests/node/tests/operations.mjs @@ -589,8 +589,7 @@ Password: 282760`; ...[1, 3, 4, 5, 6, 7].map(version => it(`Analyze UUID v${version}`, () => { const uuid = chef.generateUUID("", { "version": `v${version}` }).toString(); const result = chef.analyseUUID(uuid).toString(); - const expected = `UUID version: ${version}`; - assert.strictEqual(result, expected); + assert.ok(result.startsWith(`Version:\n${version}\n`), `Expected output to start with "Version:\\n${version}\\n", got: ${result}`); })), it("Generate UUID using defaults", () => { @@ -598,7 +597,7 @@ Password: 282760`; assert.ok(uuid); const analysis = chef.analyseUUID(uuid).toString(); - assert.strictEqual(analysis, "UUID version: 4"); + assert.ok(analysis.startsWith("Version:\n4\n"), `Expected output to start with "Version:\\n4\\n", got: ${analysis}`); }), it("Gzip, Gunzip", () => { diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index d18fbffe..420d0129 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -16,6 +16,7 @@ import { setLongTestFailure, logTestReport } from "../lib/utils.mjs"; import TestRegister from "../lib/TestRegister.mjs"; import "./tests/A1Z26CipherDecode.mjs"; import "./tests/AESKeyWrap.mjs"; +import "./tests/AnalyseUUID.mjs"; import "./tests/AlternatingCaps.mjs"; import "./tests/AvroToJSON.mjs"; import "./tests/BaconCipher.mjs"; diff --git a/tests/operations/tests/AnalyseUUID.mjs b/tests/operations/tests/AnalyseUUID.mjs new file mode 100644 index 00000000..89118421 --- /dev/null +++ b/tests/operations/tests/AnalyseUUID.mjs @@ -0,0 +1,66 @@ +/** + * Analyse UUID tests + * + * @author ko80240 [csk.dev@proton.me] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + "name": "Analyse UUID: v1 UUID extracts timestamp, clock, and node", + "input": "cefa1760-28ee-11f1-9f95-1fb76af3e239", + "expectedOutput": "Version:\n1\n\nTimestamp:\n1774514156502\n\nTimestamp (ISO):\n2026-03-26T08:35:56.502Z\n\nNode:\n1F:B7:6A:F3:E2:39\n\nClock:\n8085\n\nUUID Integer:\n275119515460318071558429785403790975545", + "recipeConfig": [ + { + "op": "Analyse UUID", + "args": [true] + } + ] + }, + { + "name": "Analyse UUID: v7 UUID extracts timestamp, randA, and randB", + "input": "019d294a-af64-7728-9524-26da08f50708", + "expectedOutput": "Version:\n7\n\nTimestamp:\n1774514253668\n\nTimestamp (ISO):\n2026-03-26T08:37:33.668Z\n\nRand A:\n1832\n\nRand B:\n952426DA08F50708\n\nUUID Integer:\n2145256098533991595556290452700595976", + "recipeConfig": [ + { + "op": "Analyse UUID", + "args": [true] + } + ] + }, + { + "name": "Analyse UUID: v4 UUID should show no metadata - not possible", + "input": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "expectedOutput": "Version:\n4\n\nNo metadata available. Only versions 1, 6, 7 are supported.\n\nUUID Integer:\n324969006592305634633390616021200786553", + "recipeConfig": [ + { + "op": "Analyse UUID", + "args": [true] + } + ] + }, + { + "name": "Analyse UUID: if the 'Include Metadata' option is false it should return not metadata", + "input": "cefa1760-28ee-11f1-9f95-1fb76af3e239", + "expectedOutput": "Version:\n1\n\nUUID Integer:\n275119515460318071558429785403790975545", + "recipeConfig": [ + { + "op": "Analyse UUID", + "args": [false] + } + ] + }, + { + "name": "Analyse UUID: invalid UUID should return error message", + "input": "not-a-uuid", + "expectedOutput": "Invalid UUID", + "recipeConfig": [ + { + "op": "Analyse UUID", + "args": [true] + } + ] + } +]);