Initial Commit
This commit is contained in:
240
packages/security-stack/security/vault/tests/property.test.ts
Normal file
240
packages/security-stack/security/vault/tests/property.test.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import * as fc from "fast-check";
|
||||
import {
|
||||
createEmptyContainer,
|
||||
decryptContainer,
|
||||
encryptContainer,
|
||||
} from "../src/container/container.js";
|
||||
import {
|
||||
decryptField,
|
||||
encryptField,
|
||||
} from "../src/encryption/field-encryption.js";
|
||||
import { VAULT_CODES } from "../src/errors/codes.js";
|
||||
import { VaultError } from "../src/errors/vault-error.js";
|
||||
import {
|
||||
createManifest,
|
||||
packManifest,
|
||||
unpackManifest,
|
||||
} from "../src/manifest/manifest.js";
|
||||
import type {
|
||||
DriveLike,
|
||||
DriveSharingLike,
|
||||
SyncDelegationResult,
|
||||
SyncSnapshot,
|
||||
VaultSyncLike,
|
||||
} from "../src/types/adapters.js";
|
||||
import type { VaultConfig } from "../src/types/types.js";
|
||||
import { Vault } from "../src/vault/vault.js";
|
||||
|
||||
function vaultKey(): Uint8Array {
|
||||
const key = new Uint8Array(32);
|
||||
crypto.getRandomValues(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
function memoryDrive(): DriveLike & { blobs: Map<string, Uint8Array> } {
|
||||
const blobs = new Map<string, Uint8Array>();
|
||||
return {
|
||||
blobs,
|
||||
async read(path) {
|
||||
const v = blobs.get(path);
|
||||
if (v === undefined) throw new Error(`missing ${path}`);
|
||||
return v;
|
||||
},
|
||||
async write(path, data) {
|
||||
blobs.set(path, new Uint8Array(data));
|
||||
},
|
||||
async delete(path) {
|
||||
return blobs.delete(path);
|
||||
},
|
||||
async exists(path) {
|
||||
return blobs.has(path);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function noopSharing(): DriveSharingLike {
|
||||
return {
|
||||
async createLink() {
|
||||
return { id: "x", url: "https://x.example/x" };
|
||||
},
|
||||
async revokeLink() {},
|
||||
async isActive() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function noopSync(): VaultSyncLike {
|
||||
let last: SyncSnapshot = { items: [], snapshotAt: 0 };
|
||||
return {
|
||||
hydrate(s) {
|
||||
last = s;
|
||||
},
|
||||
async pull(): Promise<SyncDelegationResult> {
|
||||
return { merged: [], conflicts: [], deleted: [], durationMs: 0 };
|
||||
},
|
||||
async push(): Promise<SyncDelegationResult> {
|
||||
return { merged: [], conflicts: [], deleted: [], durationMs: 0 };
|
||||
},
|
||||
async sync(): Promise<SyncDelegationResult> {
|
||||
return { merged: [], conflicts: [], deleted: [], durationMs: 0 };
|
||||
},
|
||||
currentSnapshot() {
|
||||
return last;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeConfig(): VaultConfig {
|
||||
return {
|
||||
vaultId: "personal",
|
||||
email: "u@example.com",
|
||||
containerPath: "/v.bin",
|
||||
attachmentsPrefix: "/att",
|
||||
argon2id: {
|
||||
memoryKB: 65536,
|
||||
iterations: 3,
|
||||
parallelism: 1,
|
||||
outputLength: 32,
|
||||
},
|
||||
argon2idSalt: new Uint8Array(16),
|
||||
drive: memoryDrive(),
|
||||
driveSharing: noopSharing(),
|
||||
sync: noopSync(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("vault — property tests", () => {
|
||||
it("encryptField + decryptField round-trip preserves arbitrary plaintexts", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
const key = vaultKey();
|
||||
fc.assert(
|
||||
fc.property(fc.string({ maxLength: 256 }), (value) => {
|
||||
const envelope = encryptField({
|
||||
vaultKey: key,
|
||||
itemId: "i1",
|
||||
fieldName: "f",
|
||||
value,
|
||||
});
|
||||
const recovered = decryptField({
|
||||
vaultKey: key,
|
||||
itemId: "i1",
|
||||
fieldName: "f",
|
||||
envelope,
|
||||
});
|
||||
return recovered === value;
|
||||
}),
|
||||
{ numRuns: 50 }
|
||||
);
|
||||
});
|
||||
|
||||
it("manifest pack/unpack round-trips for arbitrary parameter combinations", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.integer({ min: 1, max: 0xff_ff_ff_ff }),
|
||||
fc.integer({ min: 1, max: 0xff_ff }),
|
||||
fc.integer({ min: 1, max: 0xff }),
|
||||
fc.integer({ min: 0, max: 0xff_ff }),
|
||||
(memoryKB, iterations, parallelism, schemaVersion) => {
|
||||
const m = createManifest({
|
||||
vaultId: "v",
|
||||
argon2idMemoryKB: memoryKB,
|
||||
argon2idIterations: iterations,
|
||||
argon2idParallelism: parallelism,
|
||||
schemaVersion,
|
||||
now: 1_700_000_000_000,
|
||||
});
|
||||
const packed = packManifest(m);
|
||||
const round = unpackManifest(packed);
|
||||
return (
|
||||
round.argon2idMemoryKB === memoryKB &&
|
||||
round.argon2idIterations === iterations &&
|
||||
round.argon2idParallelism === parallelism &&
|
||||
round.schemaVersion === schemaVersion
|
||||
);
|
||||
}
|
||||
),
|
||||
{ numRuns: 30 }
|
||||
);
|
||||
});
|
||||
|
||||
it("container encrypt + decrypt round-trips arbitrary item ids", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
const key = vaultKey();
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.string({ minLength: 1, maxLength: 32 }),
|
||||
fc.string({ maxLength: 64 }),
|
||||
(id, title) => {
|
||||
const { manifest, state } = createEmptyContainer({
|
||||
vaultId: "v",
|
||||
argon2idMemoryKB: 65536,
|
||||
argon2idIterations: 3,
|
||||
argon2idParallelism: 4,
|
||||
now: 1,
|
||||
});
|
||||
state.items.set(id, {
|
||||
id,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
version: 1,
|
||||
deleted: false,
|
||||
title,
|
||||
category: "x",
|
||||
fieldsJson: "{}",
|
||||
concealedFieldsJson: "{}",
|
||||
tagsJson: "[]",
|
||||
favorite: false,
|
||||
});
|
||||
const blob = encryptContainer({
|
||||
manifest,
|
||||
state,
|
||||
vaultKey: key,
|
||||
});
|
||||
const round = decryptContainer(blob, key);
|
||||
return round.state.items.get(id)?.title === title;
|
||||
}
|
||||
),
|
||||
{ numRuns: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it("lock is idempotent across repeat calls", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const vault = Vault.open(makeConfig());
|
||||
await vault.lock();
|
||||
await vault.lock();
|
||||
await vault.lock();
|
||||
expect(vault.isLocked).toBe(true);
|
||||
});
|
||||
|
||||
it("every CRUD method on a locked vault throws VAULT.LOCKED", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
const vault = Vault.open(makeConfig());
|
||||
const ops: Array<() => unknown> = [
|
||||
() => vault.items,
|
||||
() => vault.search,
|
||||
() => vault.attachments,
|
||||
() => vault.shares,
|
||||
() => vault.sync,
|
||||
() => vault.currentManifest(),
|
||||
];
|
||||
for (const fn of ops) {
|
||||
let captured: unknown = null;
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
captured = err;
|
||||
}
|
||||
expect(VaultError.is(captured)).toBe(true);
|
||||
expect((captured as VaultError).code).toBe(VAULT_CODES.LOCKED);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user