feat: TOFU key-pinning warning, restrict chat permissions to authenticated users
This commit is contained in:
@@ -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,
|
||||
|
||||
75
js/services/key-pinning.js
Normal file
75
js/services/key-pinning.js
Normal 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()
|
||||
Reference in New Issue
Block a user