Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,252 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { VAULT_CODES } from "../src/errors/codes.js";
import { VaultError } from "../src/errors/vault-error.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 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;
},
};
}
class CountingSync implements VaultSyncLike {
public lastSnapshot: SyncSnapshot = { items: [], snapshotAt: 0 };
public hydrateCount = 0;
public hydrate(s: SyncSnapshot): void {
this.hydrateCount += 1;
this.lastSnapshot = s;
}
public async pull(): Promise<SyncDelegationResult> {
return {
merged: this.lastSnapshot.items.map((i) => i.id),
conflicts: [],
deleted: [],
durationMs: 0,
};
}
public async push(): Promise<SyncDelegationResult> {
return {
merged: this.lastSnapshot.items.map((i) => i.id),
conflicts: [],
deleted: [],
durationMs: 0,
};
}
public async sync(): Promise<SyncDelegationResult> {
return {
merged: this.lastSnapshot.items.map((i) => i.id),
conflicts: [],
deleted: [],
durationMs: 0,
};
}
public currentSnapshot(): SyncSnapshot {
return this.lastSnapshot;
}
}
function makeConfig(
drive = memoryDrive(),
sync: VaultSyncLike = new CountingSync()
): VaultConfig {
return {
vaultId: "personal",
email: "user@example.com",
containerPath: "/personal.vault",
attachmentsPrefix: "/personal/att",
argon2id: {
memoryKB: 65536,
iterations: 3,
parallelism: 1,
outputLength: 32,
},
argon2idSalt: new Uint8Array(16),
drive,
driveSharing: noopSharing(),
sync,
};
}
describe("vault — integration", () => {
it("end-to-end: unlock, create item with concealed field, lock, re-unlock, get item", {
timeout: 30000,
}, async () => {
const drive = memoryDrive();
const config = makeConfig(drive);
const vault = Vault.open(config);
await vault.unlock("pass", "secret");
const created = vault.items.create({
title: "GitHub",
category: "login",
fields: { username: "alice", url: "https://github.com" },
concealedFields: { password: "p@ssw0rd" },
tags: ["work"],
favorite: true,
});
expect(created.concealedFields.password).toBe("p@ssw0rd");
await vault.flush();
await vault.lock();
expect(vault.isLocked).toBe(true);
const v2 = Vault.open(config);
await v2.unlock("pass", "secret");
const fetched = v2.items.get(created.id);
expect(fetched).not.toBeNull();
expect(fetched?.title).toBe("GitHub");
expect(fetched?.concealedFields.password).toBe("p@ssw0rd");
await v2.lock();
});
it("wrong password rejects with DECRYPT_FAILED, vault stays locked", {
timeout: 30000,
}, async () => {
const drive = memoryDrive();
const config = makeConfig(drive);
const v1 = Vault.open(config);
await v1.unlock("pass", "secret");
await v1.flush();
await v1.lock();
const v2 = Vault.open(config);
try {
await v2.unlock("wrong", "secret");
throw new Error("should have thrown");
} catch (err) {
expect(VaultError.is(err)).toBe(true);
expect((err as VaultError).code).toBe(VAULT_CODES.DECRYPT_FAILED);
}
expect(v2.isLocked).toBe(true);
});
it("tampered drive blob rejects with DECRYPT_FAILED", {
timeout: 30000,
}, async () => {
const drive = memoryDrive();
const config = makeConfig(drive);
const v1 = Vault.open(config);
await v1.unlock("pass", "secret");
await v1.flush();
await v1.lock();
const blob = drive.blobs.get(config.containerPath) as Uint8Array;
const tampered = new Uint8Array(blob);
tampered[tampered.length - 5] ^= 0x42;
drive.blobs.set(config.containerPath, tampered);
const v2 = Vault.open(config);
try {
await v2.unlock("pass", "secret");
throw new Error("should have thrown");
} catch (err) {
expect(VaultError.is(err)).toBe(true);
expect((err as VaultError).code).toBe(VAULT_CODES.DECRYPT_FAILED);
}
});
it("attachments survive lock + re-unlock through drive", {
timeout: 30000,
}, async () => {
const drive = memoryDrive();
const config = makeConfig(drive);
const vault = Vault.open(config);
await vault.unlock("pass", "secret");
const data = new TextEncoder().encode("hello attachment");
const meta = await vault.attachments.upload({
itemId: "i",
filename: "f.txt",
mimeType: "text/plain",
data,
});
await vault.flush();
await vault.lock();
const v2 = Vault.open(config);
await v2.unlock("pass", "secret");
// Attachment metadata is per-runtime; re-injecting is the
// caller's job. We still verify the drive blob is decrypted
// correctly when metadata is known.
(
v2.attachments as unknown as {
metadata: Map<string, typeof meta>;
}
).metadata.set(meta.id, meta);
const recovered = await v2.attachments.download(meta.id);
expect(new TextDecoder().decode(recovered)).toBe("hello attachment");
await v2.lock();
});
it("sync delegation works end-to-end through the orchestrator", {
timeout: 30000,
}, async () => {
const sync = new CountingSync();
const drive = memoryDrive();
const vault = Vault.open(makeConfig(drive, sync));
await vault.unlock("pass", "secret");
vault.items.create({
title: "x",
category: "login",
fields: {},
concealedFields: {},
tags: [],
favorite: false,
});
const result = await vault.sync.sync();
expect(result.merged.length).toBe(1);
expect(sync.hydrateCount).toBeGreaterThan(0);
await vault.lock();
});
it("shares delegation works end-to-end", { timeout: 30000 }, async () => {
const drive = memoryDrive();
const vault = Vault.open(makeConfig(drive));
await vault.unlock("pass", "secret");
const link = await vault.shares.createLink({
itemId: "i",
itemPlaintext: new TextEncoder().encode("payload"),
expiresAt: null,
maxViews: null,
});
expect(link.url).toContain("https://");
expect(await vault.shares.isActive(link.id)).toBe(true);
await vault.shares.revokeLink(link.id);
expect(await vault.shares.isActive(link.id)).toBe(false);
await vault.lock();
});
});

View 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);
}
});
});