Initial Commit
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user