Added metadata extraction for UUID strings. (#2322)

This commit is contained in:
ko80240 2026-04-24 15:14:35 +01:00 committed by GitHub
parent e0774ed940
commit 4080df0bbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 178 additions and 9 deletions

View File

@ -1,5 +1,6 @@
/** /**
* @author n1474335 [n1474335@gmail.com] * @author n1474335 [n1474335@gmail.com]
* @author ko80240 [csk.dev@proton.me]
* @copyright Crown Copyright 2023 * @copyright Crown Copyright 2023
* @license Apache-2.0 * @license Apache-2.0
*/ */
@ -8,6 +9,7 @@ import * as uuid from "uuid";
import Operation from "../Operation.mjs"; import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs"; import OperationError from "../errors/OperationError.mjs";
import { toHex } from "../lib/Hex.mjs";
/** /**
* Analyse UUID operation * Analyse UUID operation
@ -22,27 +24,128 @@ class AnalyseUUID extends Operation {
this.name = "Analyse UUID"; this.name = "Analyse UUID";
this.module = "Crypto"; 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.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier";
this.inputType = "string"; this.inputType = "string";
this.outputType = "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 * @param {Object[]} args
* @returns {string} * @returns {string}
*/ */
run(input, args) { run(input, args) {
input = input.trim();
let uuidVersion, uuidBytes;
try { try {
const uuidVersion = uuid.version(input); uuidVersion = uuid.version(input); // Re-using the uuid library to extract version
return "UUID version: " + uuidVersion; uuidBytes = uuid.parse(input); // Re-using the uuid library to parse bytes
} catch (error) { } catch (error) {
throw new OperationError("Invalid UUID"); 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; 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");
}

View File

@ -589,8 +589,7 @@ Password: 282760`;
...[1, 3, 4, 5, 6, 7].map(version => it(`Analyze UUID v${version}`, () => { ...[1, 3, 4, 5, 6, 7].map(version => it(`Analyze UUID v${version}`, () => {
const uuid = chef.generateUUID("", { "version": `v${version}` }).toString(); const uuid = chef.generateUUID("", { "version": `v${version}` }).toString();
const result = chef.analyseUUID(uuid).toString(); const result = chef.analyseUUID(uuid).toString();
const expected = `UUID version: ${version}`; assert.ok(result.startsWith(`Version:\n${version}\n`), `Expected output to start with "Version:\\n${version}\\n", got: ${result}`);
assert.strictEqual(result, expected);
})), })),
it("Generate UUID using defaults", () => { it("Generate UUID using defaults", () => {
@ -598,7 +597,7 @@ Password: 282760`;
assert.ok(uuid); assert.ok(uuid);
const analysis = chef.analyseUUID(uuid).toString(); 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", () => { it("Gzip, Gunzip", () => {

View File

@ -16,6 +16,7 @@ import { setLongTestFailure, logTestReport } from "../lib/utils.mjs";
import TestRegister from "../lib/TestRegister.mjs"; import TestRegister from "../lib/TestRegister.mjs";
import "./tests/A1Z26CipherDecode.mjs"; import "./tests/A1Z26CipherDecode.mjs";
import "./tests/AESKeyWrap.mjs"; import "./tests/AESKeyWrap.mjs";
import "./tests/AnalyseUUID.mjs";
import "./tests/AlternatingCaps.mjs"; import "./tests/AlternatingCaps.mjs";
import "./tests/AvroToJSON.mjs"; import "./tests/AvroToJSON.mjs";
import "./tests/BaconCipher.mjs"; import "./tests/BaconCipher.mjs";

View File

@ -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]
}
]
}
]);