Added metadata extraction for UUID strings. (#2322)
This commit is contained in:
parent
e0774ed940
commit
4080df0bbd
@ -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");
|
||||||
|
}
|
||||||
|
|||||||
@ -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", () => {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
66
tests/operations/tests/AnalyseUUID.mjs
Normal file
66
tests/operations/tests/AnalyseUUID.mjs
Normal 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]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
Loading…
x
Reference in New Issue
Block a user