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

@@ -6,6 +6,7 @@
import { t, i18n } from '../i18n.js'
import { conversationsService } from '../services/conversations.js'
import { cryptoService } from '../services/crypto.js'
import { keyPinningService } from '../services/key-pinning.js'
import { escapeHTML } from '../utils/helpers.js'
import { reputationService } from '../services/reputation.js'
@@ -25,6 +26,7 @@ class ChatWidget extends HTMLElement {
this.deal = null
this.hasRated = false
this.mySecretKey = null
this.keyWarning = false
}
connectedCallback() {
@@ -72,6 +74,16 @@ class ChatWidget extends HTMLElement {
this.render()
return
}
const pinStatus = keyPinningService.check(this.listingId, this.sellerPublicKey)
if (pinStatus === 'changed') {
this.keyWarning = true
this.loading = false
this.render()
this.setupEventListeners()
return
}
this.conversation = await conversationsService.startOrGetConversation(this.listingId, this.sellerPublicKey)
this.mySecretKey = await conversationsService.getMySecretKeyForConversation(this.conversation)
await this.loadMessages()
@@ -85,6 +97,14 @@ class ChatWidget extends HTMLElement {
this.setupEventListeners()
}
async acceptKeyChange() {
keyPinningService.acceptChange(this.listingId, this.sellerPublicKey)
this.keyWarning = false
this.loading = true
this.render()
await this.initConversation()
}
async loadMessages() {
if (!this.conversation) return
this.messages = await conversationsService.getMessages(
@@ -141,6 +161,22 @@ class ChatWidget extends HTMLElement {
return
}
if (this.keyWarning) {
this.innerHTML = /* html */`
<div class="chat-widget">
<div class="chat-key-warning">
<div class="key-warning-icon">⚠</div>
<h4>${t('chat.keyChanged')}</h4>
<p>${t('chat.keyChangedHint')}</p>
<div class="key-warning-actions">
<button class="btn btn-outline" id="key-accept-btn">${t('chat.keyAccept')}</button>
</div>
</div>
</div>
`
return
}
this.innerHTML = /* html */`
<div class="chat-widget">
<div class="chat-header">
@@ -272,6 +308,9 @@ class ChatWidget extends HTMLElement {
}
setupEventListeners() {
const keyAcceptBtn = this.querySelector('#key-accept-btn')
keyAcceptBtn?.addEventListener('click', () => this.acceptKeyChange())
const form = this.querySelector('#chat-form')
form?.addEventListener('submit', (e) => this.handleSubmit(e))
@@ -381,6 +420,39 @@ style.textContent = /* css */`
text-align: center;
padding: var(--space-lg);
}
chat-widget .chat-key-warning {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-xl);
text-align: center;
}
chat-widget .key-warning-icon {
font-size: 2.5rem;
margin-bottom: var(--space-md);
filter: grayscale(1);
}
chat-widget .chat-key-warning h4 {
margin: 0 0 var(--space-sm);
color: var(--color-text);
}
chat-widget .chat-key-warning p {
margin: 0 0 var(--space-lg);
color: var(--color-text-muted);
font-size: var(--font-size-sm);
max-width: 300px;
}
chat-widget .key-warning-actions {
display: flex;
gap: var(--space-sm);
}
chat-widget .chat-header {
display: flex;