test: add per-listing keypair and TOFU key-pinning tests

This commit is contained in:
2026-02-10 07:31:49 +01:00
parent f99178f7e3
commit 73769d6af2

View File

@@ -5,6 +5,7 @@
import { asyncTest, describe, assertEquals, assertTrue, assertFalse, assertNotEquals } from './test-runner.js' import { asyncTest, describe, assertEquals, assertTrue, assertFalse, assertNotEquals } from './test-runner.js'
import { cryptoService } from '../js/services/crypto.js' import { cryptoService } from '../js/services/crypto.js'
import { conversationsService } from '../js/services/conversations.js' import { conversationsService } from '../js/services/conversations.js'
import { keyPinningService } from '../js/services/key-pinning.js'
const TEST_UUID = '00000000-0000-4000-a000-000000000001' const TEST_UUID = '00000000-0000-4000-a000-000000000001'
const TEST_UUID_2 = '00000000-0000-4000-a000-000000000002' const TEST_UUID_2 = '00000000-0000-4000-a000-000000000002'
@@ -250,6 +251,126 @@ describe('ConversationsService.hashPublicKey', () => {
}) })
}) })
// ── Per-listing keypair management ──
describe('Per-listing keypair management', () => {
asyncTest('generateListingKeyPair creates and stores keypair', async () => {
localStorage.removeItem('dgray_listing_keys')
await cryptoService.unlock(TEST_UUID)
const publicKey = await cryptoService.generateListingKeyPair('test-listing-1')
assertTrue(publicKey !== null)
assertTrue(publicKey.length > 10)
})
asyncTest('getListingSecretKey retrieves stored key', async () => {
const secretKey = await cryptoService.getListingSecretKey('test-listing-1')
assertTrue(secretKey !== null)
assertTrue(secretKey.length > 10)
})
asyncTest('getListingSecretKey returns null for unknown listing', async () => {
const secretKey = await cryptoService.getListingSecretKey('nonexistent')
assertEquals(secretKey, null)
})
asyncTest('encrypt/decrypt with listing-specific secret key', async () => {
await cryptoService.ready
const nacl = cryptoService.nacl
const naclUtil = cryptoService.naclUtil
// Simulate: seller creates listing key, buyer uses account key
const buyerKp = nacl.box.keyPair()
const buyerPub = naclUtil.encodeBase64(buyerKp.publicKey)
// Seller generates listing keypair
await cryptoService.unlock(TEST_UUID)
const sellerListingPub = await cryptoService.generateListingKeyPair('test-listing-2')
const sellerListingSk = await cryptoService.getListingSecretKey('test-listing-2')
// Buyer encrypts to seller listing key
cryptoService.keyPair = buyerKp
const { nonce, ciphertext } = cryptoService.encrypt('Hello seller!', sellerListingPub)
// Seller decrypts using listing secret key (not account key)
const plaintext = cryptoService.decrypt(ciphertext, nonce, buyerPub, sellerListingSk)
assertEquals(plaintext, 'Hello seller!')
})
asyncTest('seller can encrypt back to buyer using listing key', async () => {
await cryptoService.ready
const nacl = cryptoService.nacl
const naclUtil = cryptoService.naclUtil
const buyerKp = nacl.box.keyPair()
const buyerPub = naclUtil.encodeBase64(buyerKp.publicKey)
const sellerListingSk = await cryptoService.getListingSecretKey('test-listing-2')
assertTrue(sellerListingSk !== null)
// Seller encrypts with listing key
const { nonce, ciphertext } = cryptoService.encrypt('Hi buyer!', buyerPub, sellerListingSk)
// Buyer decrypts
const sk = naclUtil.decodeBase64(sellerListingSk)
const kp = nacl.box.keyPair.fromSecretKey(sk)
const sellerListingPub = naclUtil.encodeBase64(kp.publicKey)
cryptoService.keyPair = buyerKp
const plaintext = cryptoService.decrypt(ciphertext, nonce, sellerListingPub)
assertEquals(plaintext, 'Hi buyer!')
})
asyncTest('destroyKeyPair also clears listing keys', async () => {
await cryptoService.unlock(TEST_UUID)
await cryptoService.generateListingKeyPair('temp-listing')
cryptoService.destroyKeyPair()
assertEquals(localStorage.getItem('dgray_listing_keys'), null)
})
})
// ── Key pinning (TOFU) ──
describe('KeyPinningService (TOFU)', () => {
asyncTest('check returns "new" for unknown listing and pins it', async () => {
keyPinningService.clear()
const result = keyPinningService.check('listing-a', 'pubkey-123')
assertEquals(result, 'new')
})
asyncTest('check returns "ok" for matching pinned key', async () => {
const result = keyPinningService.check('listing-a', 'pubkey-123')
assertEquals(result, 'ok')
})
asyncTest('check returns "changed" when key differs from pinned', async () => {
const result = keyPinningService.check('listing-a', 'pubkey-different')
assertEquals(result, 'changed')
})
asyncTest('acceptChange updates pinned key', async () => {
keyPinningService.acceptChange('listing-a', 'pubkey-different')
const result = keyPinningService.check('listing-a', 'pubkey-different')
assertEquals(result, 'ok')
})
asyncTest('unpin removes pin for listing', async () => {
keyPinningService.unpin('listing-a')
const result = keyPinningService.check('listing-a', 'pubkey-new')
assertEquals(result, 'new')
})
asyncTest('clear removes all pinned keys', async () => {
keyPinningService.check('listing-x', 'key-x')
keyPinningService.check('listing-y', 'key-y')
keyPinningService.clear()
assertEquals(keyPinningService.check('listing-x', 'key-x'), 'new')
assertEquals(keyPinningService.check('listing-y', 'key-y'), 'new')
keyPinningService.clear()
})
})
// Cleanup after all tests // Cleanup after all tests
describe('Crypto test cleanup', () => { describe('Crypto test cleanup', () => {
asyncTest('cleanup test data from storage', async () => { asyncTest('cleanup test data from storage', async () => {