94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { utf8ToBytes } from "../../src/crypto/encoding";
|
|
import { exportEncryptedPskc, importEncryptedPskc } from "../../src/otp/pskc";
|
|
import type { OtpProfile } from "../../src/otp/profile";
|
|
|
|
const profiles: OtpProfile[] = [
|
|
{
|
|
kind: "totp",
|
|
account: "alice@example.test",
|
|
issuer: "Example & Sons",
|
|
secret: utf8ToBytes("12345678901234567890"),
|
|
algorithm: "SHA-256",
|
|
digits: 8,
|
|
period: 45,
|
|
epoch: 10,
|
|
counter: 0n,
|
|
extensions: new Map(),
|
|
},
|
|
{
|
|
kind: "hotp",
|
|
account: "hardware token",
|
|
issuer: "Example",
|
|
secret: utf8ToBytes("abcdefghijklmnopqrst"),
|
|
algorithm: "SHA-1",
|
|
digits: 6,
|
|
period: 30,
|
|
counter: 1042n,
|
|
extensions: new Map(),
|
|
},
|
|
];
|
|
|
|
describe("encrypted PSKC", () => {
|
|
it("round-trips authenticated password-encrypted collections", async () => {
|
|
const exported = await exportEncryptedPskc(
|
|
profiles,
|
|
"correct horse battery staple",
|
|
{ iterations: 100_000 },
|
|
);
|
|
expect(exported).toContain("PBKDF2-params");
|
|
expect(exported).toContain(
|
|
'Algorithm="http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#pbkdf2"',
|
|
);
|
|
expect(exported).toContain("<pskc:MACKey><xenc:EncryptionMethod");
|
|
expect(exported).toContain('<pskc:EncryptedValue Id="ED-1">');
|
|
expect(exported).not.toContain("12345678901234567890");
|
|
const result = await importEncryptedPskc(
|
|
exported,
|
|
"correct horse battery staple",
|
|
);
|
|
expect(result.profiles).toHaveLength(2);
|
|
expect(result.profiles[0]).toMatchObject({
|
|
kind: "totp",
|
|
account: "alice@example.test",
|
|
issuer: "Example & Sons",
|
|
algorithm: "SHA-256",
|
|
digits: 8,
|
|
period: 45,
|
|
epoch: 10,
|
|
});
|
|
expect(result.profiles[1]).toMatchObject({
|
|
kind: "hotp",
|
|
counter: 1042n,
|
|
});
|
|
expect([...result.profiles[0]!.secret]).toEqual([...profiles[0]!.secret]);
|
|
});
|
|
|
|
it("rejects wrong passwords and unauthenticated changes", async () => {
|
|
const exported = await exportEncryptedPskc(
|
|
profiles.slice(0, 1),
|
|
"correct horse battery staple",
|
|
{ iterations: 100_000 },
|
|
);
|
|
await expect(
|
|
importEncryptedPskc(exported, "incorrect password"),
|
|
).rejects.toThrow(/incorrect|damaged/u);
|
|
const tampered = exported.replace(
|
|
/<pskc:ValueMAC>([^<])/u,
|
|
(_match, first: string) => `<pskc:ValueMAC>${first === "A" ? "B" : "A"}`,
|
|
);
|
|
await expect(
|
|
importEncryptedPskc(tampered, "correct horse battery staple"),
|
|
).rejects.toThrow(/ValueMAC/u);
|
|
});
|
|
|
|
it("rejects XML entity declarations", async () => {
|
|
await expect(
|
|
importEncryptedPskc(
|
|
'<!DOCTYPE x [<!ENTITY e SYSTEM "file:///etc/passwd">]><x/>',
|
|
"password",
|
|
),
|
|
).rejects.toThrow(/entity/u);
|
|
});
|
|
});
|