OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-desktop / test / encryption.cjs

130 lines · javascript ref:
// test/encryption.js - Tests for lib/encryption.cjs

const test = require('brittle')
const { encrypt, decrypt, isValidFormat } = require('../lib/encryption.cjs')
const { eventFlush } = require('./helpers/index.cjs')

test('encrypt produces valid base64', async function (t) {
  const plaintext = 'test secret message'
  const password = 'strong-password-123'

  const encrypted = encrypt(plaintext, password)

  t.ok(encrypted, 'encrypted output exists')
  t.ok(typeof encrypted === 'string', 'encrypted is string')
  t.ok(encrypted.length > plaintext.length, 'encrypted is longer than plaintext')
  t.ok(/^[A-Za-z0-9+/=]+$/.test(encrypted), 'is valid base64')

  const b4a = require('b4a')
  const decoded = b4a.from(encrypted, 'base64')
  t.ok(decoded.length >= 60, 'decoded has minimum length (salt 32 + iv 12 + tag 16)')
})

test('decrypt with correct password', async function (t) {
  const plaintext = 'my secret wallet mnemonic'
  const password = 'correct-password'

  const encrypted = encrypt(plaintext, password)
  const decrypted = decrypt(encrypted, password)

  t.is(decrypted, plaintext, 'decrypted matches original')
})

test('decrypt throws with wrong password', async function (t) {
  const plaintext = 'sensitive data'
  const password = 'correct-password'
  const wrongPassword = 'wrong-password'

  const encrypted = encrypt(plaintext, password)

  await t.exception(() => {
    decrypt(encrypted, wrongPassword)
  }, 'throws with wrong password')
})

test('decrypt throws with corrupted ciphertext', async function (t) {
  const plaintext = 'test data'
  const password = 'password123'

  const encrypted = encrypt(plaintext, password)
  const corrupted = encrypted.slice(0, -5) + 'XXXXX'

  await t.exception(() => {
    decrypt(corrupted, password)
  }, 'throws with corrupted data')
})

test('decrypt throws with truncated data', async function (t) {
  const plaintext = 'test'
  const password = 'pass'

  const encrypted = encrypt(plaintext, password)
  const truncated = encrypted.slice(0, -20)

  await t.exception(() => {
    decrypt(truncated, password)
  }, 'throws with truncated data')
})

test('decrypt throws with invalid base64', async function (t) {
  const invalid = 'not-valid-base64!!!'

  await t.exception(() => {
    decrypt(invalid, 'password')
  }, 'throws with invalid base64')
})

test('isValidFormat validates correct format', async function (t) {
  const plaintext = 'test'
  const password = 'password'

  const encrypted = encrypt(plaintext, password)

  t.ok(isValidFormat(encrypted), 'recognizes valid format')
  t.absent(isValidFormat(''), 'rejects empty string')
  t.absent(isValidFormat('!!!###'), 'rejects invalid base64')
})

test('round-trip encryption', async function (t) {
  const testCases = [
    'simple text',
    'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
    '🔐 Unicode emoji test 日本語',
    'a'.repeat(1000),
    ''
  ]

  const password = 'test-password-xyz'

  for (const plaintext of testCases) {
    const encrypted = encrypt(plaintext, password)
    const decrypted = decrypt(encrypted, password)
    t.is(decrypted, plaintext, `round-trip: "${plaintext.slice(0, 20)}"`)
  }
})

test('different passwords produce different ciphertexts', async function (t) {
  const plaintext = 'same message'
  const password1 = 'password-one'
  const password2 = 'password-two'

  const encrypted1 = encrypt(plaintext, password1)
  const encrypted2 = encrypt(plaintext, password2)

  t.not(encrypted1, encrypted2, 'different ciphertexts')
  t.is(decrypt(encrypted1, password1), plaintext, 'first decrypts')
  t.is(decrypt(encrypted2, password2), plaintext, 'second decrypts')
})

test('random IV makes ciphertexts different', async function (t) {
  const plaintext = 'repeated message'
  const password = 'same-password'

  const encrypted1 = encrypt(plaintext, password)
  const encrypted2 = encrypt(plaintext, password)

  t.not(encrypted1, encrypted2, 'random IV changes output')
  t.is(decrypt(encrypted1, password), plaintext, 'first decrypts')
  t.is(decrypt(encrypted2, password), plaintext, 'second decrypts')
})