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( JSON.stringify({ db: { entries: [ { type: "totp", name: "alice", issuer: "Example", info: { secret, algo: "SHA1", digits: 6, period: 30 }, }, { type: "steam", name: "game", issuer: "Steam", info: { secret } }, ], }, }), ); expect(result.profiles[0]).toMatchObject({ issuer: "Example", account: "alice", }); expect(result.warnings[0]).toMatch(/steam/iu); }); it("imports 2FAS, andOTP and FreeOTP field layouts", () => { expect( importTwoFasBackup( JSON.stringify({ services: [ { name: "Example", secret, otp: { account: "alice", issuer: "Issuer", tokenType: "TOTP", algorithm: "SHA1", digits: 6, period: 30, }, }, ], }), ).profiles[0], ).toMatchObject({ account: "alice", issuer: "Issuer" }); expect( importAndOtpBackup( JSON.stringify([ { secret, issuer: "Example", label: "bob", type: "TOTP", algorithm: "SHA1", digits: 6, period: 30, }, ]), ).profiles[0]!.account, ).toBe("bob"); expect( importFreeOtpBackup( JSON.stringify([ { secret: [49, 50, 51, 52], issuerExt: "Example", label: "carol", type: "totp", algo: "SHA1", digits: 6, period: 30, }, ]), ).profiles[0]!.account, ).toBe("carol"); }); it("refuses encrypted Aegis content instead of guessing", () => { expect(() => 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, ); }); });