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 — each is an attempt to solve the same ancient problem: how does a group verify a newcomer’s right to participate without revealing the secret that grants that right?
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 — where no server exists, where every participant is simultaneously a client and a validator, where the network itself is the only authority — 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?
The answer, implemented in the GRIDNET OS Swarms API and formally described in the MDPI research paper “WebRTC Swarms: Decentralized, Incentivized, and Privacy-Preserving Signaling with Designated Verifier Zero-Knowledge Authentication” (Skowroński, Future Internet, 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.
1. The Mathematical Foundation — SHA3-Keyed MAC as Zero-Knowledge Proof
At its core, the GRIDNET OS ZKP protocol is a designated verifier proof constructed from a keyed hash function. The proof is computed as:
ZKP = SHA3-256( PSK ∥ IV₁ ∥ IV₂ )
Where:
PSK— the 256-bit effective pre-shared key (never transmitted; derived through a two-stage process from the user-provided password — see below)IV₁— a 256-bit random nonce generated by the ValidatorIV₂— a 256-bit random nonce generated by the Candidate
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:
- No replay attacks: Each proof is bound to a unique pair of nonces. Replaying a captured proof against a new challenge will fail because IV₁ is always freshly generated.
- No offline dictionary attacks: An attacker who observes (IV₁, IV₂, ZKP) would need to brute-force the PSK through SHA3-256 — computationally infeasible for strong passwords.
- Designated verifier property: Only the validator who generated IV₁ (and who knows the PSK) can verify the proof. A third party who intercepts the proof cannot verify it without the PSK.
- Mutual freshness: The candidate contributes IV₂, preventing the validator from using a pre-computed challenge to extract information.
2. Credential Issuance and the Pre-Shared Key Lifecycle
In the GRIDNET OS model, the “credential” is a pre-shared key (PSK) that all legitimate swarm participants know. The PSK is not stored in plaintext — 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.
// From CSwarm — setting the pre-shared key
async setPreSharedKey(pass) {
if (gTools.isNull(pass) || pass.length == 0) {
// Make swarm public
armingPrivacy = false;
} else {
armingPrivacy = true;
}
if (armingPrivacy) {
// Transform password to key image
// PSK = SHA3-256(password)
// The raw password is NEVER stored
this.mPasswordImage = sha3_256.arrayBuffer(pass);
}
}
Critical: Two-Stage Key Derivation. The PSK used in ZKP computation is not simply SHA3-256(password). There is a second stage. When getPreSharedKey(true) is called (at authentication time), the system adds a time-based IV derived from the current Unix timestamp:
// From CSwarm.getPreSharedKey(addNonce = true) // Stage 1: mPasswordImage = SHA3-256(password) [stored at setPreSharedKey time] // Stage 2: effective PSK = SHA3-256(mPasswordImage ∥ time_IV) // // The time_IV is a 3-byte slice of the Big-Endian timestamp: let IV = gTools.numberToArrayBuffer( gTools.getTime(), false ).slice(4, 7); // bytes 4-6 of timestamp let dc = new CDataConcatenator(); dc.add(this.mPasswordImage); // 32-byte stored key image dc.add(IV); // 3-byte time slice return sha3_256.arrayBuffer(dc.getData()); // 32-byte effective PSK
This time-dependent derivation has a crucial purpose: it provides an approximately 18-hour drift tolerance 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 — while preventing long-delayed replay attacks from succeeding even if an attacker captures a valid (IV₁, IV₂, ZKP) triple.
The lifecycle of a PSK follows a clear state machine:
- Issuance: A participant issues the
/setkey <password>command in the swarm’s chat. - Propagation: The password must be communicated to other participants through an out-of-band channel (voice, secure message, etc.). The system never transmits it.
- Activation: Upon setting, the swarm’s
authRequirementtransitions toPSK_ZK, and all active connections are immediately challenged to re-authenticate. - Rotation: A new
/setkeycommand with a different password immediately mutes all media streams (await this.mute(true, true, true, true)) and forces re-authentication of all peers — preventing data leakage during the re-auth window. - Revocation:
/setkeywith no arguments clears the PSK, returning the swarm to open mode.
3. The Protocol State Machine — A Detailed Walk-Through
The ZKP protocol operates as a tightly coordinated state machine between two roles: Validator (the peer that initiates the authentication challenge) and Candidate (the peer that must prove knowledge of the PSK). In GRIDNET OS, authentication is mutual — each peer acts as both validator and candidate simultaneously, using separate state machines that run in parallel.
3.1 Phase 1 — Bootstrap (Nonce Exchange)
When a data channel opens on a private swarm, the validator immediately dispatches a Phase 1 authRequestVal message with an empty data payload. This is the “authenticate yourself” challenge:
// Validator initiates (CSwarmConnection.dispatchAuthRequestVal)
dispatchAuthRequestVal(phase = 1) {
if (phase == 1) {
let authMsg = new CSwarmAuthData();
authMsg.isZKP = true;
authMsg.isDedicatedPSK = this.mSwarm.isDedicatedPSK;
authMsg.data = new ArrayBuffer(); // Empty = Phase 1
let msg = new CSwarmMsg(
eSwarmMsgType.authenticationRequestVal,
this.mSwarm.getMyID,
this.getPeerID,
authMsg.getPackedData()
);
let wrapper = new CNetMsg(
this.getProtocolID, eNetReqType.request,
msg.getPackedData()
);
this.send(wrapper.getPackedData(), false);
}
}
The candidate receives this message, generates a 256-bit random nonce (IV₂), and returns it to the validator while starting Timer₁ (10 seconds). The critical security invariant is that the candidate will reject any IV₁ received before Timer₁ expires:
// Candidate responds (CSwarmConnection.processAuthRequestVal)
processAuthRequestVal(authData) {
let nonce = authData.data;
if (gTools.getLength(nonce) == 0) {
// Phase 1: Validator says "authenticate!"
this.genZKPNonce2(); // Generate IV₂ (32 random bytes)
let msg = new CSwarmMsg(
eSwarmMsgType.authenticationRequestCand,
this.mSwarm.getMyID,
this.getPeerID,
this.mZKPNonce2Local // Send IV₂
);
this.startZKPTimer1Local(); // Start 10-second timer
this.send(wrapper.getPackedData(), false);
}
}
3.2 Phase 2 — Challenge Delivery (IV₁)
After receiving IV₂, the validator stores it and schedules the delivery of IV₁ for after Timer₁ expires. This is implemented via setTimeout() — a deliberate choice that leverages the browser’s event loop for timing:
// Validator processes candidate's response
processAuthRequestCand(data) {
if (gTools.getLength(data) != 32) {
this.resetZKPState(false, true);
return false;
}
this.mZKPNonce2Remote = data; // Store IV₂ from candidate
this.startZKPTimer1Remote();
// Schedule IV₁ delivery after Timer₁ expires
setTimeout(function() {
this.dispatchAuthRequestVal(2); // Phase 2
}.bind(this), this.mZKPTimer1ExpMS); // 10,000ms
return true;
}
When Timer₁ expires, the validator generates IV₁ (its own 256-bit nonce) and sends it to the candidate, simultaneously starting Timer₂ (3 seconds) — the window within which the ZKP must arrive:
// Phase 2 dispatch
dispatchAuthRequestVal(phase = 2) {
this.genZKPNonce1(); // Generate IV₁
let lIV1ToBeSent = this.mZKPNonce1Local;
// ... pack and send authRequestVal with IV₁ ...
// SECURITY: Start Timer₂ — ZKP must arrive within 3 seconds
this.startZKPTimer2();
}
The mandatory 10-second delay between Phase 1 and Phase 2 is the protocol’s primary defense against replay attacks. An attacker who captures a valid (IV₂, ZKP) pair from a previous session cannot replay it because: (a) IV₁ is freshly generated, and (b) the candidate’s Timer₁ prevents premature acceptance of an IV₁ that might have been pre-computed.
3.3 Phase 3 — Proof Generation and Verification
Upon receiving IV₁, the candidate first verifies that Timer₁ has expired (the critical anti-replay check), then computes the ZKP:
// Candidate computes ZKP (CSwarmConnection.prepareAndDispatchZKP)
prepareAndDispatchZKP() {
let psk = this.mSwarm.getPreSharedKey(true); // Key image
let IV1 = this.mZKPNonce1Remote; // From validator
let IV2 = this.mZKPNonce2Local; // Generated locally
// Validate: all values must be 32 bytes
if (gTools.getLength(IV1) != 32 ||
gTools.getLength(IV2) != 32 ||
gTools.getLength(psk) != 32) {
this.resetZKPState(true, false);
return false;
}
// Compute ZKP = SHA3-256(PSK ∥ IV₁ ∥ IV₂)
let dc = new CDataConcatenator();
dc.add(psk);
dc.add(IV1);
dc.add(IV2);
let ephemeralZKP = sha3_256.arrayBuffer(dc.getData());
// Send to validator
let msg = new CSwarmMsg(
eSwarmMsgType.zeroKnowledgeProof,
this.mSwarm.getMyID,
this.getPeerID,
ephemeralZKP
);
this.send(wrapper.getPackedData(), false);
return true;
}
The validator receives the ZKP and performs an identical computation using its own copies of IV₁, IV₂, and the PSK. If the computed and received values match, authentication succeeds:
// Validator verifies ZKP (CSwarmConnection.processZKP)
async processZKP(ZKP) {
// TIMING CHECK: reject if Timer₂ has expired
if (this.isTimer2Expired()) {
this.resetZKPState(false, true);
return false;
}
let psk = this.mSwarm.getPreSharedKey(true);
let IV1 = this.mZKPNonce1Local; // Generated locally
let IV2 = this.mZKPNonce2Remote; // From candidate
// Compute expected ZKP
let dc = new CDataConcatenator();
dc.add(psk);
dc.add(IV1);
dc.add(IV2);
let expectedZKP = sha3_256.arrayBuffer(dc.getData());
// Compare
if (gTools.compareByteVectors(expectedZKP, ZKP)) {
this.isAuthenticated = true;
this.notifyAuthenticationSuccess();
} else {
this.isAuthenticated = false;
this.notifyAuthenticationFailure();
}
this.onPeerAuth(result, eSwarmAuthMode.preSharedSecret);
return result;
}
4. Timing Constraints — The Anti-Replay Arsenal
The protocol employs three timing parameters, each serving a distinct security purpose:
// CSwarmConnection constructor — timing constants this.mClockDrift = 2000; // Allowed clock drift (ms) this.mZKPTimer1ExpMS = 10000; // Timer₁: nonce aging window this.mZKPTimer2ExpMS = 3000; // Timer₂: proof delivery window
Timer₁ (10 seconds) — Started by the candidate when IV₂ is sent and by the validator when IV₂ is received. The candidate will reject any IV₁ received before Timer₁ expires. This prevents an attacker from immediately replaying a captured IV₁ — by the time the 10-second window opens, any previously captured proof is stale.
Timer₂ (3 seconds) — Started by the validator when IV₁ is sent. The ZKP must arrive within this window. This prevents slow-path attacks where an attacker captures IV₁ and attempts to brute-force the PSK offline before replaying.
Clock Drift (2 seconds) — An allowance for network propagation delay and clock synchronization differences between peers. The validator’s Timer₁ check includes this tolerance:
isTimer1RemoteExpired() {
let now = gTools.getTime(true);
// Account for clock drift on the remote end
if ((now - this.mZKPTimer1RemoteStartMS) >
(this.mZKPTimer1ExpMS + this.mClockDrift)) {
return true;
}
return false;
}
5. State Machine Reset — Graceful Failure
When any phase of the protocol fails — invalid nonce length, expired timer, incorrect proof — the state machine resets cleanly. The resetZKPState() method is carefully parameterized to reset either the local candidacy state or the local validator state independently, preserving the parallel mutual authentication:
resetZKPState(myCandState = true, myValState = true) {
if (myCandState) {
// Reset local node acting as a candidate
this.ZKPNonce1Remote = new ArrayBuffer(); // Clear validator's IV₁
this.ZKPNonce2Local = new ArrayBuffer(); // Clear own IV₂
this.mZKPTimer1LocalStartMS = 0;
}
if (myValState) {
// Reset local node acting as a validator
this.ZKPNonce1Local = new ArrayBuffer(); // Clear own IV₁
this.ZKPNonce2Remote = new ArrayBuffer(); // Clear candidate's IV₂
this.mZKPTimer1RemoteStartMS = 0;
this.mZKPTimer2LocalStartMS = 0;
}
}
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.
6. The CSwarmAuthData Container
Authentication metadata is encapsulated in CSwarmAuthData, a bitfield-equipped container that carries flags and arbitrary data through the same ASN.1/BER serialization pipeline as all other swarm messages:
export class CSwarmAuthData {
constructor() {
this.mData = new ArrayBuffer();
this.mFlags = new ArrayBuffer(1);
this.mVersion = 1;
}
// Bitfield accessors
set isZKP(isIt) {
let view = new Uint8Array(this.mFlags);
if (isIt) view[0] |= 0b00000001;
else view[0] &= ~0b00000001;
}
set isDedicatedPSK(isIt) {
let view = new Uint8Array(this.mFlags);
if (isIt) view[0] |= 0b00000001;
else view[0] &= ~0b00000001;
}
}
The isDedicatedPSK flag distinguishes between a user-provided password and an automatically derived key (computed from the swarm’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.
7. The Meeting dApp — Authentication in Practice
In the Meeting dApp, authentication events are surfaced through a rich visual UI. Each peer’s video feed includes an authentication state icon that transitions through four states:
setPeerAuthStateInUI(peerID, authState) {
switch (authState) {
case 0: // No auth required (open swarm)
icon.classList.add('fa-globe', 'authNone');
break;
case 1: // Auth required but not yet verified
icon.classList.add('fa-lock', 'authLocked');
audioControl.muted = true; // Mute audio (prevent stub beeping)
break;
case 2: // Successfully authenticated
icon.classList.add('fa-lock-open', 'authUnlocked');
if (shouldAudioBeActive) audioControl.muted = false;
break;
case 3: // Local peer has set auth requirement (key icon)
icon.classList.add('fa-key', 'authKey');
break;
}
}
The progression from 🌐 (open) → 🔒 (locked/challenging) → 🔓 (unlocked/authenticated) provides users with immediate visual feedback about the security state of each connection. The key icon (🔑) on the local peer’s feed indicates that the user has set an authentication requirement.
When a swarm transitions from open to private, the Meeting dApp triggers the authentication flow by calling requestAuthentication(true) on the swarm, which iterates through all active connections and initiates the ZKP protocol with each peer:
// CSwarm.requestAuthentication — triggers ZKP for all peers
requestAuthentication(forceIt = false) {
for (let i = 0; i < this.mActiveConnections.length; i++) {
if (forceIt || !this.mActiveConnections[i].isAuthenticated) {
this.mActiveConnections[i].requestAuthentication(forceIt);
}
}
}
8. Security Analysis
The protocol’s security properties can be summarized against standard threat models:
Passive Eavesdropper: Observes (IV₁, IV₂, ZKP) on the wire. To recover PSK, must compute SHA3-256 pre-image — computationally infeasible.
Active Man-in-the-Middle: 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.
Replay Attack: Captured (IV₂, ZKP) pair from a previous session is useless because IV₁ is freshly generated each time. Timer₁ ensures that IV₁ cannot arrive prematurely (preventing prepared-challenge attacks).
Timing Attack: The compareByteVectors() 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.
Denial of Service: Rate-limiting is enforced through the mLastTimeOutgressAuthInitTimestamp check, which prevents rapid re-initiation of the protocol:
if ((nowMS - this.mLastTimeOutgressAuthInitTimestamp) <
(this.mZKPTimer1ExpMS + this.mZKPTimer2ExpMS + this.mClockDrift)) {
return false; // "it's too early!"
}
9. Relationship to the MDPI Paper
The implementation described here is the production realization of the protocol formally specified in Skowroński (2025). The paper provides the theoretical framework — 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.
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 — 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.
10. Conclusion — The Password at the Digital Gate
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.
But perhaps the most remarkable thing about this protocol is not its cryptographic sophistication — it is its simplicity in use. A user types /setkey mySecretPassword into a chat window. The mathematics unfold invisibly. A lock icon turns to an unlock. The conversation continues, now secured by zero-knowledge proof.
In the grand sweep of human communication — from smoke signals to satellites, from sealed letters to end-to-end encryption — this represents a quiet but significant advance: the moment when decentralized group authentication became not merely possible, but effortless.
References
[1] Skowroński, R. (2025). “WebRTC Swarms: Decentralized, Incentivized, and Privacy-Preserving Signaling with Designated Verifier Zero-Knowledge Authentication.” Future Internet, 18(1), 13. https://doi.org/10.3390/fi18010013
[2] Bellare, M., Canetti, R., & Krawczyk, H. (1996). “Keying Hash Functions for Message Authentication.” Advances in Cryptology — CRYPTO ’96, Lecture Notes in Computer Science, vol. 1109, pp. 1–15. Springer. https://doi.org/10.1007/3-540-68697-5_1
[3] Goldwasser, S., Micali, S., & Rackoff, C. (1989). “The Knowledge Complexity of Interactive Proof Systems.” SIAM Journal on Computing, 18(1), 186–208. https://doi.org/10.1137/0218012
[4] Jakobsson, M., Sako, K., & Impagliazzo, R. (1996). “Designated Verifier Proofs and Their Applications.” Advances in Cryptology — EUROCRYPT ’96, Lecture Notes in Computer Science, vol. 1070, pp. 143–154. Springer. https://doi.org/10.1007/3-540-68339-9_13
[5] Bertoni, G., Daemen, J., Peeters, M., & Van Assche, G. (2013). “Keccak.” Advances in Cryptology — EUROCRYPT 2013, Lecture Notes in Computer Science, vol. 7881. Springer. https://doi.org/10.1007/978-3-642-38348-9_19
[6] Rescorla, E. (2018). “The Transport Layer Security (TLS) Protocol Version 1.3.” RFC 8446, IETF. https://datatracker.ietf.org/doc/html/rfc8446
[7] W3C. (2024). “WebRTC 1.0: Real-Time Communication Between Browsers.” https://www.w3.org/TR/webrtc/
[8] NIST. (2015). “SHA-3 Standard: Permutation-Based Hash and Extendable-Output Functions.” FIPS 202. https://doi.org/10.6028/NIST.FIPS.202


