import { randomBytes } from 'node:crypto' import { describe, expect, it } from 'vitest' import { deriveKey, seal, unseal, SECRET_CONTEXT } from '../src/main/security/secretBox' const MASTER = randomBytes(32) const CTX = SECRET_CONTEXT.credential describe('secretBox', () => { it('round-trips a secret', () => { const sealed = seal(MASTER, 'hunter2', CTX) expect(unseal(MASTER, sealed, CTX)).toBe('hunter2') }) it('round-trips non-ASCII and empty strings intact', () => { // Byte-length assumptions are the classic failure here: these strings are // longer in bytes than in characters, and a naive length check or a // truncating transform mangles them. for (const secret of ['密码', 'emoji-🔐-ok', 'ünïcödé', '', 'a'.repeat(4096)]) { const sealed = seal(MASTER, secret, CTX) expect(unseal(MASTER, sealed, CTX)).toBe(secret) } }) it('does not leave the plaintext recoverable from the ciphertext', () => { const sealed = seal(MASTER, 'hunter2', CTX) expect(sealed.ciphertext.toString('latin1')).not.toContain('hunter2') }) it('produces different ciphertext each time the same secret is sealed', () => { // A fresh nonce and salt per call. Identical ciphertext would leak that two // hosts share a password. const a = seal(MASTER, 'same-secret', CTX) const b = seal(MASTER, 'same-secret', CTX) expect(a.ciphertext.equals(b.ciphertext)).toBe(false) expect(a.nonce.equals(b.nonce)).toBe(false) expect(a.salt.equals(b.salt)).toBe(false) }) it('rejects a tampered ciphertext', () => { const sealed = seal(MASTER, 'hunter2', CTX) sealed.ciphertext[0] ^= 0x01 expect(() => unseal(MASTER, sealed, CTX)).toThrow() }) it('rejects a tampered auth tag', () => { const sealed = seal(MASTER, 'hunter2', CTX) const last = sealed.ciphertext.length - 1 sealed.ciphertext[last] ^= 0x01 expect(() => unseal(MASTER, sealed, CTX)).toThrow() }) it('rejects the wrong master key', () => { const sealed = seal(MASTER, 'hunter2', CTX) expect(() => unseal(randomBytes(32), sealed, CTX)).toThrow() }) it('rejects the wrong context', () => { // Context separation: a credential blob must not decrypt as a private key // even when both are sealed under the same master key. const sealed = seal(MASTER, 'hunter2', CTX) expect(() => unseal(MASTER, sealed, SECRET_CONTEXT.privateKey)).toThrow() }) it('rejects a truncated ciphertext rather than returning a short secret', () => { const sealed = seal(MASTER, 'hunter2', CTX) sealed.ciphertext = sealed.ciphertext.subarray(0, 4) expect(() => unseal(MASTER, sealed, CTX)).toThrow() }) it('derives a stable key for identical inputs and a distinct one per salt', () => { const salt = randomBytes(16) expect(deriveKey(MASTER, salt, CTX).equals(deriveKey(MASTER, salt, CTX))).toBe(true) expect(deriveKey(MASTER, randomBytes(16), CTX).equals(deriveKey(MASTER, salt, CTX))).toBe( false, ) }) })