mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-18 13:56:55 +00:00
chat: APIVerifySimplexName command + CEvtSimplexNameUnverified warning
Addresses the TOFU vulnerability where peer-claimed simplex_name was accepted unverified. Adds: - contacts.simplex_name_verified_at + groups.simplex_name_verified_at (M20260606_simplex_name_verified) - APIVerifySimplexName ChatRef command: RSLV-resolves the claimed name and compares the resolved link to the peer's stored connection link; on match writes verified_at and emits CEvtSimplexNameVerified; on mismatch emits CEvtSimplexNameVerifyFailed - CEvtSimplexNameUnverified passive warning emitted on incoming XInfo / XGrpInfo when a name claim arrives without a current verification - updateContactProfileWithConflict / updateGroupProfileWithConflict clear simplex_name_verified_at whenever the peer's claim transitions (any value change including Nothing<->Just): the prior verification was bound to the prior claim. UI can surface the unverified indicator next to a contact / group's name, and prompt the user to invoke the verify command. This shifts the security model from "TOFU + last-writer-wins" to "TOFU + on-demand RSLV verification".
This commit is contained in:
@@ -486,6 +486,11 @@ data ChatCommand
|
||||
| APIConnectPreparedGroup {groupId :: GroupId, incognito :: IncognitoEnabled, ownerContact :: Maybe GroupOwnerContact, msgContent_ :: Maybe MsgContent}
|
||||
| APIConnect {userId :: UserId, incognito :: IncognitoEnabled, preparedLink_ :: Maybe ACreatedConnLink} -- Maybe is used to report link parsing failure as special error
|
||||
| Connect {incognito :: IncognitoEnabled, connTarget_ :: Maybe ConnectTarget}
|
||||
| -- Resolves the simplex_name claim on the chat row (contact or group) via
|
||||
-- RSLV and compares the resolved link to the peer's stored connection link.
|
||||
-- On match: writes simplex_name_verified_at and emits CEvtSimplexNameVerified.
|
||||
-- On link mismatch or resolver failure: emits CEvtSimplexNameVerifyFailed.
|
||||
APIVerifySimplexName {chatRef :: ChatRef}
|
||||
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId
|
||||
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
|
||||
| DeleteContact ContactName ChatDeleteMode
|
||||
@@ -952,11 +957,40 @@ data ChatEvent
|
||||
-- (newer-claim-wins per RSLV); displacedFrom is the old row's local
|
||||
-- display_name, claimedBy is the peer / group whose claim won.
|
||||
CEvtSimplexNameConflict {user :: User, simplexName :: SimplexNameInfo, entity :: SimplexNameConflictEntity, claimedBy :: ContactName, displacedFrom :: ContactName}
|
||||
| -- Emitted by APIVerifySimplexName when the RSLV-resolved link for the
|
||||
-- claimed name matches the peer's stored connection link. simplex_name_verified_at
|
||||
-- has been written; UI should clear the unverified indicator.
|
||||
CEvtSimplexNameVerified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, verifiedAt :: UTCTime}
|
||||
| -- Emitted by APIVerifySimplexName when verification did not succeed.
|
||||
-- The simplex_name claim is NOT cleared; the user may still wish to keep
|
||||
-- the contact/group. UI should surface the failure reason.
|
||||
CEvtSimplexNameVerifyFailed {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, reason :: SimplexNameVerifyFailReason}
|
||||
| -- Passive warning emitted when an incoming XInfo / XGrpInfo carries a
|
||||
-- simplex_name claim that the user has not (yet) verified — i.e.
|
||||
-- simplex_name_verified_at is NULL. The UI is expected to show an
|
||||
-- unverified indicator; the user can invoke APIVerifySimplexName to clear it.
|
||||
CEvtSimplexNameUnverified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo}
|
||||
deriving (Show)
|
||||
|
||||
data SimplexNameConflictEntity = SNCEContact | SNCEGroup
|
||||
deriving (Show)
|
||||
|
||||
-- | Why APIVerifySimplexName failed. The resolved record is not stashed: we
|
||||
-- intentionally do not allow a "verified to point at a DIFFERENT contact"
|
||||
-- state; the user must decide whether to keep the existing contact or start
|
||||
-- a fresh connection with the resolved link.
|
||||
data SimplexNameVerifyFailReason
|
||||
= -- | Resolver returned a NameRecord but its link for this entity's type
|
||||
-- (nrSimplexContact for contacts, nrSimplexChannel for groups) differs
|
||||
-- from the link stored locally for the peer.
|
||||
SNVFLinkMismatch
|
||||
| -- | RSLV returned AUTH (NameNotRegistered): no on-chain record exists.
|
||||
SNVFNameNotRegistered
|
||||
| -- | Transport / proxy / other resolver-side failure. The agent error is
|
||||
-- surfaced verbatim so the UI can reuse existing agent-error rendering.
|
||||
SNVFResolverError {agentError :: AgentErrorType}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data TerminalEvent
|
||||
= TEGroupLinkRejected {user :: User, groupInfo :: GroupInfo, groupRejectionReason :: GroupRejectionReason}
|
||||
| TERelayRejected {user :: User, groupInfo :: GroupInfo, relayRejectionReason :: RelayRejectionReason}
|
||||
@@ -1783,6 +1817,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse)
|
||||
|
||||
$(JQ.deriveJSON (enumJSON $ dropPrefix "SNCE") ''SimplexNameConflictEntity)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SNVF") ''SimplexNameVerifyFailReason)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CEvt") ''ChatEvent)
|
||||
|
||||
$(JQ.deriveFromJSON defaultJSON ''ArchiveConfig)
|
||||
|
||||
Reference in New Issue
Block a user