feat(device-core): add restricted identity references
This commit is contained in:
@@ -25,6 +25,7 @@ export const DEVICE_BINDING_CAPABILITIES = Object.freeze([
|
||||
const OPAQUE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const IMEI_RE = /^\d{15}$/;
|
||||
const DIGEST_RE = /^hmac-sha256:[a-f0-9]{64}$/;
|
||||
const IDENTIFIER_KIND_RE = /^[a-z][a-z0-9._:-]{1,63}$/;
|
||||
const forbiddenKeyFragments = Object.freeze([
|
||||
"password",
|
||||
"secret",
|
||||
@@ -88,7 +89,7 @@ export function toSafeDiscoveryView(signal, options = {}) {
|
||||
protocol: normalized.protocol,
|
||||
observedAt: normalized.observedAt,
|
||||
lifecycleState: normalized.lifecycleState,
|
||||
identifier: Object.freeze({
|
||||
identifier: normalizeRestrictedIdentifierProjection({
|
||||
kind: normalized.identifier.kind,
|
||||
masked: maskRestrictedIdentifier(normalized.identifier),
|
||||
}),
|
||||
@@ -116,6 +117,55 @@ export function assertIdentifierDigest(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function normalizeRestrictedIdentifierProjection(input) {
|
||||
assertPlainObject(input, "restricted_identifier_projection");
|
||||
const allowedKeys = new Set(["kind", "masked"]);
|
||||
for (const key of Object.keys(input)) {
|
||||
if (!allowedKeys.has(key)) {
|
||||
throw new TypeError(
|
||||
`restricted_identifier_projection_field_unexpected:${key}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (typeof input.kind !== "string" || !IDENTIFIER_KIND_RE.test(input.kind)) {
|
||||
throw new TypeError("restricted_identifier_projection_kind_invalid");
|
||||
}
|
||||
if (
|
||||
typeof input.masked !== "string"
|
||||
|| input.masked.length < 5
|
||||
|| input.masked.length > 128
|
||||
|| !input.masked.includes("*")
|
||||
|| /\u0000|[\u0001-\u001f\u007f]/.test(input.masked)
|
||||
|| /\b\d{15}\b/.test(input.masked)
|
||||
) {
|
||||
throw new TypeError("restricted_identifier_projection_mask_invalid");
|
||||
}
|
||||
return Object.freeze({
|
||||
kind: input.kind,
|
||||
masked: input.masked,
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeRestrictedIdentifierRecord(input) {
|
||||
assertPlainObject(input, "restricted_identifier_record");
|
||||
const allowedKeys = new Set(["kind", "digest", "masked"]);
|
||||
for (const key of Object.keys(input)) {
|
||||
if (!allowedKeys.has(key)) {
|
||||
throw new TypeError(
|
||||
`restricted_identifier_record_field_unexpected:${key}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const projection = normalizeRestrictedIdentifierProjection({
|
||||
kind: input.kind,
|
||||
masked: input.masked,
|
||||
});
|
||||
return Object.freeze({
|
||||
...projection,
|
||||
digest: assertIdentifierDigest(input.digest),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeDevicePlaneBinding(input) {
|
||||
assertPlainObject(input, "device_plane_binding");
|
||||
rejectForbiddenKeys(input);
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
hashRestrictedIdentifier,
|
||||
normalizeDevicePlaneBinding,
|
||||
normalizeDiscoverySignal,
|
||||
normalizeRestrictedIdentifierProjection,
|
||||
normalizeRestrictedIdentifierRecord,
|
||||
toSafeDiscoveryView,
|
||||
} from "../src/index.mjs";
|
||||
|
||||
@@ -82,6 +84,32 @@ test("identifier hashing requires a strong process-only pepper", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("restricted identifier records keep digest internal and expose only a mask", () => {
|
||||
const record = normalizeRestrictedIdentifierRecord({
|
||||
kind: "vendor.serial",
|
||||
digest: `hmac-sha256:${"a".repeat(64)}`,
|
||||
masked: "********ABCD",
|
||||
});
|
||||
const projection = normalizeRestrictedIdentifierProjection({
|
||||
kind: record.kind,
|
||||
masked: record.masked,
|
||||
});
|
||||
|
||||
assert.deepEqual(projection, {
|
||||
kind: "vendor.serial",
|
||||
masked: "********ABCD",
|
||||
});
|
||||
assert.equal("digest" in projection, false);
|
||||
assertSafeProjection({ identifier: projection });
|
||||
assert.throws(
|
||||
() => normalizeRestrictedIdentifierProjection({
|
||||
kind: "vendor.serial",
|
||||
masked: "SERIAL-PLAINTEXT",
|
||||
}),
|
||||
/restricted_identifier_projection_mask_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects unverified framing and command-shaped discovery input", () => {
|
||||
assert.throws(
|
||||
() => normalizeDiscoverySignal({
|
||||
|
||||
Reference in New Issue
Block a user