Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 109 additions & 7 deletions src/core/operations/AnalyseUUID.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @author ko80240 [csk.dev@proton.me]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
Expand All @@ -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
Expand All @@ -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");
}
43 changes: 43 additions & 0 deletions tatus
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this file was added by mistake?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File has been removed in latest commit

Original file line number Diff line number Diff line change
@@ -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)
5 changes: 2 additions & 3 deletions tests/node/tests/operations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -589,16 +589,15 @@ 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", () => {
const uuid = chef.generateUUID();
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", () => {
Expand Down
1 change: 1 addition & 0 deletions tests/operations/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
66 changes: 66 additions & 0 deletions tests/operations/tests/AnalyseUUID.mjs
Original file line number Diff line number Diff line change
@@ -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]
}
]
}
]);
Loading