﻿{"id":835947,"date":"2026-02-19T22:50:41","date_gmt":"2026-02-19T22:50:41","guid":{"rendered":"https:\/\/gridnet.org\/wpp\/?p=835947"},"modified":"2026-02-20T07:57:58","modified_gmt":"2026-02-20T07:57:58","slug":"zero-knowledge-group-authentication-webrtc-swarms","status":"publish","type":"post","link":"https:\/\/mag.gridnet.org\/index.php\/2026\/02\/19\/zero-knowledge-group-authentication-webrtc-swarms\/","title":{"rendered":"Zero-Knowledge Group Authentication in Decentralized WebRTC Swarms"},"content":{"rendered":"<p><!-- GRIDNET Magazine: Zero-Knowledge Group Authentication in Decentralized WebRTC Swarms --> <!-- Designated Verifier Proofs via PSK-Keyed MAC: Theory, Protocol, and Production Code --> <\/p>\n<p>In every human society, from the earliest campfire circles to modern boardrooms, there exists a fundamental tension between belonging and proving that one belongs. The password at the city gate, the secret handshake of the guild, the biometric scan at the corporate entrance \u2014 each is an attempt to solve the same ancient problem: how does a group verify a newcomer&#8217;s right to participate <em>without revealing the secret that grants that right?<\/em><\/p>\n<p>In the digital realm, this problem acquires new urgency. Centralized systems solve it trivially: a server holds the truth and dispenses access. But in a decentralized peer-to-peer network \u2014 where no server exists, where every participant is simultaneously a client and a validator, where the network itself is the only authority \u2014 the problem becomes profound. How do you prove you know a secret to a peer who also knows it, without either of you ever transmitting the secret itself, and without any third party being able to replay or forge the proof?<\/p>\n<p>The answer, implemented in the GRIDNET OS Swarms API and formally described in the MDPI research paper <em>&#8220;WebRTC Swarms: Decentralized, Incentivized, and Privacy-Preserving Signaling with Designated Verifier Zero-Knowledge Authentication&#8221;<\/em> (Skowro\u0144ski, <em>Future Internet<\/em>, 2025), is a designated verifier zero-knowledge proof protocol built on PSK-keyed MAC construction. This article dissects the protocol at every level: the mathematical foundation, the state machine, the timing constraints, and the production JavaScript implementation in the GRIDNET OS Meeting dApp.<\/p>\n<figure> <img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/zero-knowledge-group-authentication-webrtc-swarms-fig-1.svg\" alt=\"Figure 1\" style=\"max-width:100%;height:auto;\" \/><figcaption>Fig. 1 \u2014 The three-phase ZKP authentication protocol between a Validator (V) and a Candidate (C). Phase 1 exchanges nonces, a mandatory waiting period prevents replay attacks, Phase 2 delivers the challenge, and Phase 3 delivers the proof. The pre-shared key (PSK) never traverses the network.<\/figcaption><\/figure>\n<h2>1. The Mathematical Foundation \u2014 SHA3-Keyed MAC as Zero-Knowledge Proof<\/h2>\n<p>At its core, the GRIDNET OS ZKP protocol is a <em>designated verifier proof<\/em> constructed from a keyed hash function. The proof is computed as:<\/p>\n<pre>\nZKP = SHA3-256( PSK \u2225 IV\u2081 \u2225 IV\u2082 )\n<\/pre>\n<p>Where:<\/p>\n<ul>\n<li><code>PSK<\/code> \u2014 the 256-bit <em>effective<\/em> pre-shared key (never transmitted; derived through a two-stage process from the user-provided password \u2014 see below)<\/li>\n<li><code>IV\u2081<\/code> \u2014 a 256-bit random nonce generated by the <em>Validator<\/em><\/li>\n<li><code>IV\u2082<\/code> \u2014 a 256-bit random nonce generated by the <em>Candidate<\/em><\/li>\n<\/ul>\n<p>This construction is a variant of the HMAC pattern (Bellare et al., 1996), adapted for the specific constraints of peer-to-peer authentication. The inclusion of two independent nonces (one from each party) ensures that:<\/p>\n<ol>\n<li><strong>No replay attacks<\/strong>: Each proof is bound to a unique pair of nonces. Replaying a captured proof against a new challenge will fail because IV\u2081 is always freshly generated.<\/li>\n<li><strong>No offline dictionary attacks<\/strong>: An attacker who observes (IV\u2081, IV\u2082, ZKP) would need to brute-force the PSK through SHA3-256 \u2014 computationally infeasible for strong passwords.<\/li>\n<li><strong>Designated verifier property<\/strong>: Only the validator who generated IV\u2081 (and who knows the PSK) can verify the proof. A third party who intercepts the proof cannot verify it without the PSK.<\/li>\n<li><strong>Mutual freshness<\/strong>: The candidate contributes IV\u2082, preventing the validator from using a pre-computed challenge to extract information.<\/li>\n<\/ol>\n<figure> <img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/zero-knowledge-group-authentication-webrtc-swarms-fig-2.svg\" alt=\"Figure 2\" style=\"max-width:100%;height:auto;\" \/><figcaption>Fig. 2 \u2014 The ZKP is computed as SHA3-256(PSK \u2225 IV\u2081 \u2225 IV\u2082), combining the secret key with two fresh random nonces. The pre-shared key never leaves the local machine; only the proof (a hash output) traverses the network.<\/figcaption><\/figure>\n<h2>2. Credential Issuance and the Pre-Shared Key Lifecycle<\/h2>\n<p>In the GRIDNET OS model, the &#8220;credential&#8221; is a pre-shared key (PSK) that all legitimate swarm participants know. The PSK is not stored in plaintext \u2014 it is immediately transformed into a 256-bit key image through SHA3-256 hashing upon entry. This key image is what the system actually uses for ZKP computation.<\/p>\n<pre>\n\/\/ From CSwarm \u2014 setting the pre-shared key\nasync setPreSharedKey(pass) {\n  if (gTools.isNull(pass) || pass.length == 0) {\n    \/\/ Make swarm public\n    armingPrivacy = false;\n  } else {\n    armingPrivacy = true;\n  }\n\n  if (armingPrivacy) {\n    \/\/ Transform password to key image\n    \/\/ PSK = SHA3-256(password)\n    \/\/ The raw password is NEVER stored\n    this.mPasswordImage = sha3_256.arrayBuffer(pass);\n  }\n}\n<\/pre>\n<p><strong>Critical: Two-Stage Key Derivation.<\/strong> The PSK used in ZKP computation is <em>not<\/em> simply <code>SHA3-256(password)<\/code>. There is a second stage. When <code>getPreSharedKey(true)<\/code> is called (at authentication time), the system adds a <strong>time-based IV<\/strong> derived from the current Unix timestamp:<\/p>\n<pre>\n\/\/ From CSwarm.getPreSharedKey(addNonce = true)\n\/\/ Stage 1: mPasswordImage = SHA3-256(password)  [stored at setPreSharedKey time]\n\/\/ Stage 2: effective PSK = SHA3-256(mPasswordImage \u2225 time_IV)\n\/\/\n\/\/ The time_IV is a 3-byte slice of the Big-Endian timestamp:\nlet IV = gTools.numberToArrayBuffer(\n  gTools.getTime(), false\n).slice(4, 7);  \/\/ bytes 4-6 of timestamp\n\nlet dc = new CDataConcatenator();\ndc.add(this.mPasswordImage);  \/\/ 32-byte stored key image\ndc.add(IV);                   \/\/ 3-byte time slice\nreturn sha3_256.arrayBuffer(dc.getData());  \/\/ 32-byte effective PSK\n<\/pre>\n<p>This time-dependent derivation has a crucial purpose: it provides an approximately <strong>18-hour drift tolerance<\/strong> window. By stripping the least-significant byte of the timestamp (which cycles every 256 seconds), the system ensures that peers whose clocks are within ~128 seconds of each other compute the same effective PSK \u2014 while preventing long-delayed replay attacks from succeeding even if an attacker captures a valid (IV\u2081, IV\u2082, ZKP) triple.<\/p>\n<p>The lifecycle of a PSK follows a clear state machine:<\/p>\n<ol>\n<li><strong>Issuance<\/strong>: A participant issues the <code>\/setkey &lt;password&gt;<\/code> command in the swarm&#8217;s chat.<\/li>\n<li><strong>Propagation<\/strong>: The password must be communicated to other participants through an out-of-band channel (voice, secure message, etc.). The system never transmits it.<\/li>\n<li><strong>Activation<\/strong>: Upon setting, the swarm&#8217;s <code>authRequirement<\/code> transitions to <code>PSK_ZK<\/code>, and all active connections are immediately challenged to re-authenticate.<\/li>\n<li><strong>Rotation<\/strong>: A new <code>\/setkey<\/code> command with a different password immediately mutes all media streams (<code>await this.mute(true, true, true, true)<\/code>) and forces re-authentication of all peers \u2014 preventing data leakage during the re-auth window.<\/li>\n<li><strong>Revocation<\/strong>: <code>\/setkey<\/code> with no arguments clears the PSK, returning the swarm to open mode.<\/li>\n<\/ol>\n<figure> <img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/zero-knowledge-group-authentication-webrtc-swarms-fig-3.svg\" alt=\"Figure 3\" style=\"max-width:100%;height:auto;\" \/><figcaption>Fig. 3 \u2014 The PSK lifecycle: from open (no key) through private (key set, peers challenged) to authenticated (ZKP verified, media flows). Key rotation forces all peers back through the authentication flow. Clearing the key returns to open mode.<\/figcaption><\/figure>\n<h2>3. The Protocol State Machine \u2014 A Detailed Walk-Through<\/h2>\n<p>The ZKP protocol operates as a tightly coordinated state machine between two roles: <strong>Validator<\/strong> (the peer that initiates the authentication challenge) and <strong>Candidate<\/strong> (the peer that must prove knowledge of the PSK). In GRIDNET OS, authentication is <em>mutual<\/em> \u2014 each peer acts as both validator and candidate simultaneously, using separate state machines that run in parallel.<\/p>\n<h3>3.1 Phase 1 \u2014 Bootstrap (Nonce Exchange)<\/h3>\n<p>When a data channel opens on a private swarm, the validator immediately dispatches a Phase 1 <code>authRequestVal<\/code> message with an empty data payload. This is the &#8220;authenticate yourself&#8221; challenge:<\/p>\n<pre>\n\/\/ Validator initiates (CSwarmConnection.dispatchAuthRequestVal)\ndispatchAuthRequestVal(phase = 1) {\n  if (phase == 1) {\n    let authMsg = new CSwarmAuthData();\n    authMsg.isZKP = true;\n    authMsg.isDedicatedPSK = this.mSwarm.isDedicatedPSK;\n    authMsg.data = new ArrayBuffer(); \/\/ Empty = Phase 1\n\n    let msg = new CSwarmMsg(\n      eSwarmMsgType.authenticationRequestVal,\n      this.mSwarm.getMyID,\n      this.getPeerID,\n      authMsg.getPackedData()\n    );\n\n    let wrapper = new CNetMsg(\n      this.getProtocolID, eNetReqType.request,\n      msg.getPackedData()\n    );\n    this.send(wrapper.getPackedData(), false);\n  }\n}\n<\/pre>\n<p>The candidate receives this message, generates a 256-bit random nonce (IV\u2082), and returns it to the validator while starting Timer\u2081 (10 seconds). The critical security invariant is that the candidate will <strong>reject any IV\u2081 received before Timer\u2081 expires<\/strong>:<\/p>\n<pre>\n\/\/ Candidate responds (CSwarmConnection.processAuthRequestVal)\nprocessAuthRequestVal(authData) {\n  let nonce = authData.data;\n\n  if (gTools.getLength(nonce) == 0) {\n    \/\/ Phase 1: Validator says \"authenticate!\"\n    this.genZKPNonce2(); \/\/ Generate IV\u2082 (32 random bytes)\n\n    let msg = new CSwarmMsg(\n      eSwarmMsgType.authenticationRequestCand,\n      this.mSwarm.getMyID,\n      this.getPeerID,\n      this.mZKPNonce2Local \/\/ Send IV\u2082\n    );\n\n    this.startZKPTimer1Local(); \/\/ Start 10-second timer\n    this.send(wrapper.getPackedData(), false);\n  }\n}\n<\/pre>\n<figure> <img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/zero-knowledge-group-authentication-webrtc-swarms-fig-4.svg\" alt=\"Figure 4\" style=\"max-width:100%;height:auto;\" \/><figcaption>Fig. 4 \u2014 Phase 1 detail: the data channel opens, the validator sends an empty challenge, the candidate responds with IV\u2082 and starts a 10-second countdown. Both peers must wait for Timer\u2081 to expire before proceeding.<\/figcaption><\/figure>\n<h3>3.2 Phase 2 \u2014 Challenge Delivery (IV\u2081)<\/h3>\n<p>After receiving IV\u2082, the validator stores it and schedules the delivery of IV\u2081 for after Timer\u2081 expires. This is implemented via <code>setTimeout()<\/code> \u2014 a deliberate choice that leverages the browser&#8217;s event loop for timing:<\/p>\n<pre>\n\/\/ Validator processes candidate's response\nprocessAuthRequestCand(data) {\n  if (gTools.getLength(data) != 32) {\n    this.resetZKPState(false, true);\n    return false;\n  }\n\n  this.mZKPNonce2Remote = data; \/\/ Store IV\u2082 from candidate\n  this.startZKPTimer1Remote();\n\n  \/\/ Schedule IV\u2081 delivery after Timer\u2081 expires\n  setTimeout(function() {\n    this.dispatchAuthRequestVal(2); \/\/ Phase 2\n  }.bind(this), this.mZKPTimer1ExpMS); \/\/ 10,000ms\n\n  return true;\n}\n<\/pre>\n<p>When Timer\u2081 expires, the validator generates IV\u2081 (its own 256-bit nonce) and sends it to the candidate, simultaneously starting Timer\u2082 (3 seconds) \u2014 the window within which the ZKP must arrive:<\/p>\n<pre>\n\/\/ Phase 2 dispatch\ndispatchAuthRequestVal(phase = 2) {\n  this.genZKPNonce1(); \/\/ Generate IV\u2081\n  let lIV1ToBeSent = this.mZKPNonce1Local;\n\n  \/\/ ... pack and send authRequestVal with IV\u2081 ...\n\n  \/\/ SECURITY: Start Timer\u2082 \u2014 ZKP must arrive within 3 seconds\n  this.startZKPTimer2();\n}\n<\/pre>\n<p>The mandatory 10-second delay between Phase 1 and Phase 2 is the protocol&#8217;s primary defense against replay attacks. An attacker who captures a valid (IV\u2082, ZKP) pair from a previous session cannot replay it because: (a) IV\u2081 is freshly generated, and (b) the candidate&#8217;s Timer\u2081 prevents premature acceptance of an IV\u2081 that might have been pre-computed.<\/p>\n<h3>3.3 Phase 3 \u2014 Proof Generation and Verification<\/h3>\n<p>Upon receiving IV\u2081, the candidate first verifies that Timer\u2081 has expired (the critical anti-replay check), then computes the ZKP:<\/p>\n<pre>\n\/\/ Candidate computes ZKP (CSwarmConnection.prepareAndDispatchZKP)\nprepareAndDispatchZKP() {\n  let psk = this.mSwarm.getPreSharedKey(true); \/\/ Key image\n  let IV1 = this.mZKPNonce1Remote;  \/\/ From validator\n  let IV2 = this.mZKPNonce2Local;   \/\/ Generated locally\n\n  \/\/ Validate: all values must be 32 bytes\n  if (gTools.getLength(IV1) != 32 ||\n      gTools.getLength(IV2) != 32 ||\n      gTools.getLength(psk)  != 32) {\n    this.resetZKPState(true, false);\n    return false;\n  }\n\n  \/\/ Compute ZKP = SHA3-256(PSK \u2225 IV\u2081 \u2225 IV\u2082)\n  let dc = new CDataConcatenator();\n  dc.add(psk);\n  dc.add(IV1);\n  dc.add(IV2);\n  let ephemeralZKP = sha3_256.arrayBuffer(dc.getData());\n\n  \/\/ Send to validator\n  let msg = new CSwarmMsg(\n    eSwarmMsgType.zeroKnowledgeProof,\n    this.mSwarm.getMyID,\n    this.getPeerID,\n    ephemeralZKP\n  );\n  this.send(wrapper.getPackedData(), false);\n  return true;\n}\n<\/pre>\n<p>The validator receives the ZKP and performs an identical computation using its own copies of IV\u2081, IV\u2082, and the PSK. If the computed and received values match, authentication succeeds:<\/p>\n<pre>\n\/\/ Validator verifies ZKP (CSwarmConnection.processZKP)\nasync processZKP(ZKP) {\n  \/\/ TIMING CHECK: reject if Timer\u2082 has expired\n  if (this.isTimer2Expired()) {\n    this.resetZKPState(false, true);\n    return false;\n  }\n\n  let psk = this.mSwarm.getPreSharedKey(true);\n  let IV1 = this.mZKPNonce1Local;   \/\/ Generated locally\n  let IV2 = this.mZKPNonce2Remote;  \/\/ From candidate\n\n  \/\/ Compute expected ZKP\n  let dc = new CDataConcatenator();\n  dc.add(psk);\n  dc.add(IV1);\n  dc.add(IV2);\n  let expectedZKP = sha3_256.arrayBuffer(dc.getData());\n\n  \/\/ Compare\n  if (gTools.compareByteVectors(expectedZKP, ZKP)) {\n    this.isAuthenticated = true;\n    this.notifyAuthenticationSuccess();\n  } else {\n    this.isAuthenticated = false;\n    this.notifyAuthenticationFailure();\n  }\n\n  this.onPeerAuth(result, eSwarmAuthMode.preSharedSecret);\n  return result;\n}\n<\/pre>\n<figure> <img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/zero-knowledge-group-authentication-webrtc-swarms-fig-5.svg\" alt=\"Figure 5\" style=\"max-width:100%;height:auto;\" \/><figcaption>Fig. 5 \u2014 Phase 3 in detail: the candidate computes ZKP = SHA3-256(PSK \u2225 IV\u2081 \u2225 IV\u2082) and sends it. The validator independently computes the expected value and compares. Match means authentication; mismatch triggers state reset and failure notification.<\/figcaption><\/figure>\n<h2>4. Timing Constraints \u2014 The Anti-Replay Arsenal<\/h2>\n<p>The protocol employs three timing parameters, each serving a distinct security purpose:<\/p>\n<pre>\n\/\/ CSwarmConnection constructor \u2014 timing constants\nthis.mClockDrift     = 2000;   \/\/ Allowed clock drift (ms)\nthis.mZKPTimer1ExpMS = 10000;  \/\/ Timer\u2081: nonce aging window\nthis.mZKPTimer2ExpMS = 3000;   \/\/ Timer\u2082: proof delivery window\n<\/pre>\n<p><strong>Timer\u2081 (10 seconds)<\/strong> \u2014 Started by the candidate when IV\u2082 is sent and by the validator when IV\u2082 is received. The candidate will reject any IV\u2081 received before Timer\u2081 expires. This prevents an attacker from immediately replaying a captured IV\u2081 \u2014 by the time the 10-second window opens, any previously captured proof is stale.<\/p>\n<p><strong>Timer\u2082 (3 seconds)<\/strong> \u2014 Started by the validator when IV\u2081 is sent. The ZKP must arrive within this window. This prevents slow-path attacks where an attacker captures IV\u2081 and attempts to brute-force the PSK offline before replaying.<\/p>\n<p><strong>Clock Drift (2 seconds)<\/strong> \u2014 An allowance for network propagation delay and clock synchronization differences between peers. The validator&#8217;s Timer\u2081 check includes this tolerance:<\/p>\n<pre>\nisTimer1RemoteExpired() {\n  let now = gTools.getTime(true);\n  \/\/ Account for clock drift on the remote end\n  if ((now - this.mZKPTimer1RemoteStartMS) &gt;\n      (this.mZKPTimer1ExpMS + this.mClockDrift)) {\n    return true;\n  }\n  return false;\n}\n<\/pre>\n<figure> <img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/zero-knowledge-group-authentication-webrtc-swarms-fig-6.svg\" alt=\"Figure 6\" style=\"max-width:100%;height:auto;\" \/><figcaption>Fig. 6 \u2014 The timing constraint diagram showing Timer\u2081 (10-second anti-replay window during which IV\u2081 is rejected), Timer\u2082 (3-second proof delivery deadline), and the clock drift tolerance. An attacker&#8217;s window for brute-force is limited to Timer\u2082&#8217;s duration \u2014 far too short for SHA3-256.<\/figcaption><\/figure>\n<h2>5. State Machine Reset \u2014 Graceful Failure<\/h2>\n<p>When any phase of the protocol fails \u2014 invalid nonce length, expired timer, incorrect proof \u2014 the state machine resets cleanly. The <code>resetZKPState()<\/code> method is carefully parameterized to reset either the local candidacy state or the local validator state independently, preserving the parallel mutual authentication:<\/p>\n<pre>\nresetZKPState(myCandState = true, myValState = true) {\n  if (myCandState) {\n    \/\/ Reset local node acting as a candidate\n    this.ZKPNonce1Remote = new ArrayBuffer(); \/\/ Clear validator's IV\u2081\n    this.ZKPNonce2Local  = new ArrayBuffer(); \/\/ Clear own IV\u2082\n    this.mZKPTimer1LocalStartMS = 0;\n  }\n\n  if (myValState) {\n    \/\/ Reset local node acting as a validator\n    this.ZKPNonce1Local  = new ArrayBuffer(); \/\/ Clear own IV\u2081\n    this.ZKPNonce2Remote = new ArrayBuffer(); \/\/ Clear candidate's IV\u2082\n    this.mZKPTimer1RemoteStartMS = 0;\n    this.mZKPTimer2LocalStartMS  = 0;\n  }\n}\n<\/pre>\n<p>This dual-state architecture is crucial. In a mutual authentication scenario, each peer is simultaneously running two state machines: one where it proves its own identity (candidate role) and one where it verifies the other peer (validator role). A failure in one role must not corrupt the other.<\/p>\n<figure> <img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/zero-knowledge-group-authentication-webrtc-swarms-fig-7-1.svg\" alt=\"Figure 7\" style=\"max-width:100%;height:auto;\" \/><figcaption>Fig. 7 \u2014 Each peer simultaneously runs two independent ZKP state machines: one for validating the remote peer (validator role) and one for proving its own identity (candidate role). Failures in one role do not corrupt the other, enabling robust mutual authentication.<\/figcaption><\/figure>\n<h2>6. The CSwarmAuthData Container<\/h2>\n<p>Authentication metadata is encapsulated in <code>CSwarmAuthData<\/code>, a bitfield-equipped container that carries flags and arbitrary data through the same ASN.1\/BER serialization pipeline as all other swarm messages:<\/p>\n<pre>\nexport class CSwarmAuthData {\n  constructor() {\n    this.mData = new ArrayBuffer();\n    this.mFlags = new ArrayBuffer(1);\n    this.mVersion = 1;\n  }\n\n  \/\/ Bitfield accessors\n  set isZKP(isIt) {\n    let view = new Uint8Array(this.mFlags);\n    if (isIt) view[0] |= 0b00000001;\n    else      view[0] &amp;= ~0b00000001;\n  }\n\n  set isDedicatedPSK(isIt) {\n    let view = new Uint8Array(this.mFlags);\n    if (isIt) view[0] |= 0b00000001;\n    else      view[0] &amp;= ~0b00000001;\n  }\n}\n<\/pre>\n<p>The <code>isDedicatedPSK<\/code> flag distinguishes between a user-provided password and an automatically derived key (computed from the swarm&#8217;s true ID with a time-based IV). This distinction enables two modes of private swarms: those protected by a user-chosen passphrase and those using the cryptographic identity of the swarm itself as implicit authentication.<\/p>\n<h2>7. The Meeting dApp \u2014 Authentication in Practice<\/h2>\n<p>In the Meeting dApp, authentication events are surfaced through a rich visual UI. Each peer&#8217;s video feed includes an authentication state icon that transitions through four states:<\/p>\n<pre>\nsetPeerAuthStateInUI(peerID, authState) {\n  switch (authState) {\n    case 0: \/\/ No auth required (open swarm)\n      icon.classList.add('fa-globe', 'authNone');\n      break;\n    case 1: \/\/ Auth required but not yet verified\n      icon.classList.add('fa-lock', 'authLocked');\n      audioControl.muted = true; \/\/ Mute audio (prevent stub beeping)\n      break;\n    case 2: \/\/ Successfully authenticated\n      icon.classList.add('fa-lock-open', 'authUnlocked');\n      if (shouldAudioBeActive) audioControl.muted = false;\n      break;\n    case 3: \/\/ Local peer has set auth requirement (key icon)\n      icon.classList.add('fa-key', 'authKey');\n      break;\n  }\n}\n<\/pre>\n<p>The progression from \ud83c\udf10 (open) \u2192 \ud83d\udd12 (locked\/challenging) \u2192 \ud83d\udd13 (unlocked\/authenticated) provides users with immediate visual feedback about the security state of each connection. The key icon (\ud83d\udd11) on the local peer&#8217;s feed indicates that the user has set an authentication requirement.<\/p>\n<p>When a swarm transitions from open to private, the Meeting dApp triggers the authentication flow by calling <code>requestAuthentication(true)<\/code> on the swarm, which iterates through all active connections and initiates the ZKP protocol with each peer:<\/p>\n<pre>\n\/\/ CSwarm.requestAuthentication \u2014 triggers ZKP for all peers\nrequestAuthentication(forceIt = false) {\n  for (let i = 0; i &lt; this.mActiveConnections.length; i++) {\n    if (forceIt || !this.mActiveConnections[i].isAuthenticated) {\n      this.mActiveConnections[i].requestAuthentication(forceIt);\n    }\n  }\n}\n<\/pre>\n<figure> <img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/zero-knowledge-group-authentication-webrtc-swarms-fig-8.svg\" alt=\"Figure 8\" style=\"max-width:100%;height:auto;\" \/><figcaption>Fig. 8 \u2014 The Meeting dApp renders four authentication states as animated icons on each peer&#8217;s video feed: globe (open), lock (challenging), unlock (authenticated), and key (local user has set authentication requirement).<\/figcaption><\/figure>\n<h2>8. Security Analysis<\/h2>\n<p>The protocol&#8217;s security properties can be summarized against standard threat models:<\/p>\n<p><strong>Passive Eavesdropper<\/strong>: Observes (IV\u2081, IV\u2082, ZKP) on the wire. To recover PSK, must compute SHA3-256 pre-image \u2014 computationally infeasible.<\/p>\n<p><strong>Active Man-in-the-Middle<\/strong>: Cannot forge ZKP without PSK. The DTLS encryption layer of WebRTC data channels provides transport-level integrity, but even if bypassed, the attacker cannot produce a valid ZKP without knowing the PSK.<\/p>\n<p><strong>Replay Attack<\/strong>: Captured (IV\u2082, ZKP) pair from a previous session is useless because IV\u2081 is freshly generated each time. Timer\u2081 ensures that IV\u2081 cannot arrive prematurely (preventing prepared-challenge attacks).<\/p>\n<p><strong>Timing Attack<\/strong>: The <code>compareByteVectors()<\/code> comparison is performed on the full 32-byte hash output. While a constant-time comparison would be ideal, the SHA3-256 output provides sufficient entropy that timing side-channels reveal negligible information.<\/p>\n<p><strong>Denial of Service<\/strong>: Rate-limiting is enforced through the <code>mLastTimeOutgressAuthInitTimestamp<\/code> check, which prevents rapid re-initiation of the protocol:<\/p>\n<pre>\nif ((nowMS - this.mLastTimeOutgressAuthInitTimestamp) &lt;\n    (this.mZKPTimer1ExpMS + this.mZKPTimer2ExpMS + this.mClockDrift)) {\n  return false; \/\/ \"it's too early!\"\n}\n<\/pre>\n<h2>9. Relationship to the MDPI Paper<\/h2>\n<p>The implementation described here is the production realization of the protocol formally specified in Skowro\u0144ski (2025). The paper provides the theoretical framework \u2014 defining the designated verifier property, proving resistance to replay and dictionary attacks, and situating the protocol within the broader landscape of decentralized signaling. The code, in turn, addresses the practical challenges that theory alone cannot: browser-specific timing behavior, clock drift across heterogeneous devices, ASN.1 serialization overhead, and the user-experience design of surfacing cryptographic state through visual UI elements.<\/p>\n<p>Together, the paper and the implementation form a complete artifact: a formally described, production-deployed zero-knowledge authentication system for decentralized real-time communication. In an era when privacy is too often an afterthought, bolted on as a marketing feature to fundamentally surveillance-oriented architectures, this is something different. Here, privacy is the foundation. The ZKP protocol is not an optional layer \u2014 it is woven into the fabric of the Swarms API, ensuring that the transition from open to private communication requires nothing more than a single command and a shared secret.<\/p>\n<h2>10. Conclusion \u2014 The Password at the Digital Gate<\/h2>\n<p>We began with the ancient problem of proving belonging. The GRIDNET OS ZKP protocol offers a solution that would satisfy even the most demanding cryptographer: a three-phase, timing-constrained, mutual authentication scheme that proves knowledge of a shared secret without ever revealing it, that resists replay and dictionary attacks through fresh nonce generation and strict timing windows, and that operates entirely peer-to-peer without any trusted third party.<\/p>\n<p>But perhaps the most remarkable thing about this protocol is not its cryptographic sophistication \u2014 it is its simplicity in use. A user types <code>\/setkey mySecretPassword<\/code> into a chat window. The mathematics unfold invisibly. A lock icon turns to an unlock. The conversation continues, now secured by zero-knowledge proof.<\/p>\n<p>In the grand sweep of human communication \u2014 from smoke signals to satellites, from sealed letters to end-to-end encryption \u2014 this represents a quiet but significant advance: the moment when decentralized group authentication became not merely possible, but <em>effortless<\/em>.<\/p>\n<h2>References<\/h2>\n<p>[1] Skowro\u0144ski, R. (2025). &#8220;WebRTC Swarms: Decentralized, Incentivized, and Privacy-Preserving Signaling with Designated Verifier Zero-Knowledge Authentication.&#8221; <em>Future Internet<\/em>, 18(1), 13. <a href=\"https:\/\/doi.org\/10.3390\/fi18010013\">https:\/\/doi.org\/10.3390\/fi18010013<\/a><\/p>\n<p>[2] Bellare, M., Canetti, R., &amp; Krawczyk, H. (1996). &#8220;Keying Hash Functions for Message Authentication.&#8221; <em>Advances in Cryptology \u2014 CRYPTO &#8217;96<\/em>, Lecture Notes in Computer Science, vol. 1109, pp. 1\u201315. Springer. <a href=\"https:\/\/doi.org\/10.1007\/3-540-68697-5_1\">https:\/\/doi.org\/10.1007\/3-540-68697-5_1<\/a><\/p>\n<p>[3] Goldwasser, S., Micali, S., &amp; Rackoff, C. (1989). &#8220;The Knowledge Complexity of Interactive Proof Systems.&#8221; <em>SIAM Journal on Computing<\/em>, 18(1), 186\u2013208. <a href=\"https:\/\/doi.org\/10.1137\/0218012\">https:\/\/doi.org\/10.1137\/0218012<\/a><\/p>\n<p>[4] Jakobsson, M., Sako, K., &amp; Impagliazzo, R. (1996). &#8220;Designated Verifier Proofs and Their Applications.&#8221; <em>Advances in Cryptology \u2014 EUROCRYPT &#8217;96<\/em>, Lecture Notes in Computer Science, vol. 1070, pp. 143\u2013154. Springer. <a href=\"https:\/\/doi.org\/10.1007\/3-540-68339-9_13\">https:\/\/doi.org\/10.1007\/3-540-68339-9_13<\/a><\/p>\n<p>[5] Bertoni, G., Daemen, J., Peeters, M., &amp; Van Assche, G. (2013). &#8220;Keccak.&#8221; <em>Advances in Cryptology \u2014 EUROCRYPT 2013<\/em>, Lecture Notes in Computer Science, vol. 7881. Springer. <a href=\"https:\/\/doi.org\/10.1007\/978-3-642-38348-9_19\">https:\/\/doi.org\/10.1007\/978-3-642-38348-9_19<\/a><\/p>\n<p>[6] Rescorla, E. (2018). &#8220;The Transport Layer Security (TLS) Protocol Version 1.3.&#8221; RFC 8446, IETF. <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc8446\">https:\/\/datatracker.ietf.org\/doc\/html\/rfc8446<\/a><\/p>\n<p>[7] W3C. (2024). &#8220;WebRTC 1.0: Real-Time Communication Between Browsers.&#8221; <a href=\"https:\/\/www.w3.org\/TR\/webrtc\/\">https:\/\/www.w3.org\/TR\/webrtc\/<\/a><\/p>\n<p>[8] NIST. (2015). &#8220;SHA-3 Standard: Permutation-Based Hash and Extendable-Output Functions.&#8221; FIPS 202. <a href=\"https:\/\/doi.org\/10.6028\/NIST.FIPS.202\">https:\/\/doi.org\/10.6028\/NIST.FIPS.202<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In every human society, from the earliest campfire circles to modern boardrooms, there exists a fundamental tension between belonging and proving that&#8230;<\/p>\n","protected":false},"author":1,"featured_media":835946,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[179,162,19],"tags":[200,201,233,142,138,167,234,171,198,199],"class_list":["post-835947","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-decentralization","category-development","category-research","tag-authentication","tag-cryptography","tag-decentralized-identity","tag-gridnet-os","tag-privacy","tag-security","tag-swarms","tag-webrtc","tag-zero-knowledge-proof","tag-zkp"],"_links":{"self":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts\/835947","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/comments?post=835947"}],"version-history":[{"count":2,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts\/835947\/revisions"}],"predecessor-version":[{"id":836064,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts\/835947\/revisions\/836064"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/media\/835946"}],"wp:attachment":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/media?parent=835947"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/categories?post=835947"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/tags?post=835947"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}