Initial Commit
This commit is contained in:
252
packages/security-stack/security/vault/tests/integration.test.ts
Normal file
252
packages/security-stack/security/vault/tests/integration.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user