feat: release authentication laboratories 0.3.0
This commit is contained in:
@@ -1,13 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
importAegisBackup,
|
||||
importEncryptedAegisBackup,
|
||||
importAndOtpBackup,
|
||||
importFreeOtpBackup,
|
||||
importTwoFasBackup,
|
||||
} from "../../src/otp/vendor-backups";
|
||||
import { scrypt } from "@noble/hashes/scrypt.js";
|
||||
import {
|
||||
bytesToArrayBuffer,
|
||||
bytesToHex,
|
||||
utf8ToBytes,
|
||||
} from "../../src/crypto/encoding";
|
||||
|
||||
const secret = "JBSWY3DPEHPK3PXP";
|
||||
|
||||
async function encryptGcm(
|
||||
keyBytes: Uint8Array,
|
||||
plaintext: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
): Promise<{ ciphertext: Uint8Array; tag: Uint8Array }> {
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
bytesToArrayBuffer(keyBytes),
|
||||
"AES-GCM",
|
||||
false,
|
||||
["encrypt"],
|
||||
);
|
||||
const output = new Uint8Array(
|
||||
await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv: bytesToArrayBuffer(nonce) },
|
||||
key,
|
||||
bytesToArrayBuffer(plaintext),
|
||||
),
|
||||
);
|
||||
return { ciphertext: output.slice(0, -16), tag: output.slice(-16) };
|
||||
}
|
||||
|
||||
function base64(bytes: Uint8Array): string {
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
describe("vendor OTP backup importers", () => {
|
||||
it("imports Aegis plaintext entries and skips non-portable token types", () => {
|
||||
const result = importAegisBackup(
|
||||
@@ -90,4 +123,67 @@ describe("vendor OTP backup importers", () => {
|
||||
importAegisBackup(JSON.stringify({ db: "ciphertext", header: {} })),
|
||||
).toThrow(/encrypted/iu);
|
||||
});
|
||||
|
||||
it("decrypts authenticated Aegis password vaults locally", async () => {
|
||||
const password = "correct horse battery staple";
|
||||
const salt = Uint8Array.from({ length: 16 }, (_, index) => index + 1);
|
||||
const masterKey = Uint8Array.from(
|
||||
{ length: 32 },
|
||||
(_, index) => 255 - index,
|
||||
);
|
||||
const wrappingKey = scrypt(password, salt, {
|
||||
N: 16,
|
||||
r: 8,
|
||||
p: 1,
|
||||
dkLen: 32,
|
||||
maxmem: 1024 * 1024,
|
||||
});
|
||||
const slotNonce = Uint8Array.from({ length: 12 }, (_, index) => index + 20);
|
||||
const dbNonce = Uint8Array.from({ length: 12 }, (_, index) => index + 40);
|
||||
const wrapped = await encryptGcm(wrappingKey, masterKey, slotNonce);
|
||||
const database = utf8ToBytes(
|
||||
JSON.stringify({
|
||||
entries: [
|
||||
{
|
||||
type: "totp",
|
||||
name: "alice",
|
||||
issuer: "Example",
|
||||
info: { secret, algo: "SHA1", digits: 6, period: 30 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const encrypted = await encryptGcm(masterKey, database, dbNonce);
|
||||
const vault = JSON.stringify({
|
||||
version: 1,
|
||||
header: {
|
||||
slots: [
|
||||
{
|
||||
type: 1,
|
||||
n: 16,
|
||||
r: 8,
|
||||
p: 1,
|
||||
salt: bytesToHex(salt),
|
||||
key: bytesToHex(wrapped.ciphertext),
|
||||
key_params: {
|
||||
nonce: bytesToHex(slotNonce),
|
||||
tag: bytesToHex(wrapped.tag),
|
||||
},
|
||||
},
|
||||
],
|
||||
params: { nonce: bytesToHex(dbNonce), tag: bytesToHex(encrypted.tag) },
|
||||
},
|
||||
db: base64(encrypted.ciphertext),
|
||||
});
|
||||
await expect(
|
||||
importEncryptedAegisBackup(vault, password),
|
||||
).resolves.toMatchObject({
|
||||
profiles: [
|
||||
expect.objectContaining({ account: "alice", issuer: "Example" }),
|
||||
],
|
||||
});
|
||||
await expect(importEncryptedAegisBackup(vault, "wrong")).rejects.toThrow(
|
||||
/incorrect/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user