diff --git a/tests/crypto.test.js b/tests/crypto.test.js index 4cd2935..3c1d98a 100644 --- a/tests/crypto.test.js +++ b/tests/crypto.test.js @@ -5,6 +5,7 @@ import { asyncTest, describe, assertEquals, assertTrue, assertFalse, assertNotEquals } from './test-runner.js' import { cryptoService } from '../js/services/crypto.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_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 describe('Crypto test cleanup', () => { asyncTest('cleanup test data from storage', async () => {