feat: TOFU key-pinning warning, restrict chat permissions to authenticated users

This commit is contained in:
2026-02-10 07:27:46 +01:00
parent 531c32140a
commit f99178f7e3
11 changed files with 213 additions and 9 deletions

View File

@@ -1,3 +1,5 @@
// Security: Directus permissions filter by user_created=$CURRENT_USER server-side.
// Client-side participant_hash filters remain for hash-based identity matching.
import { client } from './client.js'
export async function getConversations(participantHash) {
@@ -31,6 +33,7 @@ export async function getConversation(id) {
return response.data
}
// Messages access restricted server-side to conversations owned by $CURRENT_USER
export async function getConversationMessages(conversationId) {
const response = await client.get('/items/messages', {
fields: ['*'],
@@ -51,6 +54,7 @@ export async function sendMessage(conversationId, senderHash, encryptedContent,
return response.data
}
// Directus sets user_created automatically for authenticated requests
export async function startConversation(listingId, participantHash1, participantHash2, publicKey1, publicKey2) {
const response = await client.post('/items/conversations', {
listing_id: listingId,

View File

@@ -0,0 +1,75 @@
/**
* TOFU Key Pinning Service
* Stores seller contact keys on first use, warns on key changes
*/
const PINNED_KEYS_STORAGE = 'dgray_pinned_keys'
class KeyPinningService {
constructor() {
this.pinnedKeys = this.load()
}
load() {
try {
return JSON.parse(localStorage.getItem(PINNED_KEYS_STORAGE)) || {}
} catch {
return {}
}
}
save() {
localStorage.setItem(PINNED_KEYS_STORAGE, JSON.stringify(this.pinnedKeys))
}
/**
* Check a listing's contact key against the pinned key
* @param {string} listingId
* @param {string} contactPublicKey
* @returns {'ok'|'new'|'changed'}
* - 'ok': key matches pinned key
* - 'new': first time seeing this listing, key is now pinned
* - 'changed': key differs from pinned key (possible attack)
*/
check(listingId, contactPublicKey) {
if (!listingId || !contactPublicKey) return 'new'
const pinned = this.pinnedKeys[listingId]
if (!pinned) {
this.pin(listingId, contactPublicKey)
return 'new'
}
if (pinned === contactPublicKey) return 'ok'
return 'changed'
}
pin(listingId, contactPublicKey) {
this.pinnedKeys[listingId] = contactPublicKey
this.save()
}
/**
* Accept a changed key (user explicitly trusts the new key)
*/
acceptChange(listingId, newPublicKey) {
this.pinnedKeys[listingId] = newPublicKey
this.save()
}
/**
* Remove pin for a listing
*/
unpin(listingId) {
delete this.pinnedKeys[listingId]
this.save()
}
clear() {
this.pinnedKeys = {}
localStorage.removeItem(PINNED_KEYS_STORAGE)
}
}
export const keyPinningService = new KeyPinningService()