feat(e2ee): F3 identity/TOFU + W2-4/W3-3 hardening — checkpoint before F3 UI

WIP save point. Server + W2-4/W3-3 complete and gate-green; F3 voice E2EE
identity keys + TOFU implemented and MITM-verified-closed; the F3 voice-panel
UI (safety-number display, verified/mismatch badge, re-pin modal) is still TODO.

- W2-4 attachment link (coverage confirmed); W3-3a XFF CIDR pre-parse;
  W3-3b update-binary TOCTOU (single-handle verify + O_EXCL staging)
- F3 server: migration 017 identity_public_key, PATCH /users/me persist,
  ready/member_join/user_update carry key, signed voice_e2ee_announce
- F3 client: ECDSA identity keypair (keyring + pin store), publish wired into
  ready, verifyPeerAnnounce pin-before-legacy, rePinPeerIdentity recovery
- Gates: server full CI mirror green (-race/-deadlock/lint/4 build tags);
  client typecheck/lint/format + 3337 vitest green. Rust CI-verify only.

Next: build F3 voice-panel UI, then adversarial review, then finalize commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-07-23 21:07:09 +02:00
co-authored by Claude Opus 4.8
parent 865dffc771
commit 81a0b63e65
61 changed files with 2814 additions and 343 deletions
+1
View File
@@ -59,6 +59,7 @@ type Store interface {
UpdateUserPassword(ctx context.Context, userID int64, newPasswordHash string) error
UpdateUserStatus(ctx context.Context, id int64, status string) error
UpdateUserTOTPSecret(ctx context.Context, id int64, secret *string) error
UpdateUserIdentityKey(ctx context.Context, id int64, key *string) error
UpdateUserRole(ctx context.Context, userID, roleID int64) error
ResetAllUserStatuses(ctx context.Context) error
DeleteAccount(ctx context.Context, userID int64) error
+17
View File
@@ -51,6 +51,23 @@ func (s *UserService) UpdateProfile(ctx context.Context, userID int64, username
return user, nil
}
// UpdateIdentityKey publishes the user's long-term E2EE identity public key
// (F3 voice E2EE TOFU). Last write wins; every write is audited so a key
// rotation — which peers surface as a TOFU mismatch — leaves a trail.
// Returns the updated user for response building.
func (s *UserService) UpdateIdentityKey(ctx context.Context, userID int64, key string) (*db.User, error) {
if err := s.st.UpdateUserIdentityKey(ctx, userID, &key); err != nil {
return nil, fmt.Errorf("%w: failed to update identity key", ErrInternal)
}
user, err := s.st.GetUserByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("%w: failed to fetch updated user", ErrInternal)
}
db.WriteAudit(context.WithoutCancel(ctx), s.st, userID, "identity_key_update", "user", userID, "")
slog.Info("identity key published", "user_id", userID)
return user, nil
}
// ChangePasswordResult reports a completed password change. RevokeFailed is
// set when the password committed but other sessions could not be revoked —
// a partial success the caller must surface as a warning, never as a 5xx: