Files
PicList/tests/aeshelper.test.ts
2025-12-30 13:20:28 +08:00

98 lines
3.2 KiB
TypeScript

// AESHelper.spec.ts
import crypto from 'node:crypto'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AESHelper } from '../src/main/utils/aesHelper'
vi.mock('@core/picgo', () => ({
default: { getConfig: vi.fn(() => undefined) },
}))
vi.mock('~/utils/configPaths', () => ({
configPaths: { settings: { aesPassword: 'settings.aesPassword' } },
}))
const HEX_RE = /^[0-9a-f]+$/i
describe('AESHelper', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('round-trips (encrypt -> decrypt) with explicit password', () => {
const helper = new AESHelper('testPass123')
const plaintext = JSON.stringify({ hello: 'world', n: 42 })
const enc = helper.encrypt(plaintext)
const dec = helper.decrypt(enc)
expect(dec).toBe(plaintext)
})
it('produces iv:cipher hex format with 16-byte IV', () => {
const helper = new AESHelper('formatPass')
const enc = helper.encrypt('format-check')
const parts = enc.split(':')
expect(parts.length).toBe(2)
const [ivHex, cipherHex] = parts
expect(ivHex.length).toBe(32)
expect(HEX_RE.test(ivHex)).toBe(true)
expect(cipherHex.length).toBeGreaterThan(0)
expect(HEX_RE.test(cipherHex)).toBe(true)
})
it('is non-deterministic (random IV) for the same plaintext', () => {
const helper = new AESHelper('randomIvPass')
const pt = 'same-plaintext'
const a = helper.encrypt(pt)
const b = helper.encrypt(pt)
expect(a).not.toBe(b)
expect(helper.decrypt(a)).toBe(pt)
expect(helper.decrypt(b)).toBe(pt)
})
it('returns "{}" on malformed inputs (compatibility behavior)', () => {
const helper = new AESHelper('badInputPass')
expect(helper.decrypt('')).toBe('{}') // empty
expect(helper.decrypt('nocolonhere')).toBe('{}') // no separator
expect(helper.decrypt('00:abcd')).toBe('{}') // IV too short
})
it('returns "{}" if ciphertext is tampered', () => {
const helper = new AESHelper('tamperPass')
const enc = helper.encrypt('secure-data')
const [ivHex, cipherHex] = enc.split(':')
const last = cipherHex.at(-1)!
const flipped = last.toLowerCase() === 'a' ? 'b' : 'a'
const tampered = `${ivHex}:${cipherHex.slice(0, -1)}${flipped}`
expect(helper.decrypt(tampered)).toBe('{}')
})
it('two instances with the same password can decrypt each other', () => {
const h1 = new AESHelper('sharedPass')
const h2 = new AESHelper('sharedPass')
const enc1 = h1.encrypt('hello')
const enc2 = h2.encrypt('world')
expect(h2.decrypt(enc1)).toBe('hello')
expect(h1.decrypt(enc2)).toBe('world')
})
it('works with default constructor (uses picgo fallback password)', async () => {
const h1 = new AESHelper()
const h2 = new AESHelper()
const enc = h1.encrypt('fallback-ok')
expect(h2.decrypt(enc)).toBe('fallback-ok')
})
it('caches derived keys for the same password (pbkdf2Sync called once)', () => {
const spy = vi.spyOn(crypto, 'pbkdf2Sync')
const uniquePwd = 'cache-me-please-' + Math.random().toString(36).slice(2)
const a = new AESHelper(uniquePwd)
const b = new AESHelper(uniquePwd)
a.encrypt('x')
b.encrypt('y')
const callsForPwd = spy.mock.calls.filter(args => args[0] === uniquePwd)
expect(callsForPwd.length).toBe(1)
})
})