From 26a8d382e4c7f32373c25672164022b62c2a6ca4 Mon Sep 17 00:00:00 2001 From: cblgh Date: Tue, 16 Jun 2026 09:58:18 +0200 Subject: [PATCH 01/20] wip: re-add mtls, start aligning responses with other clients mTLs implementation does not follow spec yet but is the state of mTLS prior to it being removed from the previous sprint. most of the work has consisted in spec work so far :) --- backend/core/modules/registration/handler.go | 29 ++++++++--- backend/core/modules/server/handler.go | 11 ++++- backend/core/modules/server/service.go | 49 ++++++++++++++++++- backend/core/modules/transfer/service.go | 2 +- backend/utils/devlog/devlog.go | 2 +- .../utils/filestoreutils/filestoreutils.go | 1 - backend/utils/tls/certificate.go | 2 +- 7 files changed, 81 insertions(+), 15 deletions(-) diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index 7edb5ca..a503e95 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -42,6 +42,7 @@ func NewHandler(service Service, ctx context.Context) *Handler { } } +// TODO (2026-06-15): only respond to ping if not registered/registration not in progress func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) @@ -54,15 +55,11 @@ func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { "state": "waiting", }) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(struct { - Status string `json:"status"` - }{ - Status: "ok", - }) + w.WriteHeader(http.StatusOK) } -func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) { +// rememberClientFingerprint changes tls config of package server. this change also restarts the https server instance. +func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, rememberClientFingerprint func (string) error) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -79,6 +76,9 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) { var request struct { PIN string `json:"pin"` Nonce string `json:"nonce"` + // TODO (2026-06-09): remove cert hash from request payload, and instead extract it from the request's connection information? + // OTHERWISE: document inclusion of senderCertHash in protocol once more + SenderCertificateHash string `json:"senderCertificateHash"` } if err := json.Unmarshal(requestBody, &request); err != nil { @@ -87,7 +87,7 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) { return } - if request.PIN == "" || request.Nonce == "" { + if request.PIN == "" || request.Nonce == "" || len(request.SenderCertificateHash) != 64 { http.Error(w, "Missing required parameters", http.StatusBadRequest) return } @@ -117,6 +117,19 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) { h.mu.Lock() h.pendingRegistration = nil + // if we're sending a successful response it was because the pin was valid! + // since PIN was valid, we can now save the sender's certificate hash and restart the server + // note: since `rememberClientFingerprint` requires a restart of the https server, we likely have to send the + // response before restarting? + err = rememberClientFingerprint(request.SenderCertificateHash) + // TODO cblgh(2026-03-15): figure out something less catastrophic for the app than just panic here. but https + // handler is a hard place to recover from the death of the https server ^^' + // + // maybe emit some kind of event to frontend to signal catastrophic failure && need to restart? + // runtime.EventsEmit(h.ctx, "register-request-received", map[string]interface{}{ + if err != nil { + panic(err) + } h.mu.Unlock() case err := <-h.pendingRegistration.Error: diff --git a/backend/core/modules/server/handler.go b/backend/core/modules/server/handler.go index a787618..1366fcb 100644 --- a/backend/core/modules/server/handler.go +++ b/backend/core/modules/server/handler.go @@ -24,10 +24,17 @@ func NewHandler( } } -func (h *Handler) SetupRoutes() { +// TODO (2026-06-15): implement /api/v2/* +// TODO (2026-06-15): keep /api/v1/ping /api/v1/register around and serve legacy responses to sent queries there +func (h *Handler) SetupRoutes(pinFingerprint func (string) error) { h.mux.HandleFunc("/api/v1/ping", h.registrationHandler.HandlePing) - h.mux.HandleFunc("/api/v1/register", h.registrationHandler.HandleRegister) + h.mux.HandleFunc("/api/v1/register", func(w http.ResponseWriter, r *http.Request) { + h.registrationHandler.HandleRegister(w, r, pinFingerprint) + }) h.mux.HandleFunc("/api/v1/prepare-upload", h.transferHandler.HandlePrepare) h.mux.HandleFunc("/api/v1/upload", h.transferHandler.HandleUpload) h.mux.HandleFunc("/api/v1/close-connection", h.transferHandler.HandleCloseConnection) + h.mux.HandleFunc("/", func (res http.ResponseWriter, req *http.Request) { + http.Error(res, "Not found", http.StatusNotFound) + }) } diff --git a/backend/core/modules/server/service.go b/backend/core/modules/server/service.go index 6057edb..efe014b 100644 --- a/backend/core/modules/server/service.go +++ b/backend/core/modules/server/service.go @@ -5,6 +5,8 @@ import ( crand "crypto/rand" ctls "crypto/tls" "fmt" + "crypto/x509" + "crypto/sha256" "math/big" "net" "net/http" @@ -28,6 +30,7 @@ import ( var log = devlog.Logger("server") type service struct { + fingerprint string // pinned fingerprint for sender limitingMiddleware http.Handler nonceManager *nonces.NonceManager limiter *RateLimitingWare @@ -139,11 +142,31 @@ func (s *service) Start(port int) error { Organization: []string{"Tella"}, IPAddresses: ips, }) + if err != nil { log("failed to generate TLS config: %v", err) return errStart } + // do not require any client certs when server is freshly started + tlsConfig.ClientAuth = ctls.NoClientCert + // NOTE cblgh(2026-03-15): set up a custom cert pool using the pinned cert? + // to allow use of tls.Config.ClientAuth: tls.RequireAndVerifyClientCert + // c.f https://stackoverflow.com/a/63317898 + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + log("VerifyPeerCertificate called") + encodedCert, err := tls.EncodeCertAsPEM(rawCerts[0]) + if err != nil { + return err + } + calculatedPEMHash := sha256.Sum256(encodedCert) + log("incoming cert hash\n%x\n", calculatedPEMHash) + if fmt.Sprintf("%x", calculatedPEMHash) != s.fingerprint { + return errors.New("pin did not match") + } + return nil + } + s.tlsConfig = tlsConfig mux := http.NewServeMux() @@ -153,7 +176,7 @@ func (s *service) Start(port int) error { // TODO (2026-02-19): dhekra / iOS closes the server when the transfer is explicitly stopped handler := NewHandler(mux, s.registrationHandler, transferHandler) - handler.SetupRoutes() + handler.SetupRoutes(s.PinFingerprint) s.limitingMiddleware = s.limiter.Handler(mux) s.port = port @@ -200,6 +223,29 @@ func (s *service) startServer() error { return nil } +// PinFingerprint pins the SHA256 hash of the PEM-encoded cert. The TLS config is changed to require a client cert, which necessitates restarting the https server instance. +func (s *service) PinFingerprint(fingerprint string) error { + log("Pin fingerprint called") + if len(fingerprint) != 64 { + return errors.New("expected fingerprint string length of 64ch") + } + s.fingerprint = fingerprint + // terminate the previous instance + shutdownCtx, cancel := context.WithTimeout(s.ctx, 1500*time.Millisecond) + defer cancel() + if err := s.server.Shutdown(shutdownCtx); err != nil { + log("Graceful shutdown failed: %v, forcing close\n", err) + } + log("Stopped server & restarting") + // change the tls config to require client certs on connection going forward. + // we use `tls.RequireAnyClientCert` as we do not have the full client cert + // => can't create and pass a cert pool that will allow tls.VerifyCertificate to succeed + s.tlsConfig.ClientAuth = ctls.RequireAnyClientCert + // NOTE cblgh(2026-03-15): we need to allocate a new instance of http.Server and set the updated TLS config on it + // before restarting the server for the config changes to take effect + return s.startServer() +} + func (s *service) Stop(ctx context.Context) error { s.mu.Lock() defer s.mu.Unlock() @@ -221,6 +267,7 @@ func (s *service) Stop(ctx context.Context) error { s.server = nil s.tlsConfig = nil s.limitingMiddleware = nil + s.fingerprint = "" log("HTTPS Server stopped\n") diff --git a/backend/core/modules/transfer/service.go b/backend/core/modules/transfer/service.go index 1b3ecbd..f4e414a 100644 --- a/backend/core/modules/transfer/service.go +++ b/backend/core/modules/transfer/service.go @@ -225,7 +225,7 @@ func (s *service) AcceptTransfer(sessionID string) error { log("Transfer accepted for session: %s", sessionID) return nil default: - fmt.Errorf("failed to send acceptance response") + log("failed to send acceptance response") return errAccept } } diff --git a/backend/utils/devlog/devlog.go b/backend/utils/devlog/devlog.go index c1fc978..5c1a608 100644 --- a/backend/utils/devlog/devlog.go +++ b/backend/utils/devlog/devlog.go @@ -6,7 +6,7 @@ import ( func Log(usageContext, msg string, args ...interface{}) { if IsDevelop() { - line := fmt.Sprintf(msg) + line := msg if len(args) > 0 { line = fmt.Sprintf(msg, args...) } diff --git a/backend/utils/filestoreutils/filestoreutils.go b/backend/utils/filestoreutils/filestoreutils.go index 6081f84..5a96156 100644 --- a/backend/utils/filestoreutils/filestoreutils.go +++ b/backend/utils/filestoreutils/filestoreutils.go @@ -310,7 +310,6 @@ func ExportSingleFile(db *sql.DB, dbKey []byte, id int64, tvault *os.File, expor decryptedData, fileName, err := decryptAndGetFilename(db, id, dbKey, tvault) if err != nil { return "", err - return "", errExportFile } defer util.SecureZeroMemory(decryptedData) diff --git a/backend/utils/tls/certificate.go b/backend/utils/tls/certificate.go index 4b6146d..92dcb28 100644 --- a/backend/utils/tls/certificate.go +++ b/backend/utils/tls/certificate.go @@ -50,7 +50,7 @@ func GenerateTLSConfig(ctx context.Context, config Config) (*tls.Config, error) //generate hash of certificate hash := sha256.Sum256(cert.Certificate[0]) hashStr := hex.EncodeToString(hash[:]) - log("Hash value: %s", hashStr) + log("Receiver certificate hash: %s", hashStr) runtime.EventsEmit(ctx, "certificate-hash", hashStr) tlsConfig := &tls.Config{ From c4aac16a65a409b3f20e092e7b1ed3ca2c570388 Mon Sep 17 00:00:00 2001 From: cblgh Date: Tue, 16 Jun 2026 09:59:58 +0200 Subject: [PATCH 02/20] wip frontend mTLS: QR removed, starting to introduce changes into the connection flow the QR code flow has been disabled and removed from being visible. however QR code logic has been kept in case we reintroduce it at a later stage during this work (still in the air?) this commit has started to add changes to the connection flow as reflected in the new designs, including the new spinner modal graphic. this commit acts as a checkpoint before more widespread changes are attempted to be implemented --- .../CertificateHash/CertificateHash.tsx | 52 ------- .../CertificateVerificationModal.tsx | 143 +++++++++--------- .../src/Components/CertificateHash/index.ts | 1 - .../src/Components/NearbySharing/Connect.tsx | 22 +-- .../NearbySharing/Hooks/useNearbySharing.tsx | 34 ++++- .../NearbySharing/NearbySharing.tsx | 4 + .../Components/NearbySharing/SpinnerModal.tsx | 135 +++++++++++++++++ frontend/src/util/util.ts | 15 ++ 8 files changed, 260 insertions(+), 146 deletions(-) delete mode 100644 frontend/src/Components/CertificateHash/CertificateHash.tsx delete mode 100644 frontend/src/Components/CertificateHash/index.ts create mode 100644 frontend/src/Components/NearbySharing/SpinnerModal.tsx diff --git a/frontend/src/Components/CertificateHash/CertificateHash.tsx b/frontend/src/Components/CertificateHash/CertificateHash.tsx deleted file mode 100644 index 55e80c6..0000000 --- a/frontend/src/Components/CertificateHash/CertificateHash.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// frontend/src/Components/CertificateHash/CertificateHash.tsx -import React, { useState, useEffect } from 'react'; -import { EventsOn } from '../../../wailsjs/runtime/runtime'; -import { log } from "../../util/util" - -interface Props { - serverRunning: boolean; -} - -function formatHash(hashString: string): string { - const input = []; - for (let i = 0; i <= hashString.length; i += 4) { - // grab groups of 4 characters each - let entry = hashString.slice(i, i+4) + " "; - // output a newline after four groups of 4 - if (((i+4) % 16) === 0) { - entry += "\n"; - } - input.push(entry); - } - // trim the last newline and return as a string - return input.join("").trim(); -} - -export function CertificateHash({ serverRunning }: Props) { - const [certHash, setCertHash] = useState(''); - - useEffect(() => { - log("Subscribing to certificate-hash events"); - - const cleanup = EventsOn("certificate-hash", (data) => { - log("Received certificate hash event:", data); - setCertHash(data.toString()); - }); - - return () => { - log("Cleaning up certificate-hash subscription"); - cleanup(); - }; - }, []); - - if (!serverRunning || !certHash) { - return null; - } - - return ( -
-

Certificate Hash:

-
{formatHash(certHash)}
-
- ); -} diff --git a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx index 898e2e4..b9e1683 100644 --- a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx +++ b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx @@ -1,4 +1,6 @@ import styled from 'styled-components'; +import { formatHash } from "../../util/util" +import { SpinnerModal } from "../NearbySharing/SpinnerModal"; interface CertificateVerificationModalProps { isOpen: boolean; @@ -8,20 +10,8 @@ interface CertificateVerificationModalProps { onDiscard: () => void; } -function formatHash(hashString: string): string { - const input = []; - for (let i = 0; i <= hashString.length; i += 4) { - // grab groups of 4 characters each - let entry = hashString.slice(i, i+4) + " "; - // output a newline after four groups of 4 - if (((i+4) % 16) === 0) { - entry += "\n"; - } - input.push(entry); - } - // trim the last newline and return as a string - return input.join("").trim(); -} +// TODO (2026-06-16): revamp modal states to use: +// type ManualConfirmationSteps = 'COMFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER' export function CertificateVerificationModal({ isOpen, @@ -32,6 +22,22 @@ export function CertificateVerificationModal({ }: CertificateVerificationModalProps) { if (!isOpen) return null; + /* TODO (2026-06-09): add conditional that sets the background colour -> bc + 1. verifying receiver (desktop)'s cert hash has one (lighter?) color + 2. verifying sender s cert hash has another (darker?) color + 3. Positive action as different text depending on stage: + * "Confirm and continue" + * "Confirm and connect" + */ + + if (modalState === 'waiting') { + return ( + + ) + } return ( @@ -39,36 +45,31 @@ export function CertificateVerificationModal({ Verification - - - To ensure that the connection is safe, make sure that the sequence of numbers below matches what is shown on the other device. - - - -
-                {formatHash(certificateHash)}
-            
-
- - - If the sequences do not match, the connection may not be secure and should be discarded. - -
- + + Step X: Confirm Y hash + + + +
+        {formatHash(certificateHash)}
+        
+
+ + + Make sure that this sequence matches what is shown on the sender's device. + + + If the sequence on your device does not match the sequence on the sender's device, the connection may not be secure and should be discarded. + + DISCARD AND START OVER - {modalState === 'waiting' ? ( - - WAITING FOR THE SENDER... - - ) : ( - - CONFIRM AND CONNECT - - )} + + CONFIRM AND CONTINUE +
@@ -92,17 +93,16 @@ const ModalOverlay = styled.div` const ModalContainer = styled.div` background-color: white; border-radius: 8px; - max-width: 500px; + max-width: 550px; width: 90%; max-height: 80vh; overflow: hidden; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 2rem 4rem 2rem 4rem; `; const ModalHeader = styled.div` - padding: 1.5rem; - border-bottom: 1px solid #e9ecef; - text-align: center; + padding-top: 1.5rem; `; const Title = styled.h2` @@ -112,23 +112,39 @@ const Title = styled.h2` font-weight: 600; `; -const ModalContent = styled.div` - padding: 2rem 1.5rem; - text-align: center; -`; - const Description = styled.p` - color: #6c757d; - margin-bottom: 2rem; + color: #5F6368; + font-weight: 600; font-size: 1rem; line-height: 1.5; `; +const ModalFooter = styled.div` + padding: 1.5rem 0; + display: flex; + gap: 1rem; + justify-content: end; +`; + +const Button = styled.button` + padding: 0.75rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + text-transform: uppercase; + min-width: 80px; +`; + + const HashContainer = styled.div` background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; - padding: 1.5rem; + padding: 0rem 1.5rem; + display: flex; + justify-content: center; `; const HashText = styled.code` @@ -140,29 +156,12 @@ const HashText = styled.code` `; const Warning = styled.p` - color: #6c757d; - font-size: 0.9rem; + color: #5F6368; + font-size: 1rem; line-height: 1.5; margin: 0; -`; - -const ModalFooter = styled.div` - padding: 1.5rem; - border-top: 1px solid #e9ecef; - display: flex; - gap: 1rem; - justify-content: center; -`; - -const Button = styled.button` - padding: 0.75rem 1.5rem; - border-radius: 4px; - font-size: 0.875rem; - font-weight: 600; - cursor: pointer; - transition: background-color 0.2s; - text-transform: uppercase; - min-width: 140px; + padding-top: 1rem; + text-align: left; `; const DiscardButton = styled(Button)` diff --git a/frontend/src/Components/CertificateHash/index.ts b/frontend/src/Components/CertificateHash/index.ts deleted file mode 100644 index 430fdd0..0000000 --- a/frontend/src/Components/CertificateHash/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CertificateHash' \ No newline at end of file diff --git a/frontend/src/Components/NearbySharing/Connect.tsx b/frontend/src/Components/NearbySharing/Connect.tsx index 533321f..e6e4c7f 100644 --- a/frontend/src/Components/NearbySharing/Connect.tsx +++ b/frontend/src/Components/NearbySharing/Connect.tsx @@ -65,7 +65,7 @@ export function ConnectStep({ serverRunning, localIPs, certificateHash, isQRMode {isQRMode ? "Show this QR code for the sender to scan." - : "The sender needs to input the following to connect to your device." + : "The sender should input the following information in Tella on their phone." } @@ -95,7 +95,7 @@ export function ConnectStep({ serverRunning, localIPs, certificateHash, isQRMode ) : ( <> - IP ADDRESS + IP addresses {localIPs.join(', ')} @@ -110,21 +110,10 @@ export function ConnectStep({ serverRunning, localIPs, certificateHash, isQRMode )} - - - {isQRMode - ? "Having trouble with the QR code?" - : "Go back to automatic connection" - } - - - - {isQRMode ? 'CONNECT MANUALLY' : 'SEE QR CODE'} - - You will automatically move to the next screen as soon as the connection with the sender is established + You will automatically move to the next screen as soon as the connection with the sender is established. ); @@ -137,8 +126,8 @@ const StepContent = styled.div` `; const StepTitle = styled.h2` - font-size: 1.2rem; - font-weight: 600; + font-size: 1rem; + font-weight: 400; color: ##5F6368; margin-bottom: 2rem; `; @@ -148,6 +137,7 @@ const DeviceInfoCard = styled.div` border-radius: 8px; margin-bottom: 2rem; text-align: left; + padding-bottom: 1rem; `; const DeviceInfoHeader = styled.div` diff --git a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx index 95eb8ae..764c87a 100644 --- a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx @@ -8,6 +8,10 @@ import { log } from "../../../util/util" type FlowStep = 'intro' | 'connect' | 'accept' | 'receive' | 'results'; type ModalState = 'waiting' | 'confirm'; +// TODO (2026-06-15): use these state transitions for manual confirmation +// TODO (2026-06-15): remove 'waiting' from hash verification modal +type ManualConfirmationSteps = 'COMFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER' + interface FileInfo { id: string; fileName: string; @@ -45,12 +49,13 @@ export function useNearbySharing() { // Certificate verification state const [showVerificationModal, setShowVerificationModal] = useState(false); + const [showWaitingForSenderModal, setShowWaitingForSenderModal] = useState(false); const [certificateHash, setCertificateHash] = useState(''); const [modalState, setModalState] = useState('waiting'); // Connection mode state - const [isUsingQRMode, setIsUsingQRMode] = useState(true); - const isUsingQRModeRef = useRef(true); + const [isUsingQRMode, setIsUsingQRMode] = useState(false); + const isUsingQRModeRef = useRef(false); // Initialize network info and event listeners useEffect(() => { @@ -66,7 +71,7 @@ export function useNearbySharing() { fetchNetworkInfo(); const cleanupPingListener = EventsOn("ping-received", (data) => { - log("Ping received from iOS device:", data); + log("Ping received:", data); setShowVerificationModal(true); setModalState('waiting') }); @@ -101,6 +106,7 @@ export function useNearbySharing() { setCertificateHash(data.toString()); }); + // TODO (2026-06-16): "prepare-upload-request" is this the transition that ends the second "waiting for sender" screen? const cleanupPrepareRequest = EventsOn("prepare-upload-request", (data) => { log("📨 Received prepare upload request in parent:", data); const requestData = data as TransferData; @@ -167,6 +173,21 @@ export function useNearbySharing() { } }; + // TODO (2026-06-15): revise copy of handleVerificationDiscard to make sure is implemented properly + const handleWaitingForSenderCancel = async () => { + log("❌ Waiting for sender CANCELED"); + try { + await RejectRegistration(); + } catch (error) { + console.error("Failed to reject registration:", error); + } + setShowVerificationModal(false); + setShowWaitingForSenderModal(false); + setModalState('waiting'); + await handleStopServer(); + setCurrentStep('intro'); + }; + const handleVerificationDiscard = async () => { log("❌ Certificate verification DISCARDED"); try { @@ -252,11 +273,12 @@ export function useNearbySharing() { setCurrentSessionId(''); setTransferData(null); setShowVerificationModal(false); + setShowWaitingForSenderModal(false); setCertificateHash(''); setModalState('waiting'); setCurrentStep('intro'); - setIsUsingQRMode(true); - isUsingQRModeRef.current = true; + setIsUsingQRMode(false); + isUsingQRModeRef.current = false; }; return { @@ -268,6 +290,7 @@ export function useNearbySharing() { currentSessionId, transferData, showVerificationModal, + showWaitingForSenderModal, certificateHash, modalState, isUsingQRMode, @@ -286,6 +309,7 @@ export function useNearbySharing() { handleContinue, handleVerificationConfirm, handleVerificationDiscard, + handleWaitingForSenderCancel, handleFileRequestAccept, handleFileRequestReject, handleFileReceiving, diff --git a/frontend/src/Components/NearbySharing/NearbySharing.tsx b/frontend/src/Components/NearbySharing/NearbySharing.tsx index 8128fab..996f98b 100644 --- a/frontend/src/Components/NearbySharing/NearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/NearbySharing.tsx @@ -16,6 +16,7 @@ export function NearbySharing() { currentSessionId, transferData, showVerificationModal, + showWaitingForSenderModal, certificateHash, modalState, isUsingQRMode, @@ -25,6 +26,7 @@ export function NearbySharing() { handleContinue, handleVerificationConfirm, handleVerificationDiscard, + handleWaitingForSenderCancel, handleFileRequestAccept, handleFileRequestReject, handleFileReceiving, @@ -101,6 +103,8 @@ export function NearbySharing() { ); } +// TODO (2026-06-16): pass also the "Sender Certificate Hash" to CertificateVerificationModal (+ ensuing rework that enables that) + const Container = styled.div` display: flex; flex-direction: column; diff --git a/frontend/src/Components/NearbySharing/SpinnerModal.tsx b/frontend/src/Components/NearbySharing/SpinnerModal.tsx new file mode 100644 index 0000000..493d957 --- /dev/null +++ b/frontend/src/Components/NearbySharing/SpinnerModal.tsx @@ -0,0 +1,135 @@ +import styled, { keyframes } from 'styled-components'; + +interface SpinnerModalProps { + isOpen: boolean; + onCancel: () => void; +} + +export function SpinnerModal({ + isOpen, + onCancel +}: SpinnerModalProps) { + if (!isOpen) return null; + + return ( + + + + Verification + + + + Waiting for the sender to also confirm. + + + + + + + + + + + + + CANCEL + + + + + ); +} + +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`; + +const ModalContainer = styled.div` + background-color: white; + border-radius: 8px; + max-width: 500px; + width: 90%; + max-height: 80vh; + overflow: hidden; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 2rem 4rem 2rem 4rem; +`; + +const ModalHeader = styled.div` + padding-top: 1.5rem; +`; + +const Title = styled.h2` + margin: 0; + color: #212529; + font-size: 1.5rem; + font-weight: 600; +`; + +const Description = styled.p` + color: #6c757d; + margin-bottom: 2rem; + font-size: 1rem; + line-height: 1.5; +`; + +const ModalFooter = styled.div` + padding: 1.5rem 0; + display: flex; + gap: 1rem; + justify-content: end; +`; + +const Button = styled.button` + padding: 0.75rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + text-transform: uppercase; + min-width: 80px; +`; + +const CancelButton = styled(Button)` + background-color: white; + color: #6c757d; + border: 1px solid #D9D9D9; + + &:hover { + background-color: #f8f9fa; + } +`; + +const SpinnerContainer = styled.div` + text-align: center; +`; + +const spinnerAnimOne = keyframes` + 100% {transform:rotate(360deg)} +`; + +const spinnerAnimTwo = keyframes` + 0% {stroke-dasharray:0 150;stroke-dashoffset:0} + 47.5% {stroke-dasharray:42 150;stroke-dashoffset:-16} + 95%,100%{stroke-dasharray:42 150;stroke-dashoffset:-59} +`; + +const SpinnerG = styled.g` + transform-origin:center; + animation:${spinnerAnimOne} 2s linear infinite +`; + +const SpinnerCircle = styled.circle` + stroke-linecap:round;animation:${spinnerAnimTwo} 1.5s ease-in-out infinite +`; + diff --git a/frontend/src/util/util.ts b/frontend/src/util/util.ts index e4c2d38..96d4cdc 100644 --- a/frontend/src/util/util.ts +++ b/frontend/src/util/util.ts @@ -12,3 +12,18 @@ export async function log(msg: string, ...rest: any[]): Promise { console.log(msg, ...rest) } } + +export function formatHash(hashString: string): string { + const input = []; + for (let i = 0; i <= hashString.length; i += 4) { + // grab groups of 4 characters each + let entry = hashString.slice(i, i+4) + " "; + // output a newline after four groups of 4 + if (((i+4) % 16) === 0) { + entry += "\n"; + } + input.push(entry); + } + // trim the last newline and return as a string + return input.join("").trim(); +} From 07e0fde8798704023c89ea250a8daa7363d13cd8 Mon Sep 17 00:00:00 2001 From: cblgh Date: Wed, 17 Jun 2026 10:12:36 +0200 Subject: [PATCH 03/20] deps: remove qr code, selectively bump versions to fix high severity npm audit warnings --- frontend/package-lock.json | 345 ++++--------------------------------- frontend/package.json | 6 +- frontend/package.json.md5 | 2 +- 3 files changed, 32 insertions(+), 321 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8c7afd5..fa0c7f4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,12 +8,10 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@types/qrcode": "^1.5.5", - "dompurify": "^3.3.1", - "qrcode": "^1.5.4", + "dompurify": "^3.4.9", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^7.6.3", + "react-router-dom": "^7.15.1", "styled-components": "^6.1.15" }, "devDependencies": { @@ -71,7 +69,6 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -508,6 +505,9 @@ "version": "24.5.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -519,21 +519,12 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/qrcode": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", - "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/react": { "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -619,28 +610,6 @@ "vite": "^3.0.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/browserslist": { "version": "4.24.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", @@ -661,7 +630,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -675,14 +643,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -713,32 +673,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -747,12 +681,15 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/css-color-keywords": { @@ -799,24 +736,10 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" - }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", - "license": "(MPL-2.0 OR Apache-2.0)", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", + "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -828,11 +751,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, "node_modules/esbuild": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", @@ -1221,18 +1139,6 @@ "node": ">=6" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1268,14 +1174,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -1325,14 +1223,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1365,17 +1255,6 @@ "node": ">=6" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1443,47 +1322,6 @@ "dev": true, "license": "MIT" }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1497,14 +1335,6 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -1539,28 +1369,11 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -1573,7 +1386,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -1600,10 +1412,9 @@ } }, "node_modules/react-router": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz", - "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==", - "license": "MIT", + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz", + "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==", "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -1622,12 +1433,11 @@ } }, "node_modules/react-router-dom": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.3.tgz", - "integrity": "sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw==", - "license": "MIT", + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz", + "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==", "dependencies": { - "react-router": "7.6.3" + "react-router": "7.15.1" }, "engines": { "node": ">=20.0.0" @@ -1637,19 +1447,6 @@ "react-dom": ">=18" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -1706,16 +1503,10 @@ "semver": "bin/semver.js" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" }, "node_modules/shallowequal": { "version": "1.1.0", @@ -1740,30 +1531,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/styled-components": { "version": "6.1.15", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.15.tgz", @@ -1834,7 +1601,10 @@ "node_modules/undici-types": { "version": "7.12.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==" + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/update-browserslist-db": { "version": "1.1.1", @@ -1873,7 +1643,6 @@ "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.15.9", "postcss": "^8.4.18", @@ -1918,68 +1687,12 @@ } } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" - }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } } } } diff --git a/frontend/package.json b/frontend/package.json index 641ed25..081edf3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,12 +9,10 @@ "preview": "vite preview" }, "dependencies": { - "@types/qrcode": "^1.5.5", - "dompurify": "^3.3.1", - "qrcode": "^1.5.4", + "dompurify": "^3.4.9", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^7.6.3", + "react-router-dom": "^7.15.1", "styled-components": "^6.1.15" }, "devDependencies": { diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 54c1a95..45e8587 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -7ffc15a19169c7b5683105ae1996cbdb \ No newline at end of file +c9585cde90818cf8406778fed55158bd \ No newline at end of file From 3d6c7e78bdf6a897654c4f072d856854888d8a41 Mon Sep 17 00:00:00 2001 From: cblgh Date: Wed, 17 Jun 2026 10:13:09 +0200 Subject: [PATCH 04/20] per UX decision to go full manual connection, remove QR code logic If needed we can always retrieve the logic by looking back at previous commits and picking out the parts we need :) But until that day it is better to remove all of the logic relating to it to decrease the attack surface of hash verification. --- .../CertificateVerificationModal.tsx | 44 ++++-- .../src/Components/NearbySharing/Connect.tsx | 138 ++++-------------- .../NearbySharing/Hooks/useNearbySharing.tsx | 88 ++++------- .../NearbySharing/NearbySharing.tsx | 11 +- .../Components/NearbySharing/SpinnerModal.tsx | 4 - 5 files changed, 90 insertions(+), 195 deletions(-) diff --git a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx index b9e1683..493f485 100644 --- a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx +++ b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx @@ -5,23 +5,32 @@ import { SpinnerModal } from "../NearbySharing/SpinnerModal"; interface CertificateVerificationModalProps { isOpen: boolean; certificateHash: string; - modalState: 'waiting' | 'confirm'; - onConfirm: () => void; + modalState: 'CONFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER'; + onConfirmReceiverHash: () => void; + onConfirmSenderHash: () => void; onDiscard: () => void; } -// TODO (2026-06-16): revamp modal states to use: -// type ManualConfirmationSteps = 'COMFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER' - +// TODO (2026-06-16): +// confirmReceiverHash -> waiting -> waiting for register request to come in +// confirmSenderHash -> waiting -> send register response (?) <-- current onConfirm +// export function CertificateVerificationModal({ isOpen, certificateHash, modalState, - onConfirm, - onDiscard + onDiscard, + onConfirmReceiverHash, + onConfirmSenderHash }: CertificateVerificationModalProps) { if (!isOpen) return null; + const getStepTitle = () => { + if (modalState === "CONFIRM_RECEIVER") { return "Step 1: Confirm recipient hash" } + if (modalState === "CONFIRM_SENDER") { return "Step 2: Confirm sender hash" } + return "Step X: Confirm Y" + } + /* TODO (2026-06-09): add conditional that sets the background colour -> bc 1. verifying receiver (desktop)'s cert hash has one (lighter?) color 2. verifying sender s cert hash has another (darker?) color @@ -30,10 +39,9 @@ export function CertificateVerificationModal({ * "Confirm and connect" */ - if (modalState === 'waiting') { + if (modalState === 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' || modalState === 'WAITING_FOR_SENDER_CONFIRM_SENDER') { return ( ) @@ -45,8 +53,8 @@ export function CertificateVerificationModal({ Verification - - Step X: Confirm Y hash + + {getStepTitle()} @@ -67,9 +75,17 @@ export function CertificateVerificationModal({ DISCARD AND START OVER - - CONFIRM AND CONTINUE - + { modalState === "CONFIRM_RECEIVER" ? ( + + CONFIRM AND CONTINUE + + ) + : ( + + CONFIRM AND CONNECT + + ) + } diff --git a/frontend/src/Components/NearbySharing/Connect.tsx b/frontend/src/Components/NearbySharing/Connect.tsx index e6e4c7f..151f00e 100644 --- a/frontend/src/Components/NearbySharing/Connect.tsx +++ b/frontend/src/Components/NearbySharing/Connect.tsx @@ -2,114 +2,68 @@ import styled from 'styled-components'; import { useState, useEffect } from 'react'; import { PinDisplay } from "../PinDisplay"; import { GetServerPIN, GetDefaultPort } from '../../../wailsjs/go/app/App'; -import QRCode from 'qrcode'; -import qrIcon from "../../assets/images/icons/qr.svg"; import phoneIcon from "../../assets/images/icons/phone.svg"; interface ConnectStepProps { serverRunning: boolean; localIPs: string[]; certificateHash: string; - isQRMode: boolean; - onModeChange?: (isQRMode: boolean) => void; } -export function ConnectStep({ serverRunning, localIPs, certificateHash, isQRMode, onModeChange }: ConnectStepProps) { - const [qrCodeDataURL, setQrCodeDataURL] = useState(''); +export function ConnectStep({ serverRunning, localIPs, certificateHash}: ConnectStepProps) { const [pin, setPin] = useState(''); const [serverPort, setServerPort] = useState(-1); useEffect(() => { - const generateQR = async () => { - const defaultPort = await GetDefaultPort(); - setServerPort(defaultPort); - - if (serverRunning && localIPs.length > 0 && certificateHash && pin) { - try { - const qrData = { - ip_address: localIPs, - port: defaultPort, - certificate_hash: certificateHash, - pin: pin - }; - - const dataURL = await QRCode.toDataURL(JSON.stringify(qrData)); - setQrCodeDataURL(dataURL); - } catch (error) { - console.error('Failed to generate QR code:', error); - } + const setPort = async () => { + const defaultPort = await GetDefaultPort(); + setServerPort(defaultPort); } - }; - - const fetchPIN = async () => { - if (serverRunning) { - try { - const currentPIN = await GetServerPIN(); - setPin(currentPIN); - } catch (error) { - console.error('Failed to get PIN:', error); - } - } - }; - fetchPIN(); - generateQR(); + const fetchPIN = async () => { + if (serverRunning) { + try { + const currentPIN = await GetServerPIN(); + setPin(currentPIN); + } catch (error) { + console.error('Failed to get PIN:', error); + } + } + }; + setPort() + fetchPIN(); }, [serverRunning, localIPs, certificateHash, pin]); - function toggleQRCode() { - const newMode = !isQRMode; - onModeChange?.(newMode); - } return ( - {isQRMode - ? "Show this QR code for the sender to scan." - : "The sender should input the following information in Tella on their phone." - } + The sender should input the following information in Tella on their phone. - {isQRMode ? ( - - Your QR code - - ): ( Your device information - )} - - {isQRMode ? ( - - {qrCodeDataURL ? ( - - ) : ( -
Generating QR code...
- )} -
- ) : ( - <> + <> - IP addresses - {localIPs.join(', ')} + IP addresses + {localIPs.join(', ')} - PIN - + PIN + - Port - {serverPort} + Port + {serverPort} - - )} +
@@ -189,54 +143,12 @@ const BackToAutoButton = styled.p` padding-top: 1.5rem; `; -const QRCodeButton = styled.button` - background: none; - border: 1px solid #6c757d; - color: #8B8E8F; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - font-size: 0.875rem; - display: flex; - align-items: center; - gap: 0.5rem; - margin: 0 auto 2rem; - font-weight: 700; - - &:hover { - background-color: #f8f9fa; - } -`; - const AutoMoveText = styled.p` font-size: 0.875rem; color: #6c757d; font-style: italic; `; -const QRCodeContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - padding: 1rem; -`; - -const QRCodeImage = styled.img` - max-width: 150px; - width: 100%; - height: auto; -`; - -const QRIcon = styled.div` - width: 1.5rem; - height: 1.5rem; - flex-shrink: 0; - background-image: url(${qrIcon}); - background-size: contain; - background-repeat: no-repeat; - background-position: center; -`; - const IconTitleContainer = styled.div` display: flex; align-items: center; diff --git a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx index 764c87a..6027c39 100644 --- a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx @@ -6,11 +6,11 @@ import { useServer } from "../../../Contexts/ServerContext"; import { log } from "../../../util/util" type FlowStep = 'intro' | 'connect' | 'accept' | 'receive' | 'results'; -type ModalState = 'waiting' | 'confirm'; +// TODO (2026-06-16): waiting for sender confirm sender is not needed, we instead go directly to confirm registration +// and then step is set to "accept" +type ManualConfirmationState = 'CONFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER' -// TODO (2026-06-15): use these state transitions for manual confirmation -// TODO (2026-06-15): remove 'waiting' from hash verification modal -type ManualConfirmationSteps = 'COMFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER' +// TODO (2026-06-16): with state transitions etc, make sure to also handle if "sender confirmed before receiver!" interface FileInfo { id: string; @@ -49,13 +49,8 @@ export function useNearbySharing() { // Certificate verification state const [showVerificationModal, setShowVerificationModal] = useState(false); - const [showWaitingForSenderModal, setShowWaitingForSenderModal] = useState(false); const [certificateHash, setCertificateHash] = useState(''); - const [modalState, setModalState] = useState('waiting'); - - // Connection mode state - const [isUsingQRMode, setIsUsingQRMode] = useState(false); - const isUsingQRModeRef = useRef(false); + const [modalState, setModalState] = useState('CONFIRM_RECEIVER'); // Initialize network info and event listeners useEffect(() => { @@ -73,41 +68,29 @@ export function useNearbySharing() { const cleanupPingListener = EventsOn("ping-received", (data) => { log("Ping received:", data); setShowVerificationModal(true); - setModalState('waiting') + // TODO (2026-06-16): set modal state to CONFIRM_RECEIVER + setModalState('CONFIRM_RECEIVER') }); const cleanupRegisterListener = EventsOn("register-request-received", (data) => { log("Register request received:", data); - log("Current QR mode:", isUsingQRModeRef.current); - - if (isUsingQRModeRef.current) { - log("🔗 QR mode active - auto-confirming registration"); - // Auto-confirm for QR mode - ConfirmRegistration() - .then(() => { - log("✅ QR registration confirmed successfully"); - setCurrentStep('accept'); - }) - .catch((error) => { - console.error("❌ Failed to auto-confirm QR registration:", error); - // Fall back to manual confirmation - setModalState('confirm'); - setShowVerificationModal(true); - }); - } else { - log("📱 Manual mode - showing confirmation modal"); - // Manual mode - show certificate verification modal - setModalState('confirm'); - } + + log("📱 Manual mode - showing confirmation modal"); + // Manual mode - show certificate verification modal + // TODO (2026-06-16): set modal state to CONFIRM_SENDER + setModalState('CONFIRM_SENDER'); }); const cleanupCertListener = EventsOn("certificate-hash", (data) => { - log("Certificate hash received:", data); + log("Receiver Certificate hash received:", data); setCertificateHash(data.toString()); }); // TODO (2026-06-16): "prepare-upload-request" is this the transition that ends the second "waiting for sender" screen? const cleanupPrepareRequest = EventsOn("prepare-upload-request", (data) => { + // TODO (2026-06-16): when to set step to accept? one prepare-upload-request? + setCurrentStep('accept'); + setShowVerificationModal(false); log("📨 Received prepare upload request in parent:", data); const requestData = data as TransferData; setTransferData(requestData); @@ -159,13 +142,16 @@ export function useNearbySharing() { return await stopServer(); }; - // Certificate verification handlers + + const handleReceiverConfirmReceiver = async () => { + setModalState("WAITING_FOR_SENDER_CONFIRM_RECEIVER") + } + // Receiver Certificate verification handlers const handleVerificationConfirm = async () => { - log("✅ Certificate verification CONFIRMED"); + log("✅ Receiver Certificate verification CONFIRMED"); try { await ConfirmRegistration(); - setShowVerificationModal(false); - setCurrentStep('accept'); + setModalState('WAITING_FOR_SENDER_CONFIRM_SENDER'); // TODO (2026-06-16): double check this confirmation state return true; } catch (error) { console.error("Failed to confirm registration:", error); @@ -181,23 +167,24 @@ export function useNearbySharing() { } catch (error) { console.error("Failed to reject registration:", error); } + // if rejected, reset state setShowVerificationModal(false); - setShowWaitingForSenderModal(false); - setModalState('waiting'); + setModalState('CONFIRM_RECEIVER'); await handleStopServer(); setCurrentStep('intro'); }; const handleVerificationDiscard = async () => { - log("❌ Certificate verification DISCARDED"); + log("❌ Receiver Certificate verification DISCARDED"); try { await RejectRegistration(); } catch (error) { console.error("Failed to reject registration:", error); } + // reset state setShowVerificationModal(false); - setModalState('waiting'); + setModalState('CONFIRM_RECEIVER'); await handleStopServer(); setCurrentStep('intro'); }; @@ -273,12 +260,9 @@ export function useNearbySharing() { setCurrentSessionId(''); setTransferData(null); setShowVerificationModal(false); - setShowWaitingForSenderModal(false); setCertificateHash(''); - setModalState('waiting'); + setModalState('CONFIRM_RECEIVER'); setCurrentStep('intro'); - setIsUsingQRMode(false); - isUsingQRModeRef.current = false; }; return { @@ -290,23 +274,13 @@ export function useNearbySharing() { currentSessionId, transferData, showVerificationModal, - showWaitingForSenderModal, certificateHash, modalState, - isUsingQRMode, - - // State setters - - // QR mode handler - handleQRModeChange: (isQR: boolean) => { - setIsUsingQRMode(isQR); - isUsingQRModeRef.current = isQR; - log("QR mode changed to:", isQR); - }, - + // Actions handleBack, handleContinue, + handleReceiverConfirmReceiver, handleVerificationConfirm, handleVerificationDiscard, handleWaitingForSenderCancel, diff --git a/frontend/src/Components/NearbySharing/NearbySharing.tsx b/frontend/src/Components/NearbySharing/NearbySharing.tsx index 996f98b..eedf4af 100644 --- a/frontend/src/Components/NearbySharing/NearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/NearbySharing.tsx @@ -7,6 +7,7 @@ import { ConnectStep } from "./Connect"; import { IntroStep } from "./Intro"; import { ResultsStep } from "./Results"; import { useNearbySharing } from "./Hooks/useNearbySharing" +import { log } from "../../util/util" export function NearbySharing() { const { @@ -16,15 +17,12 @@ export function NearbySharing() { currentSessionId, transferData, showVerificationModal, - showWaitingForSenderModal, certificateHash, modalState, - isUsingQRMode, - - handleQRModeChange, handleContinue, handleVerificationConfirm, + handleReceiverConfirmReceiver, handleVerificationDiscard, handleWaitingForSenderCancel, handleFileRequestAccept, @@ -57,8 +55,6 @@ export function NearbySharing() { serverRunning={serverRunning} localIPs={localIPs} certificateHash={certificateHash} - isQRMode={isUsingQRMode} - onModeChange={handleQRModeChange} /> )} @@ -96,7 +92,8 @@ export function NearbySharing() { isOpen={showVerificationModal} certificateHash={certificateHash} modalState={modalState} - onConfirm={handleVerificationConfirm} + onConfirmSenderHash={handleVerificationConfirm} + onConfirmReceiverHash={handleReceiverConfirmReceiver} onDiscard={handleVerificationDiscard} /> diff --git a/frontend/src/Components/NearbySharing/SpinnerModal.tsx b/frontend/src/Components/NearbySharing/SpinnerModal.tsx index 493d957..be1bc4a 100644 --- a/frontend/src/Components/NearbySharing/SpinnerModal.tsx +++ b/frontend/src/Components/NearbySharing/SpinnerModal.tsx @@ -1,16 +1,12 @@ import styled, { keyframes } from 'styled-components'; interface SpinnerModalProps { - isOpen: boolean; onCancel: () => void; } export function SpinnerModal({ - isOpen, onCancel }: SpinnerModalProps) { - if (!isOpen) return null; - return ( From 0570dc6e35cbb8a403e254caa1a2693f4f7ee2e9 Mon Sep 17 00:00:00 2001 From: cblgh Date: Wed, 17 Jun 2026 11:37:18 +0200 Subject: [PATCH 05/20] improve authorisation handling around register & sender hash verif. --- backend/core/modules/registration/handler.go | 19 ++++++++++++++----- backend/core/modules/registration/ports.go | 1 + backend/core/modules/registration/service.go | 16 +++++++++++++--- backend/core/modules/server/service.go | 6 +++--- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index a503e95..7eb7bec 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -58,8 +58,8 @@ func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -// rememberClientFingerprint changes tls config of package server. this change also restarts the https server instance. -func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, rememberClientFingerprint func (string) error) { +// rememberSenderFingerprint changes tls config of package server. this change also restarts the https server instance. +func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, rememberSenderFingerprint func (string) error) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -92,7 +92,16 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe return } - // Store the pending registration + if authorised, err := h.service.IsAuthorised(request.PIN, request.Nonce); !authorised { + if errors.Is(err, ErrPinInvalid) { + http.Error(w, "Invalid PIN", http.StatusUnauthorized) + } + if errors.Is(err, ErrTooManyAttempts) { + http.Error(w, "Too many requests", http.StatusTooManyRequests) + } + return + } + h.mu.Lock() h.pendingRegistration = &PendingRegistration{ PIN: request.PIN, @@ -119,9 +128,9 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe h.pendingRegistration = nil // if we're sending a successful response it was because the pin was valid! // since PIN was valid, we can now save the sender's certificate hash and restart the server - // note: since `rememberClientFingerprint` requires a restart of the https server, we likely have to send the + // note: since `rememberSenderFingerprint` requires a restart of the https server, we likely have to send the // response before restarting? - err = rememberClientFingerprint(request.SenderCertificateHash) + err = rememberSenderFingerprint(request.SenderCertificateHash) // TODO cblgh(2026-03-15): figure out something less catastrophic for the app than just panic here. but https // handler is a hard place to recover from the death of the https server ^^' // diff --git a/backend/core/modules/registration/ports.go b/backend/core/modules/registration/ports.go index fd13156..652a21a 100644 --- a/backend/core/modules/registration/ports.go +++ b/backend/core/modules/registration/ports.go @@ -1,6 +1,7 @@ package registration type Service interface { + IsAuthorised(pin, nonce string) (bool, error) CreateSession(pin string, nonce string) (string, error) SetPINCode(pinCode string) ForgetSession(sessionID string) diff --git a/backend/core/modules/registration/service.go b/backend/core/modules/registration/service.go index 6e63092..a6923ee 100644 --- a/backend/core/modules/registration/service.go +++ b/backend/core/modules/registration/service.go @@ -32,16 +32,26 @@ func NewService(ctx context.Context) Service { } } -func (s *service) CreateSession(pin, nonce string) (string, error) { +var ErrTooManyAttempts = errors.New("Too many invalid attempts") +var ErrPinInvalid = errors.New("Invalid PIN") + +func (s *service) IsAuthorised(pin, nonce string) (bool, error) { // TODO cblgh(2026-02-17): guard ratelimiter with mutex alt. use sync.Map to prevent crash from malicious behaviour? // TODO cblgh(2026-03-11) change this rate limiter to not be on the nonce any more? if s.rateLimiter[nonce] >= 3 { // check this with the team - return "", errors.New("too many invalid attempts") + return false, ErrTooManyAttempts } if pin != s.pinCode { s.rateLimiter[nonce]++ - return "", errors.New("Invalid pin") + return false, ErrPinInvalid + } + return true, nil +} + +func (s *service) CreateSession(pin, nonce string) (string, error) { + if authorised, err := s.IsAuthorised(pin, nonce); !authorised { + return "", err } sessionID := uuid.New().String() diff --git a/backend/core/modules/server/service.go b/backend/core/modules/server/service.go index efe014b..df69bda 100644 --- a/backend/core/modules/server/service.go +++ b/backend/core/modules/server/service.go @@ -224,12 +224,12 @@ func (s *service) startServer() error { } // PinFingerprint pins the SHA256 hash of the PEM-encoded cert. The TLS config is changed to require a client cert, which necessitates restarting the https server instance. -func (s *service) PinFingerprint(fingerprint string) error { +func (s *service) PinFingerprint(senderFingerprint string) error { log("Pin fingerprint called") - if len(fingerprint) != 64 { + if len(senderFingerprint) != 64 { return errors.New("expected fingerprint string length of 64ch") } - s.fingerprint = fingerprint + s.fingerprint = senderFingerprint // terminate the previous instance shutdownCtx, cancel := context.WithTimeout(s.ctx, 1500*time.Millisecond) defer cancel() From 4f92f7aba6b1dbccd5eed1c6dc8d8f2ba33f1e60 Mon Sep 17 00:00:00 2001 From: cblgh Date: Wed, 17 Jun 2026 14:44:39 +0200 Subject: [PATCH 06/20] extract sender hash from requests and thread to frontend note: in this commit the behaviour is almost exceedingly strict - once a request has come in which has a certificate, we this certificate's fingerprint as the fingerprint candidate and ignore + disallow any other requests from being considered to bring a certificate. benefit: it limits some attacks by swooping in and sending a malicious register request right when the other party press confirm & connect drawback: it opens up an attack where an active malicious attacker on the network could prohibit any other legitimate connections from being established by virtue of spamming the ping route with their own certificate (which ofc will not present a match when sender + reciever verify the sender fingerprint candidate). -- also in this commit: fixed a couple glaring interface issues that are completely unrelated --- backend/core/modules/registration/handler.go | 12 ++--- backend/core/modules/server/handler.go | 4 +- backend/core/modules/server/service.go | 43 +++++++++++++++--- backend/utils/tls/certificate.go | 2 +- .../CertificateVerificationModal.tsx | 45 ++++++++++++------- .../NearbySharing/Hooks/useNearbySharing.tsx | 45 ++++++++++--------- .../NearbySharing/NearbySharing.tsx | 22 ++++----- .../NearbySharing/StepIndicator.tsx | 3 +- 8 files changed, 112 insertions(+), 64 deletions(-) diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index 7eb7bec..ded90c6 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -59,7 +59,7 @@ func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { } // rememberSenderFingerprint changes tls config of package server. this change also restarts the https server instance. -func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, rememberSenderFingerprint func (string) error) { +func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, rememberSenderFingerprint func (string) error, getSenderFingerprintCandidate func() string) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -76,9 +76,6 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe var request struct { PIN string `json:"pin"` Nonce string `json:"nonce"` - // TODO (2026-06-09): remove cert hash from request payload, and instead extract it from the request's connection information? - // OTHERWISE: document inclusion of senderCertHash in protocol once more - SenderCertificateHash string `json:"senderCertificateHash"` } if err := json.Unmarshal(requestBody, &request); err != nil { @@ -87,7 +84,9 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe return } - if request.PIN == "" || request.Nonce == "" || len(request.SenderCertificateHash) != 64 { + // it's only a claimed sender until verification has been mutually confirmed + certificateHashClaimedSender := getSenderFingerprintCandidate() + if request.PIN == "" || request.Nonce == "" || len(certificateHashClaimedSender) != 64 { http.Error(w, "Missing required parameters", http.StatusBadRequest) return } @@ -116,6 +115,7 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe "timestamp": time.Now().Unix(), "message": "Sender is requesting to register", "state": "confirm", + "senderCertificateHash": certificateHashClaimedSender, }) // Wait for user confirmation or timeout @@ -130,7 +130,7 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe // since PIN was valid, we can now save the sender's certificate hash and restart the server // note: since `rememberSenderFingerprint` requires a restart of the https server, we likely have to send the // response before restarting? - err = rememberSenderFingerprint(request.SenderCertificateHash) + err = rememberSenderFingerprint(certificateHashClaimedSender) // TODO cblgh(2026-03-15): figure out something less catastrophic for the app than just panic here. but https // handler is a hard place to recover from the death of the https server ^^' // diff --git a/backend/core/modules/server/handler.go b/backend/core/modules/server/handler.go index 1366fcb..e966f96 100644 --- a/backend/core/modules/server/handler.go +++ b/backend/core/modules/server/handler.go @@ -26,10 +26,10 @@ func NewHandler( // TODO (2026-06-15): implement /api/v2/* // TODO (2026-06-15): keep /api/v1/ping /api/v1/register around and serve legacy responses to sent queries there -func (h *Handler) SetupRoutes(pinFingerprint func (string) error) { +func (h *Handler) SetupRoutes(pinFingerprint func (string) error, getSenderFingerprintCandidate func () string) { h.mux.HandleFunc("/api/v1/ping", h.registrationHandler.HandlePing) h.mux.HandleFunc("/api/v1/register", func(w http.ResponseWriter, r *http.Request) { - h.registrationHandler.HandleRegister(w, r, pinFingerprint) + h.registrationHandler.HandleRegister(w, r, pinFingerprint, getSenderFingerprintCandidate) }) h.mux.HandleFunc("/api/v1/prepare-upload", h.transferHandler.HandlePrepare) h.mux.HandleFunc("/api/v1/upload", h.transferHandler.HandleUpload) diff --git a/backend/core/modules/server/service.go b/backend/core/modules/server/service.go index df69bda..1db8171 100644 --- a/backend/core/modules/server/service.go +++ b/backend/core/modules/server/service.go @@ -30,7 +30,10 @@ import ( var log = devlog.Logger("server") type service struct { - fingerprint string // pinned fingerprint for sender + pinnedSenderCertificateHash string // pinned fingerprint for sender + // fingerprintCandidate is derived from incoming requests when + // tlsConfig.ClientAuth is set to RequestClientCert pre-mTLS establishment (post mTLS config is set to RequireAnyClientCert) + fingerprintCandidate string limitingMiddleware http.Handler nonceManager *nonces.NonceManager limiter *RateLimitingWare @@ -149,20 +152,42 @@ func (s *service) Start(port int) error { } // do not require any client certs when server is freshly started - tlsConfig.ClientAuth = ctls.NoClientCert + tlsConfig.ClientAuth = ctls.RequestClientCert // NOTE cblgh(2026-03-15): set up a custom cert pool using the pinned cert? // to allow use of tls.Config.ClientAuth: tls.RequireAndVerifyClientCert // c.f https://stackoverflow.com/a/63317898 tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + // currently pre-mtls, and not strictly requiring cert + if (tlsConfig.ClientAuth == ctls.RequestClientCert && len(rawCerts) == 0) { + return nil + } + // we're post-mtls, this should not occur for legitimate requests + if (tlsConfig.ClientAuth == ctls.RequireAnyClientCert && len(rawCerts) == 0) { + return errors.New("Sender certificate missing") + } + log("VerifyPeerCertificate called") encodedCert, err := tls.EncodeCertAsPEM(rawCerts[0]) if err != nil { return err } calculatedPEMHash := sha256.Sum256(encodedCert) + hexPEMHash := fmt.Sprintf("%x", calculatedPEMHash) + // still pre-mtls but we have now have a candidate to use for senderFingerprint. + // to limit attack surface, we only every allow setting one fingerprint candidate + if (tlsConfig.ClientAuth == ctls.RequestClientCert && s.fingerprintCandidate == "") { + // NOTE (2026-06-17): this strictness could cause an attacker to basically spam requests and deny any actual + // requests on adversarial networks from going through; maybe reconsider + + // NOTE (2026-06-17): prevent multiple register POST (with different senderCertificateHash values) from being sent in succession? + + s.fingerprintCandidate = hexPEMHash + return nil + } + log("incoming cert hash\n%x\n", calculatedPEMHash) - if fmt.Sprintf("%x", calculatedPEMHash) != s.fingerprint { - return errors.New("pin did not match") + if hexPEMHash != s.pinnedSenderCertificateHash { + return errors.New("Hash of incoming request certificate did not pinned sender certificate hash") } return nil } @@ -176,7 +201,7 @@ func (s *service) Start(port int) error { // TODO (2026-02-19): dhekra / iOS closes the server when the transfer is explicitly stopped handler := NewHandler(mux, s.registrationHandler, transferHandler) - handler.SetupRoutes(s.PinFingerprint) + handler.SetupRoutes(s.PinFingerprint, s.GetSenderFingerprintCandidate) s.limitingMiddleware = s.limiter.Handler(mux) s.port = port @@ -222,6 +247,9 @@ func (s *service) startServer() error { s.running = true return nil } +func (s *service) GetSenderFingerprintCandidate() string { + return s.fingerprintCandidate +} // PinFingerprint pins the SHA256 hash of the PEM-encoded cert. The TLS config is changed to require a client cert, which necessitates restarting the https server instance. func (s *service) PinFingerprint(senderFingerprint string) error { @@ -229,7 +257,7 @@ func (s *service) PinFingerprint(senderFingerprint string) error { if len(senderFingerprint) != 64 { return errors.New("expected fingerprint string length of 64ch") } - s.fingerprint = senderFingerprint + s.pinnedSenderCertificateHash = senderFingerprint // terminate the previous instance shutdownCtx, cancel := context.WithTimeout(s.ctx, 1500*time.Millisecond) defer cancel() @@ -267,7 +295,8 @@ func (s *service) Stop(ctx context.Context) error { s.server = nil s.tlsConfig = nil s.limitingMiddleware = nil - s.fingerprint = "" + s.pinnedSenderCertificateHash = "" + s.fingerprintCandidate = "" log("HTTPS Server stopped\n") diff --git a/backend/utils/tls/certificate.go b/backend/utils/tls/certificate.go index 92dcb28..f8f465c 100644 --- a/backend/utils/tls/certificate.go +++ b/backend/utils/tls/certificate.go @@ -51,7 +51,7 @@ func GenerateTLSConfig(ctx context.Context, config Config) (*tls.Config, error) hash := sha256.Sum256(cert.Certificate[0]) hashStr := hex.EncodeToString(hash[:]) log("Receiver certificate hash: %s", hashStr) - runtime.EventsEmit(ctx, "certificate-hash", hashStr) + runtime.EventsEmit(ctx, "receiver-certificate-hash", hashStr) tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, diff --git a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx index 493f485..4d038d5 100644 --- a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx +++ b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx @@ -2,9 +2,15 @@ import styled from 'styled-components'; import { formatHash } from "../../util/util" import { SpinnerModal } from "../NearbySharing/SpinnerModal"; +// TODO (2026-06-17): implement waitingState below +// confirm receiver | confirm sender +// waitingState = (confirm_receiver && !SenderConfirmedReceiver) +// interface CertificateVerificationModalProps { isOpen: boolean; - certificateHash: string; + receiverCertificateHash: string; + senderCertificateHash: string; + senderConfirmedReceiver: boolean; modalState: 'CONFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER'; onConfirmReceiverHash: () => void; onConfirmSenderHash: () => void; @@ -17,7 +23,9 @@ interface CertificateVerificationModalProps { // export function CertificateVerificationModal({ isOpen, - certificateHash, + receiverCertificateHash, + senderCertificateHash, + senderConfirmedReceiver, modalState, onDiscard, onConfirmReceiverHash, @@ -25,21 +33,26 @@ export function CertificateVerificationModal({ }: CertificateVerificationModalProps) { if (!isOpen) return null; + const getHashForVerification = () => { + if (modalState === "CONFIRM_RECEIVER") { return receiverCertificateHash } + if (modalState === "CONFIRM_SENDER") { return senderCertificateHash } + return "" + } + + const isWaitingForSender = ( + modalState === "CONFIRM_SENDER" && !senderConfirmedReceiver + ) + const getStepTitle = () => { if (modalState === "CONFIRM_RECEIVER") { return "Step 1: Confirm recipient hash" } if (modalState === "CONFIRM_SENDER") { return "Step 2: Confirm sender hash" } return "Step X: Confirm Y" } - /* TODO (2026-06-09): add conditional that sets the background colour -> bc - 1. verifying receiver (desktop)'s cert hash has one (lighter?) color - 2. verifying sender s cert hash has another (darker?) color - 3. Positive action as different text depending on stage: - * "Confirm and continue" - * "Confirm and connect" - */ + // TODO (2026-06-17): no timeout or error graphic is triggered currently + // TODO (2026-06-17): implement modal for 'Connection failed {context dependent text}' - if (modalState === 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' || modalState === 'WAITING_FOR_SENDER_CONFIRM_SENDER') { + if (isWaitingForSender) { return ( - +
-        {formatHash(certificateHash)}
+        {formatHash(getHashForVerification())}
         
@@ -154,8 +167,9 @@ const Button = styled.button` `; -const HashContainer = styled.div` - background-color: #f8f9fa; +// TODO (2026-06-17): dark bg for sender hash verif +const HashContainer = styled.div<{ $isSender?: boolean }>` + background-color: ${props => props.$isSender ? "#071013CC" : "#D9D9D9"}; border: 1px solid #e9ecef; border-radius: 8px; padding: 0rem 1.5rem; @@ -163,10 +177,11 @@ const HashContainer = styled.div` justify-content: center; `; -const HashText = styled.code` +const HashText = styled.div<{ $isSender?: boolean }>` font-family: 'Courier New', monospace; font-size: 1rem; color: #212529; + color: ${props => props.$isSender ? "white" : "black"}; word-break: break-all; line-height: 1.6; `; diff --git a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx index 6027c39..c239b42 100644 --- a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx @@ -6,9 +6,7 @@ import { useServer } from "../../../Contexts/ServerContext"; import { log } from "../../../util/util" type FlowStep = 'intro' | 'connect' | 'accept' | 'receive' | 'results'; -// TODO (2026-06-16): waiting for sender confirm sender is not needed, we instead go directly to confirm registration -// and then step is set to "accept" -type ManualConfirmationState = 'CONFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER' +type ManualConfirmationState = 'CONFIRM_RECEIVER' | 'CONFIRM_SENDER' // TODO (2026-06-16): with state transitions etc, make sure to also handle if "sender confirmed before receiver!" @@ -49,8 +47,10 @@ export function useNearbySharing() { // Certificate verification state const [showVerificationModal, setShowVerificationModal] = useState(false); - const [certificateHash, setCertificateHash] = useState(''); + const [receiverCertificateHash, setReceiverCertificateHash] = useState(''); + const [senderCertificateHash, setSenderCertificateHash] = useState(''); const [modalState, setModalState] = useState('CONFIRM_RECEIVER'); + const [senderConfirmedReceiver, setSenderConfirmedReceiver] = useState(false) // Initialize network info and event listeners useEffect(() => { @@ -68,29 +68,24 @@ export function useNearbySharing() { const cleanupPingListener = EventsOn("ping-received", (data) => { log("Ping received:", data); setShowVerificationModal(true); - // TODO (2026-06-16): set modal state to CONFIRM_RECEIVER setModalState('CONFIRM_RECEIVER') }); + // TODO (2026-06-17): + // * handle early confirm by sender in way that doesn't fuck up effects + // * pass senderCertificateHash from golang in the Emit const cleanupRegisterListener = EventsOn("register-request-received", (data) => { log("Register request received:", data); - - log("📱 Manual mode - showing confirmation modal"); - // Manual mode - show certificate verification modal - // TODO (2026-06-16): set modal state to CONFIRM_SENDER - setModalState('CONFIRM_SENDER'); + setSenderCertificateHash(data.senderCertificateHash); + setSenderConfirmedReceiver(true); }); - const cleanupCertListener = EventsOn("certificate-hash", (data) => { + const cleanupCertListener = EventsOn("receiver-certificate-hash", (data) => { log("Receiver Certificate hash received:", data); - setCertificateHash(data.toString()); + setReceiverCertificateHash(data.toString()); }); - // TODO (2026-06-16): "prepare-upload-request" is this the transition that ends the second "waiting for sender" screen? const cleanupPrepareRequest = EventsOn("prepare-upload-request", (data) => { - // TODO (2026-06-16): when to set step to accept? one prepare-upload-request? - setCurrentStep('accept'); - setShowVerificationModal(false); log("📨 Received prepare upload request in parent:", data); const requestData = data as TransferData; setTransferData(requestData); @@ -142,16 +137,16 @@ export function useNearbySharing() { return await stopServer(); }; - const handleReceiverConfirmReceiver = async () => { - setModalState("WAITING_FOR_SENDER_CONFIRM_RECEIVER") + setModalState("CONFIRM_SENDER") } // Receiver Certificate verification handlers const handleVerificationConfirm = async () => { log("✅ Receiver Certificate verification CONFIRMED"); try { await ConfirmRegistration(); - setModalState('WAITING_FOR_SENDER_CONFIRM_SENDER'); // TODO (2026-06-16): double check this confirmation state + setShowVerificationModal(false); + setCurrentStep('accept'); return true; } catch (error) { console.error("Failed to confirm registration:", error); @@ -170,6 +165,8 @@ export function useNearbySharing() { // if rejected, reset state setShowVerificationModal(false); setModalState('CONFIRM_RECEIVER'); + setSenderConfirmedReceiver(false); + await handleStopServer(); setCurrentStep('intro'); }; @@ -185,6 +182,8 @@ export function useNearbySharing() { // reset state setShowVerificationModal(false); setModalState('CONFIRM_RECEIVER'); + setSenderConfirmedReceiver(false); + await handleStopServer(); setCurrentStep('intro'); }; @@ -260,9 +259,11 @@ export function useNearbySharing() { setCurrentSessionId(''); setTransferData(null); setShowVerificationModal(false); - setCertificateHash(''); + setReceiverCertificateHash(''); + setSenderCertificateHash(''); setModalState('CONFIRM_RECEIVER'); setCurrentStep('intro'); + setSenderConfirmedReceiver(false); }; return { @@ -274,7 +275,9 @@ export function useNearbySharing() { currentSessionId, transferData, showVerificationModal, - certificateHash, + receiverCertificateHash, + senderCertificateHash, + senderConfirmedReceiver, modalState, // Actions diff --git a/frontend/src/Components/NearbySharing/NearbySharing.tsx b/frontend/src/Components/NearbySharing/NearbySharing.tsx index eedf4af..7ae80d0 100644 --- a/frontend/src/Components/NearbySharing/NearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/NearbySharing.tsx @@ -17,7 +17,9 @@ export function NearbySharing() { currentSessionId, transferData, showVerificationModal, - certificateHash, + receiverCertificateHash, + senderCertificateHash, + senderConfirmedReceiver, modalState, handleContinue, @@ -54,7 +56,7 @@ export function NearbySharing() { )} @@ -90,7 +92,9 @@ export function NearbySharing() { {FLOW_STEPS.map((step, index) => { @@ -129,4 +130,4 @@ const StepConnector = styled.div` height: 1px; background-color: #CFCFCF; z-index: -1; -`; \ No newline at end of file +`; From 247d5edf41b49c5b3dae33d030a284c1ca32f24a Mon Sep 17 00:00:00 2001 From: cblgh Date: Thu, 18 Jun 2026 10:54:19 +0200 Subject: [PATCH 07/20] mtls: on sender cert. hash displayed, prevent new hashes from being chosen In order to minimize the attack surface of sender certificate hashes being either maliciously denied (attacker spamming known routes with their own certificate that would not match sender) or maliciously timed (timing a request with stolen credentials to make the receiver pin the wrong certificate) we in this commit implement a locking scheme. It works as follows: In each request that comes in pre-mTLS being established, and if the request has a certificate attached, then its fingerprint is calculated and saved as a potential candidate for a sender certificate hash. This candidate can be chosen many times. However, on successfully processing an authorised register request (PIN valid, nonce unseen), the candidate for sender certificate hash is locked and can no longer be changed to a different hash. This happens in such a way that the logic for getting the sender certificate hash to display in the desktop interface initiates the lock on the most recent fingerprint candidate. This candidate can now not be changed by future requests (unless connection is discarded and HTTPS server restarted). Since fingerprint candidate locking only happens after PIN and nonce have been checked to be valid, attacks to deny authorised request are prevented. Preventing the fingerprint candidate from being changed after a sender fingerprint has been displayed stops attacks that try to time sending a malicious request with stolen credentials. (Note for reviewers: As with all other commits by my user to date, this change and commit message has been fully authored by a human; no LLMs used.) --- backend/core/modules/registration/handler.go | 29 +++++++++--- backend/core/modules/server/service.go | 47 +++++++++++++++----- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index ded90c6..fa90897 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -58,8 +58,18 @@ func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -// rememberSenderFingerprint changes tls config of package server. this change also restarts the https server instance. -func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, rememberSenderFingerprint func (string) error, getSenderFingerprintCandidate func() string) { +// rememberSenderFingerprint pins the sender certificate hash. calling changes tls config of package server, making it stricter in terms +// of requiring tls client certs. the tls config change requires restarting the https server instance. +// +// lockAndGetSenderFingerprintCandidate locks the current candidate for sender fingerprint, +// preventing it from being changed without discarding the session and starting over. +// this locking behaviour is intended to limit the attack surface and prevent mismatches in what is presented to a user +// and what may be pinned. +// +// we lock after authorisation to limit attacks that are intended to sabotage the connection +// establishment by prematurely forcing the fingerprint to be locked and thus would prevent an authentic sender from +// having their fingerprint from being chosen. +func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, rememberSenderFingerprint func (string) error, lockAndGetSenderFingerprintCandidate func() string) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -84,9 +94,7 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe return } - // it's only a claimed sender until verification has been mutually confirmed - certificateHashClaimedSender := getSenderFingerprintCandidate() - if request.PIN == "" || request.Nonce == "" || len(certificateHashClaimedSender) != 64 { + if request.PIN == "" || request.Nonce == "" { http.Error(w, "Missing required parameters", http.StatusBadRequest) return } @@ -101,6 +109,17 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe return } + // do this after authorisation (PIN / nonce check) because getSenderFingerprintCandidate locks the current candidate, + // preventing it from being changed without discarding the session and starting over. this locking behaviour is to limit the attack + // surface of what is presented to a user and what may be pinned + // + // it's only a claimed sender until verification has been mutually confirmed + certificateHashClaimedSender := lockAndGetSenderFingerprintCandidate() + if len(certificateHashClaimedSender) != 64 { + http.Error(w, "Missing required parameters", http.StatusBadRequest) + return + } + h.mu.Lock() h.pendingRegistration = &PendingRegistration{ PIN: request.PIN, diff --git a/backend/core/modules/server/service.go b/backend/core/modules/server/service.go index 1db8171..4314e0b 100644 --- a/backend/core/modules/server/service.go +++ b/backend/core/modules/server/service.go @@ -34,6 +34,7 @@ type service struct { // fingerprintCandidate is derived from incoming requests when // tlsConfig.ClientAuth is set to RequestClientCert pre-mTLS establishment (post mTLS config is set to RequireAnyClientCert) fingerprintCandidate string + fingerprintCandidateLockedIn bool limitingMiddleware http.Handler nonceManager *nonces.NonceManager limiter *RateLimitingWare @@ -157,7 +158,7 @@ func (s *service) Start(port int) error { // to allow use of tls.Config.ClientAuth: tls.RequireAndVerifyClientCert // c.f https://stackoverflow.com/a/63317898 tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - // currently pre-mtls, and not strictly requiring cert + // currently pre-mtls and not strictly requiring cert if (tlsConfig.ClientAuth == ctls.RequestClientCert && len(rawCerts) == 0) { return nil } @@ -173,18 +174,32 @@ func (s *service) Start(port int) error { } calculatedPEMHash := sha256.Sum256(encodedCert) hexPEMHash := fmt.Sprintf("%x", calculatedPEMHash) - // still pre-mtls but we have now have a candidate to use for senderFingerprint. - // to limit attack surface, we only every allow setting one fingerprint candidate - if (tlsConfig.ClientAuth == ctls.RequestClientCert && s.fingerprintCandidate == "") { - // NOTE (2026-06-17): this strictness could cause an attacker to basically spam requests and deny any actual - // requests on adversarial networks from going through; maybe reconsider - // NOTE (2026-06-17): prevent multiple register POST (with different senderCertificateHash values) from being sent in succession? - - s.fingerprintCandidate = hexPEMHash + // in this section we use a mutex because we want to be sure to never set s.fingerprintCandidate to something else while it is being + // fetched by s.GetSenderFingerprintCandidate for being presented to the user + s.mu.Lock() + if (tlsConfig.ClientAuth == ctls.RequestClientCert) { + // prevent multiple register POST (with different senderCertificateHash values) from being sent in succession once + // hash is displayed + // NOTE: this is not our session-long pinning - we haven't definitively set s.pinnedFingerprintCandidate! + // we only set s.pinnedSenderCertificateHash on successful sender certificate hash + // verification i.e. calling s.PinFingerprint() from registration/handler.go + if !s.fingerprintCandidateLockedIn { + // still pre-mtls but we have now have a candidate to use for senderFingerprint. + s.fingerprintCandidate = hexPEMHash + log("sender fingerprint candidate %s", s.fingerprintCandidate) + } else { + if hexPEMHash != s.fingerprintCandidate { + s.mu.Unlock() + return errors.New("Hash of incoming request certificate did not match candidate for sender certificate hash") + } + } + s.mu.Unlock() return nil } + s.mu.Unlock() + // we should only reach this point in the routine once we have established mTLS and have a pinned sender certificate hash log("incoming cert hash\n%x\n", calculatedPEMHash) if hexPEMHash != s.pinnedSenderCertificateHash { return errors.New("Hash of incoming request certificate did not pinned sender certificate hash") @@ -248,7 +263,18 @@ func (s *service) startServer() error { return nil } func (s *service) GetSenderFingerprintCandidate() string { - return s.fingerprintCandidate + var candidate string + // use mutex because we want to be sure to never set s.fingerprintCandidate to something else while it is being + // fetched for being presented to the user. + // + // to limit attack surface, we only every allow setting one fingerprint + // candidate once the sender fingerprint candidate has been displayed by the user + s.mu.Lock() + candidate = s.fingerprintCandidate + log("sender fingerprint candidate locked to %s", candidate) + s.fingerprintCandidateLockedIn = true + s.mu.Unlock() + return candidate } // PinFingerprint pins the SHA256 hash of the PEM-encoded cert. The TLS config is changed to require a client cert, which necessitates restarting the https server instance. @@ -297,6 +323,7 @@ func (s *service) Stop(ctx context.Context) error { s.limitingMiddleware = nil s.pinnedSenderCertificateHash = "" s.fingerprintCandidate = "" + s.fingerprintCandidateLockedIn = false log("HTTPS Server stopped\n") From 48fc340174ca2f7e6315736ed8f9c938375c9f99 Mon Sep 17 00:00:00 2001 From: cblgh Date: Thu, 18 Jun 2026 11:30:15 +0200 Subject: [PATCH 08/20] routes: transition to /api/v2 and handle {ping,register}@v1 --- backend/core/modules/server/handler.go | 23 +++++++++++++------ .../CertificateVerificationModal.tsx | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/backend/core/modules/server/handler.go b/backend/core/modules/server/handler.go index e966f96..48a2897 100644 --- a/backend/core/modules/server/handler.go +++ b/backend/core/modules/server/handler.go @@ -24,16 +24,25 @@ func NewHandler( } } -// TODO (2026-06-15): implement /api/v2/* // TODO (2026-06-15): keep /api/v1/ping /api/v1/register around and serve legacy responses to sent queries there func (h *Handler) SetupRoutes(pinFingerprint func (string) error, getSenderFingerprintCandidate func () string) { - h.mux.HandleFunc("/api/v1/ping", h.registrationHandler.HandlePing) - h.mux.HandleFunc("/api/v1/register", func(w http.ResponseWriter, r *http.Request) { - h.registrationHandler.HandleRegister(w, r, pinFingerprint, getSenderFingerprintCandidate) + h.mux.HandleFunc("/api/v2/ping", h.registrationHandler.HandlePing) + h.mux.HandleFunc("/api/v2/register", func(res http.ResponseWriter, req *http.Request) { + h.registrationHandler.HandleRegister(res, req, pinFingerprint, getSenderFingerprintCandidate) }) - h.mux.HandleFunc("/api/v1/prepare-upload", h.transferHandler.HandlePrepare) - h.mux.HandleFunc("/api/v1/upload", h.transferHandler.HandleUpload) - h.mux.HandleFunc("/api/v1/close-connection", h.transferHandler.HandleCloseConnection) + h.mux.HandleFunc("/api/v2/prepare-upload", h.transferHandler.HandlePrepare) + h.mux.HandleFunc("/api/v2/upload", h.transferHandler.HandleUpload) + h.mux.HandleFunc("/api/v2/close-connection", h.transferHandler.HandleCloseConnection) + + // handle v1 legacy routes + h.mux.HandleFunc("/api/v1/ping", func (res http.ResponseWriter, req *http.Request) { + // TODO (2026-06-18): collectively decide what to respond to old v1-pings. Will 400 crash clients or is it OK? + http.Error(res, "Bad request", http.StatusBadRequest) + }) + h.mux.HandleFunc("/api/v1/register", func(res http.ResponseWriter, req *http.Request) { + http.Error(res, "Rejected", http.StatusForbidden) + }) + h.mux.HandleFunc("/", func (res http.ResponseWriter, req *http.Request) { http.Error(res, "Not found", http.StatusNotFound) }) diff --git a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx index 4d038d5..065f30f 100644 --- a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx +++ b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx @@ -51,7 +51,7 @@ export function CertificateVerificationModal({ // TODO (2026-06-17): no timeout or error graphic is triggered currently // TODO (2026-06-17): implement modal for 'Connection failed {context dependent text}' - + // TODO (2026-06-18): do not display step 1 after step 2 is already being displayed if (isWaitingForSender) { return ( Date: Thu, 18 Jun 2026 12:07:59 +0200 Subject: [PATCH 09/20] frontend: implement error dialog modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently this is mostly unused, but now it exists and can be attached to 'nearby-sharing-error' events emitted from the backend. However, it is currently invoked inside of the CertificateVerificationModal and might be better extracted elsewhere / live alongside CertificateVefificationModal. For example these connect-centric modals conditionally à la: if !err then verifModal else errModal --- .../CertificateVerificationModal.tsx | 23 +++- .../Components/NearbySharing/ErrorDialog.tsx | 103 ++++++++++++++++++ .../NearbySharing/Hooks/useNearbySharing.tsx | 20 ++++ .../NearbySharing/NearbySharing.tsx | 4 +- 4 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 frontend/src/Components/NearbySharing/ErrorDialog.tsx diff --git a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx index 065f30f..29b6a23 100644 --- a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx +++ b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx @@ -1,16 +1,24 @@ import styled from 'styled-components'; import { formatHash } from "../../util/util" import { SpinnerModal } from "../NearbySharing/SpinnerModal"; +import { ErrorDialog } from "../NearbySharing/ErrorDialog"; // TODO (2026-06-17): implement waitingState below // confirm receiver | confirm sender // waitingState = (confirm_receiver && !SenderConfirmedReceiver) -// + +interface NearbySharingError { + text: string; + button: string; + hasError: boolean; +} + interface CertificateVerificationModalProps { isOpen: boolean; receiverCertificateHash: string; senderCertificateHash: string; senderConfirmedReceiver: boolean; + nearbySharingError: NearbySharingError; modalState: 'CONFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER'; onConfirmReceiverHash: () => void; onConfirmSenderHash: () => void; @@ -26,6 +34,7 @@ export function CertificateVerificationModal({ receiverCertificateHash, senderCertificateHash, senderConfirmedReceiver, + nearbySharingError, modalState, onDiscard, onConfirmReceiverHash, @@ -50,8 +59,16 @@ export function CertificateVerificationModal({ } // TODO (2026-06-17): no timeout or error graphic is triggered currently - // TODO (2026-06-17): implement modal for 'Connection failed {context dependent text}' - // TODO (2026-06-18): do not display step 1 after step 2 is already being displayed + // TODO (2026-06-18): do not display step 1 after waiting has been started (or step 2 is already being displayed) + if (nearbySharingError.hasError) { + return ( + + ) + } if (isWaitingForSender) { return ( void; + text: string; + buttonLabel: string; +} + +export function ErrorDialog({ + onClose, text, buttonLabel +}: ErrorDialogProps) { + return ( + + + + Connection failed + + + + {text} + + + + + + {buttonLabel} + + + + + ); +} + +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`; + +const ModalContainer = styled.div` + background-color: white; + border-radius: 8px; + max-width: 500px; + width: 90%; + max-height: 80vh; + overflow: hidden; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 2rem 4rem 2rem 4rem; +`; + +const ModalHeader = styled.div` + padding-top: 1.5rem; +`; + +const Title = styled.h2` + margin: 0; + color: #212529; + font-size: 1.5rem; + font-weight: 600; +`; + +const Description = styled.p` + color: #6c757d; + margin-bottom: 2rem; + font-size: 1rem; + line-height: 1.5; +`; + +const ModalFooter = styled.div` + padding: 1.5rem 0; + display: flex; + gap: 1rem; + justify-content: end; +`; + +const Button = styled.button` + padding: 0.75rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + text-transform: uppercase; + min-width: 80px; +`; + +const CancelButton = styled(Button)` + text-transform: uppcase; + background-color: white; + color: #6c757d; + border: 1px solid #D9D9D9; + + &:hover { + background-color: #f8f9fa; + } +`; diff --git a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx index c239b42..2293fc6 100644 --- a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx @@ -17,6 +17,12 @@ interface FileInfo { fileType: string; } +interface NearbySharingError { + text: string; + button: string; + hasError: boolean; +} + interface TransferData { sessionId: string; title: string; @@ -40,6 +46,9 @@ export function useNearbySharing() { // Network state const [localIPs, setLocalIPs] = useState([]); + + // Error state text + const [nearbySharingError, setNearbySharingError] = useState({ text: "", button: "", hasError: false}); // Transfer state const [currentSessionId, setCurrentSessionId] = useState(''); @@ -60,6 +69,8 @@ export function useNearbySharing() { setLocalIPs(ips); } catch (error) { console.error('Failed to get network info:', error); + // TODO (2026-06-18): make sure that this will actually be seen (currently part of CertVerificationModal!) + setNearbySharingError({ text: "Failed to get network info and could not start server.", button: "Start over", hasError: true } as NearbySharingError) } }; @@ -71,6 +82,13 @@ export function useNearbySharing() { setModalState('CONFIRM_RECEIVER') }); + // TODO (2026-06-18): actually emit 'nearby-sharing-error' somewhere in the backend + const cleanupErrorListener = EventsOn("nearby-sharing-error", (data) => { + let err = data as NearbySharingError + err.hasError = true + setNearbySharingError(err) + }); + // TODO (2026-06-17): // * handle early confirm by sender in way that doesn't fuck up effects // * pass senderCertificateHash from golang in the Emit @@ -117,6 +135,7 @@ export function useNearbySharing() { return () => { cleanupFileReceived(); cleanupPingListener(); + cleanupErrorListener(); cleanupRegisterListener(); cleanupCertListener(); cleanupPrepareRequest(); @@ -274,6 +293,7 @@ export function useNearbySharing() { localIPs, currentSessionId, transferData, + nearbySharingError, showVerificationModal, receiverCertificateHash, senderCertificateHash, diff --git a/frontend/src/Components/NearbySharing/NearbySharing.tsx b/frontend/src/Components/NearbySharing/NearbySharing.tsx index 7ae80d0..34c811f 100644 --- a/frontend/src/Components/NearbySharing/NearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/NearbySharing.tsx @@ -16,6 +16,7 @@ export function NearbySharing() { localIPs, currentSessionId, transferData, + nearbySharingError, showVerificationModal, receiverCertificateHash, senderCertificateHash, @@ -91,6 +92,7 @@ export function NearbySharing() { ); } + // TODO (2026-06-18): add CertificateVerificationModal inside one of the steps? instead of free-floating -// TODO (2026-06-16): pass also the "Sender Certificate Hash" to CertificateVerificationModal (+ ensuing rework that enables that) const Container = styled.div` display: flex; From e383d5cfcb867d9fce4489d994b739d201b41450 Mon Sep 17 00:00:00 2001 From: cblgh Date: Thu, 18 Jun 2026 12:27:23 +0200 Subject: [PATCH 10/20] readme: use v2 routes --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5ab7e4f..abc7989 100644 --- a/README.md +++ b/README.md @@ -131,11 +131,16 @@ application. The application implements the [Tella Nearby Sharing protocol](https://github.com/Horizontal-org/Tella-P2P-Protocol) with the following endpoints: - Default Port: 53320 -- `POST /api/v1/ping` - Initial handshake for manual connections -- `POST /api/v1/register` - Device registration with PIN authentication -- `POST /api/v1/prepare-upload` - Prepare file transfer session -- `PUT /api/v1/upload` - File upload with binary data -* `POST /api/v1/close-connection` +- `POST /api/v2/ping` - Initial handshake for manual connections +- `POST /api/v2/register` - Device registration with PIN authentication +- `POST /api/v2/prepare-upload` - Prepare file transfer session +- `PUT /api/v2/upload` - File upload with binary data +- `POST /api/v2/close-connection` + +Legacy routes (transition to v2 is incompatible with v1): + +- `POST /api/v1/ping` - returns 400 Client error +- `POST /api/v1/register` - returns 403 Rejected ## Platform-Specific Notes From 6dea06aa068284f6fb1e1e2ffcdd646673f98df8 Mon Sep 17 00:00:00 2001 From: cblgh Date: Thu, 18 Jun 2026 13:02:04 +0200 Subject: [PATCH 11/20] v2/ping: respond with senderShowHash --- backend/core/modules/registration/handler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index fa90897..fdeb5b1 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -55,7 +55,8 @@ func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { "state": "waiting", }) - w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]bool{"senderShowHash": true}) } // rememberSenderFingerprint pins the sender certificate hash. calling changes tls config of package server, making it stricter in terms From c3a354ada70187525a15127c71f452fd813fcf06 Mon Sep 17 00:00:00 2001 From: cblgh Date: Thu, 18 Jun 2026 17:14:14 +0200 Subject: [PATCH 12/20] fix: align sender cert hash calculation with that of mobile clients --- backend/core/modules/server/service.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/core/modules/server/service.go b/backend/core/modules/server/service.go index 4314e0b..26fea7c 100644 --- a/backend/core/modules/server/service.go +++ b/backend/core/modules/server/service.go @@ -152,12 +152,15 @@ func (s *service) Start(port int) error { return errStart } + // TODO (2026-06-18): change behaviour to wait to send ping response until confirm & continue + // do not require any client certs when server is freshly started tlsConfig.ClientAuth = ctls.RequestClientCert // NOTE cblgh(2026-03-15): set up a custom cert pool using the pinned cert? // to allow use of tls.Config.ClientAuth: tls.RequireAndVerifyClientCert // c.f https://stackoverflow.com/a/63317898 tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + log("number of certs being passed in: %d", len(rawCerts)) // currently pre-mtls and not strictly requiring cert if (tlsConfig.ClientAuth == ctls.RequestClientCert && len(rawCerts) == 0) { return nil @@ -168,12 +171,9 @@ func (s *service) Start(port int) error { } log("VerifyPeerCertificate called") - encodedCert, err := tls.EncodeCertAsPEM(rawCerts[0]) - if err != nil { - return err - } - calculatedPEMHash := sha256.Sum256(encodedCert) - hexPEMHash := fmt.Sprintf("%x", calculatedPEMHash) + + sha256CertHash := sha256.Sum256(rawCerts[0]) + hexSHA256CertHash := fmt.Sprintf("%x", sha256CertHash) // in this section we use a mutex because we want to be sure to never set s.fingerprintCandidate to something else while it is being // fetched by s.GetSenderFingerprintCandidate for being presented to the user @@ -186,10 +186,10 @@ func (s *service) Start(port int) error { // verification i.e. calling s.PinFingerprint() from registration/handler.go if !s.fingerprintCandidateLockedIn { // still pre-mtls but we have now have a candidate to use for senderFingerprint. - s.fingerprintCandidate = hexPEMHash + s.fingerprintCandidate = hexSHA256CertHash log("sender fingerprint candidate %s", s.fingerprintCandidate) } else { - if hexPEMHash != s.fingerprintCandidate { + if hexSHA256CertHash != s.fingerprintCandidate { s.mu.Unlock() return errors.New("Hash of incoming request certificate did not match candidate for sender certificate hash") } @@ -200,8 +200,8 @@ func (s *service) Start(port int) error { s.mu.Unlock() // we should only reach this point in the routine once we have established mTLS and have a pinned sender certificate hash - log("incoming cert hash\n%x\n", calculatedPEMHash) - if hexPEMHash != s.pinnedSenderCertificateHash { + log("incoming cert hash\n%x\n", sha256CertHash) + if hexSHA256CertHash != s.pinnedSenderCertificateHash { return errors.New("Hash of incoming request certificate did not pinned sender certificate hash") } return nil @@ -277,7 +277,7 @@ func (s *service) GetSenderFingerprintCandidate() string { return candidate } -// PinFingerprint pins the SHA256 hash of the PEM-encoded cert. The TLS config is changed to require a client cert, which necessitates restarting the https server instance. +// PinFingerprint pins the hexadecimal-encoded SHA256 hash of the raw certificate bytes. The TLS config is changed to require a client cert, which necessitates restarting the https server instance. func (s *service) PinFingerprint(senderFingerprint string) error { log("Pin fingerprint called") if len(senderFingerprint) != 64 { From fdc96cf8899c18a7794c4950c15e58549c05ab9d Mon Sep 17 00:00:00 2001 From: cblgh Date: Mon, 22 Jun 2026 10:42:23 +0200 Subject: [PATCH 13/20] frontend(copy): login -> unlock --- frontend/src/Components/Auth/Login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Components/Auth/Login.tsx b/frontend/src/Components/Auth/Login.tsx index 16061a9..3b285b2 100644 --- a/frontend/src/Components/Auth/Login.tsx +++ b/frontend/src/Components/Auth/Login.tsx @@ -74,7 +74,7 @@ export function Login({ onLoginSuccess, initialError = '' }: LoginProps) { type="submit" disabled={loading} > - {loading ? 'Loading...' : 'LOG IN'} + {loading ? 'Loading...' : 'UNLOCK'} From 19c0f680791691dd1887b73c9ae56f02c5708aa8 Mon Sep 17 00:00:00 2001 From: cblgh Date: Mon, 22 Jun 2026 10:46:10 +0200 Subject: [PATCH 14/20] mtls: defer sending of ping response until receiver confirmation --- backend/app/app.go | 9 ++++++ backend/core/modules/registration/handler.go | 32 +++++++++++++++---- backend/core/modules/server/service.go | 3 +- .../NearbySharing/Hooks/useNearbySharing.tsx | 4 ++- frontend/wailsjs/go/app/App.d.ts | 2 ++ frontend/wailsjs/go/app/App.js | 4 +++ 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/backend/app/app.go b/backend/app/app.go index 45791b4..746f62b 100644 --- a/backend/app/app.go +++ b/backend/app/app.go @@ -112,6 +112,15 @@ func (a *App) ConfirmRegistration() error { return a.registrationHandler.ConfirmRegistration() } +// called as part of manual connection, when the receiver has confirmed the "receiver cert hash verification" by +// pressing button "confirm and continue" +func (a *App) ManualConfirmationReceiverForReceiver() error { + if a.registrationHandler == nil { + return errRegistrationNotInit + } + return a.registrationHandler.SendPingResponse() +} + func (a *App) RejectRegistration() error { if a.registrationHandler == nil { return errRegistrationNotInit diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index fdeb5b1..83119b1 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -32,6 +32,7 @@ type Handler struct { service Service ctx context.Context pendingRegistration *PendingRegistration + pendingPingResponse chan struct{} mu sync.RWMutex } @@ -42,23 +43,40 @@ func NewHandler(service Service, ctx context.Context) *Handler { } } -// TODO (2026-06-15): only respond to ping if not registered/registration not in progress func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } - runtime.EventsEmit(h.ctx, "ping-received", map[string]interface{}{ - "timestamp": time.Now().Unix(), - "message": "Device attempting to connect", - "state": "waiting", - }) - + // we only react & respond to the first ping request we receive. + // + // TODO (2026-06-22): would like to tie the confirmation action to the ip doing the ip request, so that we + // can selectively only respond to that ping request. the current frontend structure makes this a bit difficult / fraught. + // + // barring that we decide to only react & respond to the first received ping request. this is also inline with the + // "single connection"-centric model of tella's p2p protocol. i.e. we only respond to ping if sender not + // registered/registration not in progress. + if h.pendingPingResponse == nil { + h.pendingPingResponse = make(chan struct{}) + runtime.EventsEmit(h.ctx, "ping-received", map[string]interface{}{ + "timestamp": time.Now().Unix(), + "message": "Device attempting to connect", + "state": "waiting", + }) + } + + // NOTE (2026-06-22): this style of solution only handles one ping response at a time: the first ping received + <-h.pendingPingResponse w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"senderShowHash": true}) } +func (h *Handler) SendPingResponse() error { + h.pendingPingResponse <- struct{}{} + return nil +} + // rememberSenderFingerprint pins the sender certificate hash. calling changes tls config of package server, making it stricter in terms // of requiring tls client certs. the tls config change requires restarting the https server instance. // diff --git a/backend/core/modules/server/service.go b/backend/core/modules/server/service.go index 26fea7c..0610f92 100644 --- a/backend/core/modules/server/service.go +++ b/backend/core/modules/server/service.go @@ -152,8 +152,6 @@ func (s *service) Start(port int) error { return errStart } - // TODO (2026-06-18): change behaviour to wait to send ping response until confirm & continue - // do not require any client certs when server is freshly started tlsConfig.ClientAuth = ctls.RequestClientCert // NOTE cblgh(2026-03-15): set up a custom cert pool using the pinned cert? @@ -262,6 +260,7 @@ func (s *service) startServer() error { s.running = true return nil } + func (s *service) GetSenderFingerprintCandidate() string { var candidate string // use mutex because we want to be sure to never set s.fingerprintCandidate to something else while it is being diff --git a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx index 2293fc6..562ecdf 100644 --- a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from "react"; import { useNavigate } from 'react-router-dom'; -import { GetLocalIPs, RejectRegistration, ConfirmRegistration, StopTransfer } from "../../../../wailsjs/go/app/App"; +import { GetLocalIPs, RejectRegistration, ManualConfirmationReceiverForReceiver, ConfirmRegistration, StopTransfer } from "../../../../wailsjs/go/app/App"; import { EventsOn } from "../../../../wailsjs/runtime/runtime"; import { useServer } from "../../../Contexts/ServerContext"; import { log } from "../../../util/util" @@ -9,6 +9,7 @@ type FlowStep = 'intro' | 'connect' | 'accept' | 'receive' | 'results'; type ManualConfirmationState = 'CONFIRM_RECEIVER' | 'CONFIRM_SENDER' // TODO (2026-06-16): with state transitions etc, make sure to also handle if "sender confirmed before receiver!" +// TODO (2026-06-22): make desktop wait with sending ping response until "confirm & continue" is pressed interface FileInfo { id: string; @@ -157,6 +158,7 @@ export function useNearbySharing() { }; const handleReceiverConfirmReceiver = async () => { + await ManualConfirmationReceiverForReceiver() setModalState("CONFIRM_SENDER") } // Receiver Certificate verification handlers diff --git a/frontend/wailsjs/go/app/App.d.ts b/frontend/wailsjs/go/app/App.d.ts index 844e386..bdefb0a 100755 --- a/frontend/wailsjs/go/app/App.d.ts +++ b/frontend/wailsjs/go/app/App.d.ts @@ -35,6 +35,8 @@ export function IsServerRunning():Promise; export function LockApp():Promise; +export function ManualConfirmationReceiverForReceiver():Promise; + export function RejectRegistration():Promise; export function RejectTransfer(arg1:string):Promise; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index c83fcaf..26f25f0 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -66,6 +66,10 @@ export function LockApp() { return window['go']['app']['App']['LockApp'](); } +export function ManualConfirmationReceiverForReceiver() { + return window['go']['app']['App']['ManualConfirmationReceiverForReceiver'](); +} + export function RejectRegistration() { return window['go']['app']['App']['RejectRegistration'](); } From 43b87faad88db70698ee5da2283d03bae7b2584c Mon Sep 17 00:00:00 2001 From: cblgh Date: Mon, 22 Jun 2026 10:52:41 +0200 Subject: [PATCH 15/20] guard against nil channel --- backend/core/modules/registration/handler.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index 83119b1..1b7ce2b 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -73,7 +73,10 @@ func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { } func (h *Handler) SendPingResponse() error { - h.pendingPingResponse <- struct{}{} + // channel should never be nil here, but then again sometimes 'never' does happen :) + if h.pendingPingResponse != nil { + h.pendingPingResponse <- struct{}{} + } return nil } From 8bff1fba8bafe9f5fc7b61ebbdbf1fdf3aa6ad87 Mon Sep 17 00:00:00 2001 From: cblgh Date: Mon, 22 Jun 2026 11:05:06 +0200 Subject: [PATCH 16/20] backend(registration): clear registration state on server (re)start --- backend/core/modules/registration/handler.go | 10 ++++++++++ backend/core/modules/server/service.go | 3 +++ 2 files changed, 13 insertions(+) diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index 1b7ce2b..1e1d309 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -43,6 +43,16 @@ func NewHandler(service Service, ctx context.Context) *Handler { } } +func (h *Handler) Reset() { + h.mu.Lock() + if h.pendingPingResponse != nil { + close(h.pendingPingResponse) + } + h.pendingPingResponse = nil + h.pendingRegistration = nil + h.mu.Unlock() +} + func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) diff --git a/backend/core/modules/server/service.go b/backend/core/modules/server/service.go index 0610f92..0ef3772 100644 --- a/backend/core/modules/server/service.go +++ b/backend/core/modules/server/service.go @@ -123,6 +123,9 @@ func (s *service) Start(port int) error { return errStart } + // reset registrationHandler state + s.registrationHandler.Reset() + // Generate new PIN for each start s.pin = generateRandomPIN() s.registrationService.SetPINCode(s.pin) From 29e19e11698f698ec332afcb5e85a897fca4ce45 Mon Sep 17 00:00:00 2001 From: cblgh Date: Mon, 22 Jun 2026 13:37:55 +0200 Subject: [PATCH 17/20] backend(ping): improve state handling around ping response if we receive a register request with an incorrect PIN (e.g. sender typing a 1 instead of a 2 in a PIN). we now reset the channel we use to 'lock' the ping response to a single ping request. this means a sender can try anew especially if the sender goes through the whole ping flow nad again and doesn't immediately send a register request with the correct PIN this change also allows the frontend the ability (not in this commit though!) to selectively hide the verification modal on waiting for the sender, and effectively creates the ability for the receiving user to 'manually unlock the ping channel' if that makes sense. --- backend/app/app.go | 9 +++++++++ backend/core/modules/registration/handler.go | 14 +++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/backend/app/app.go b/backend/app/app.go index 746f62b..9a724c5 100644 --- a/backend/app/app.go +++ b/backend/app/app.go @@ -121,6 +121,15 @@ func (a *App) ManualConfirmationReceiverForReceiver() error { return a.registrationHandler.SendPingResponse() } +// called when we cancel waiting for the sender but still keep the server open +func (a *App) ManualConfirmationCancelWaiting() error { + if a.registrationHandler == nil { + return errRegistrationNotInit + } + a.registrationHandler.ResetPingResponse() + return nil +} + func (a *App) RejectRegistration() error { if a.registrationHandler == nil { return errRegistrationNotInit diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index 1e1d309..dc1d3d5 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -43,12 +43,16 @@ func NewHandler(service Service, ctx context.Context) *Handler { } } -func (h *Handler) Reset() { - h.mu.Lock() +func (h *Handler) ResetPingResponse() { if h.pendingPingResponse != nil { close(h.pendingPingResponse) } h.pendingPingResponse = nil +} + +func (h *Handler) Reset() { + h.mu.Lock() + h.ResetPingResponse() h.pendingRegistration = nil h.mu.Unlock() } @@ -134,7 +138,11 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe if authorised, err := h.service.IsAuthorised(request.PIN, request.Nonce); !authorised { if errors.Is(err, ErrPinInvalid) { http.Error(w, "Invalid PIN", http.StatusUnauthorized) - } + // reset ping channel to allow for another attempt + // TODO (2026-06-22): maybe reset isn't actually needed - does mobile send another ping request if PIN is incorrect + // or does it simply send another register request? + h.ResetPingResponse() + } if errors.Is(err, ErrTooManyAttempts) { http.Error(w, "Too many requests", http.StatusTooManyRequests) } From 9a53aac904c03b7e4efc95ad9da7f97e5cd4d514 Mon Sep 17 00:00:00 2001 From: cblgh Date: Mon, 22 Jun 2026 15:52:34 +0200 Subject: [PATCH 18/20] backend(register): comment out ResetPingResponse call on incorrect PIN --- backend/app/app.go | 9 --------- backend/core/modules/registration/handler.go | 7 +++++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/backend/app/app.go b/backend/app/app.go index 9a724c5..746f62b 100644 --- a/backend/app/app.go +++ b/backend/app/app.go @@ -121,15 +121,6 @@ func (a *App) ManualConfirmationReceiverForReceiver() error { return a.registrationHandler.SendPingResponse() } -// called when we cancel waiting for the sender but still keep the server open -func (a *App) ManualConfirmationCancelWaiting() error { - if a.registrationHandler == nil { - return errRegistrationNotInit - } - a.registrationHandler.ResetPingResponse() - return nil -} - func (a *App) RejectRegistration() error { if a.registrationHandler == nil { return errRegistrationNotInit diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index dc1d3d5..aa9f295 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -138,10 +138,13 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, remembe if authorised, err := h.service.IsAuthorised(request.PIN, request.Nonce); !authorised { if errors.Is(err, ErrPinInvalid) { http.Error(w, "Invalid PIN", http.StatusUnauthorized) - // reset ping channel to allow for another attempt + // TODO (2025-06-22): reset ping channel to allow for another attempt // TODO (2026-06-22): maybe reset isn't actually needed - does mobile send another ping request if PIN is incorrect // or does it simply send another register request? - h.ResetPingResponse() + // + // NOTE FOR REVIEWERS: this has been commented out until the team decides whether another connection attempt with + // a change PIN is possible with the current sender interfaces - seems like "discard & start over" renders making another attempt moot + // h.ResetPingResponse() } if errors.Is(err, ErrTooManyAttempts) { http.Error(w, "Too many requests", http.StatusTooManyRequests) From 87d3bfb668cbe310a42214d16267841614666c4e Mon Sep 17 00:00:00 2001 From: cblgh Date: Mon, 22 Jun 2026 16:47:58 +0200 Subject: [PATCH 19/20] frontend: cleanup fixed todos --- .../CertificateVerificationModal.tsx | 13 ++----------- .../NearbySharing/Hooks/useNearbySharing.tsx | 6 ------ 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx index 29b6a23..be7b89f 100644 --- a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx +++ b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx @@ -3,10 +3,6 @@ import { formatHash } from "../../util/util" import { SpinnerModal } from "../NearbySharing/SpinnerModal"; import { ErrorDialog } from "../NearbySharing/ErrorDialog"; -// TODO (2026-06-17): implement waitingState below -// confirm receiver | confirm sender -// waitingState = (confirm_receiver && !SenderConfirmedReceiver) - interface NearbySharingError { text: string; button: string; @@ -25,10 +21,6 @@ interface CertificateVerificationModalProps { onDiscard: () => void; } -// TODO (2026-06-16): -// confirmReceiverHash -> waiting -> waiting for register request to come in -// confirmSenderHash -> waiting -> send register response (?) <-- current onConfirm -// export function CertificateVerificationModal({ isOpen, receiverCertificateHash, @@ -59,7 +51,8 @@ export function CertificateVerificationModal({ } // TODO (2026-06-17): no timeout or error graphic is triggered currently - // TODO (2026-06-18): do not display step 1 after waiting has been started (or step 2 is already being displayed) + // TODO (2026-06-18): implement logic somewhere in frontend to not display step 1 after waiting has been started (or step 2 is already being displayed) + // currently this is taken care of by the ping behaviour (only 1 ping request allowed to be responded to) if (nearbySharingError.hasError) { return ( ` background-color: ${props => props.$isSender ? "#071013CC" : "#D9D9D9"}; border: 1px solid #e9ecef; diff --git a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx index 562ecdf..f7b74f5 100644 --- a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx @@ -8,9 +8,6 @@ import { log } from "../../../util/util" type FlowStep = 'intro' | 'connect' | 'accept' | 'receive' | 'results'; type ManualConfirmationState = 'CONFIRM_RECEIVER' | 'CONFIRM_SENDER' -// TODO (2026-06-16): with state transitions etc, make sure to also handle if "sender confirmed before receiver!" -// TODO (2026-06-22): make desktop wait with sending ping response until "confirm & continue" is pressed - interface FileInfo { id: string; fileName: string; @@ -90,9 +87,6 @@ export function useNearbySharing() { setNearbySharingError(err) }); - // TODO (2026-06-17): - // * handle early confirm by sender in way that doesn't fuck up effects - // * pass senderCertificateHash from golang in the Emit const cleanupRegisterListener = EventsOn("register-request-received", (data) => { log("Register request received:", data); setSenderCertificateHash(data.senderCertificateHash); From a26e11b7a7ea0853a103b9c908d3bce76254db2e Mon Sep 17 00:00:00 2001 From: cblgh Date: Mon, 22 Jun 2026 17:16:34 +0200 Subject: [PATCH 20/20] frontend: remove unused handler, add comment about event to handle and shut down on timeouts --- .../NearbySharing/Hooks/useNearbySharing.tsx | 28 +++++-------------- .../NearbySharing/NearbySharing.tsx | 1 - 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx index f7b74f5..d1e474d 100644 --- a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx @@ -105,6 +105,9 @@ export function useNearbySharing() { setCurrentSessionId(requestData.sessionId); }); + // TODO (2026-06-22): implement event in backend and handler here in frontend that signals that register timed out or + // max PIN registration attempts has been reached + const cleanupFileReceived = EventsOn("file-received", () => { setTransferData(prev => { if (prev !== null) { @@ -151,13 +154,14 @@ export function useNearbySharing() { return await stopServer(); }; + // {Receiver, Sender} Certificate Hash verification handlers const handleReceiverConfirmReceiver = async () => { await ManualConfirmationReceiverForReceiver() setModalState("CONFIRM_SENDER") } - // Receiver Certificate verification handlers + const handleVerificationConfirm = async () => { - log("✅ Receiver Certificate verification CONFIRMED"); + log("✅ Sender Certificate Hash: verification CONFIRMED"); try { await ConfirmRegistration(); setShowVerificationModal(false); @@ -169,25 +173,8 @@ export function useNearbySharing() { } }; - // TODO (2026-06-15): revise copy of handleVerificationDiscard to make sure is implemented properly - const handleWaitingForSenderCancel = async () => { - log("❌ Waiting for sender CANCELED"); - try { - await RejectRegistration(); - } catch (error) { - console.error("Failed to reject registration:", error); - } - // if rejected, reset state - setShowVerificationModal(false); - setModalState('CONFIRM_RECEIVER'); - setSenderConfirmedReceiver(false); - - await handleStopServer(); - setCurrentStep('intro'); - }; - const handleVerificationDiscard = async () => { - log("❌ Receiver Certificate verification DISCARDED"); + log("❌ Verification DISCARDED"); try { await RejectRegistration(); } catch (error) { @@ -302,7 +289,6 @@ export function useNearbySharing() { handleReceiverConfirmReceiver, handleVerificationConfirm, handleVerificationDiscard, - handleWaitingForSenderCancel, handleFileRequestAccept, handleFileRequestReject, handleFileReceiving, diff --git a/frontend/src/Components/NearbySharing/NearbySharing.tsx b/frontend/src/Components/NearbySharing/NearbySharing.tsx index 34c811f..fb2b7ad 100644 --- a/frontend/src/Components/NearbySharing/NearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/NearbySharing.tsx @@ -27,7 +27,6 @@ export function NearbySharing() { handleVerificationConfirm, handleReceiverConfirmReceiver, handleVerificationDiscard, - handleWaitingForSenderCancel, handleFileRequestAccept, handleFileRequestReject, handleFileReceiving,