146 lines
5.0 KiB
TypeScript
146 lines
5.0 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { VAULT_CODES } from "../errors/codes.js";
|
|
import { VaultError } from "../errors/vault-error.js";
|
|
import {
|
|
createManifest,
|
|
hashVaultId,
|
|
MANIFEST_AEAD_XCHACHA20_POLY1305,
|
|
MANIFEST_BYTE_SIZE,
|
|
MANIFEST_FORMAT_VERSION,
|
|
MANIFEST_KDF_ARGON2ID_2SKD,
|
|
packManifest,
|
|
touchManifest,
|
|
unpackManifest,
|
|
} from "./manifest.js";
|
|
|
|
const baseInput = {
|
|
vaultId: "personal",
|
|
argon2idMemoryKB: 65536,
|
|
argon2idIterations: 3,
|
|
argon2idParallelism: 4,
|
|
schemaVersion: 1,
|
|
now: 1_700_000_000_000,
|
|
};
|
|
|
|
describe("vault manifest", () => {
|
|
it("createManifest populates current versions and timestamps", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const m = createManifest(baseInput);
|
|
expect(m.formatVersion).toBe(MANIFEST_FORMAT_VERSION);
|
|
expect(m.kdfId).toBe(MANIFEST_KDF_ARGON2ID_2SKD);
|
|
expect(m.aeadAlgorithm).toBe(MANIFEST_AEAD_XCHACHA20_POLY1305);
|
|
expect(m.argon2idMemoryKB).toBe(65536);
|
|
expect(m.argon2idIterations).toBe(3);
|
|
expect(m.argon2idParallelism).toBe(4);
|
|
expect(m.createdAt).toBe(baseInput.now);
|
|
expect(m.lastModifiedAt).toBe(baseInput.now);
|
|
expect(m.schemaVersion).toBe(1);
|
|
expect(m.vaultIdHash.length).toBe(32);
|
|
});
|
|
|
|
it("hashVaultId is deterministic and 32 bytes", { timeout: 5000 }, () => {
|
|
const a = hashVaultId("personal");
|
|
const b = hashVaultId("personal");
|
|
expect(a).toEqual(b);
|
|
expect(a.length).toBe(32);
|
|
const c = hashVaultId("work");
|
|
expect(c).not.toEqual(a);
|
|
});
|
|
|
|
it("packManifest + unpackManifest round-trip preserves all fields", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const m = createManifest(baseInput);
|
|
const packed = packManifest(m);
|
|
expect(packed.length).toBe(MANIFEST_BYTE_SIZE);
|
|
const round = unpackManifest(packed);
|
|
expect(round).toEqual(m);
|
|
});
|
|
|
|
it("unpackManifest ignores trailing bytes", { timeout: 5000 }, () => {
|
|
const m = createManifest(baseInput);
|
|
const packed = packManifest(m);
|
|
const padded = new Uint8Array(packed.length + 100);
|
|
padded.set(packed, 0);
|
|
const round = unpackManifest(padded);
|
|
expect(round).toEqual(m);
|
|
});
|
|
|
|
it("unpackManifest rejects too-short buffers with MANIFEST_INVALID", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
try {
|
|
unpackManifest(new Uint8Array(MANIFEST_BYTE_SIZE - 1));
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(VAULT_CODES.MANIFEST_INVALID);
|
|
}
|
|
});
|
|
|
|
it("packManifest rejects non-conforming manifests", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const bad = createManifest(baseInput);
|
|
try {
|
|
packManifest({
|
|
...bad,
|
|
vaultIdHash: new Uint8Array(31),
|
|
});
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(VAULT_CODES.MANIFEST_INVALID);
|
|
}
|
|
});
|
|
|
|
it("unpackManifest rejects unsupported format versions", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const m = createManifest(baseInput);
|
|
const packed = packManifest(m);
|
|
// Mutate the formatVersion field (first 2 bytes — big-endian).
|
|
const tampered = new Uint8Array(packed);
|
|
tampered[0] = 0xff;
|
|
tampered[1] = 0xff;
|
|
try {
|
|
unpackManifest(tampered);
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(
|
|
VAULT_CODES.MANIFEST_VERSION_UNSUPPORTED
|
|
);
|
|
}
|
|
});
|
|
|
|
it("touchManifest updates only lastModifiedAt", { timeout: 5000 }, () => {
|
|
const m = createManifest(baseInput);
|
|
const next = touchManifest(m, baseInput.now + 1000);
|
|
expect(next.lastModifiedAt).toBe(baseInput.now + 1000);
|
|
expect(next.createdAt).toBe(m.createdAt);
|
|
expect(next.formatVersion).toBe(m.formatVersion);
|
|
expect(next.vaultIdHash).toEqual(m.vaultIdHash);
|
|
});
|
|
|
|
it("unpackManifest rejects manifests with corrupt internal numerics", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const m = createManifest(baseInput);
|
|
const packed = packManifest(m);
|
|
const tampered = new Uint8Array(packed);
|
|
// Zero out argon2idMemoryKB which violates min(1) on the schema.
|
|
// formatVersion u16 (2) + kdfId u8 (1) + aeadAlgorithm u8 (1) = 4
|
|
// argon2idMemoryKB is at offset 4..8.
|
|
for (let i = 4; i < 8; i++) tampered[i] = 0;
|
|
try {
|
|
unpackManifest(tampered);
|
|
throw new Error("should have thrown");
|
|
} catch (err) {
|
|
expect(VaultError.is(err)).toBe(true);
|
|
expect((err as VaultError).code).toBe(VAULT_CODES.MANIFEST_INVALID);
|
|
}
|
|
});
|
|
});
|