Skip to content

Commit 4807e17

Browse files
iot/demo: per-Pramaan — sensor for capture+match only, host owns everything else
Previously the sensor's flash slot was load-bearing: signup wrote the template at slot N, login did a 1:N search across the sensor's library, and the host stored {email, slot}. That capped the demo at 1000 accounts AND meant the sensor was the source of truth for "who's enrolled." This refactor inverts the relationship per the patent's intent. The sensor is reduced to two roles: 1. capture+combine to produce the stable template (signup) 2. 1:1 MATCH between a fresh capture and a host-supplied template (login) The host owns the template, the commitment, and the proof. Capacity is bound by disk, not by sensor flash. sensor.ts - downloadCharacteristic(buffer, data): DOWN_CHAR (0x09) opcode + followup data packets terminated by PID.END_DATA. Chunks the template to the sensor's configured packet size (128 B by default on R307). Used by the login flow to push the host's stored template back into buf2 right before MATCH. - match(): MATCH (0x03) opcode. Returns {score} on CONF.OK, null on CONF.NOT_MATCH. R307 score range 0–300+ at security level 3; well above MATCH_THRESHOLD = 50 for the same finger. crypto.ts - biometricId() now hashes the actual template bytes, not a synthetic slotSeed(slot, pepper). Same Patent-Claim-3 construction, real biometric input. - deriveSignals() takes templateBytes instead of {slot, pepper}. The pepper concept is gone — the template IS the secret. Same Poseidon derivation downstream. bridge.ts - Account schema: drops `slot`; adds `template` (base64 of the 768-byte R307 template). isValidAccount tightened to match. - nextFreeSlot() deleted. - enroll(): capture 1 → lift → capture 2 → combine → upload via UpChar → derive signals from templateBytes → Groth16 prove + verify → persist {email, template, salt, commitment, didHash, identityBinding, did, createdAt}. Sensor flash never written. - authenticate(): capture fresh → imageToCharBuffer(1) → look up account → downloadCharacteristic(2, storedTemplate) → match() → if score ≥ MATCH_THRESHOLD: re-derive (using stored salt for determinism), prove, public-signal-check, verify. Any divergence between recomputed and stored public signals is treated as account tampering (proof_failed). - reset(): no longer emptyDatabase()s the sensor. Host-only. - New Phase events: uploading_template, loading_template, matching. Per the streaming UI's contract; old `storing` + `searching` are retired because neither operation happens anymore. iot/demo/index.html - Hero copy rewritten to describe the new flow. - Sign-up card subtitle: "combines them into a stable template, uploads it to the host, derives a Poseidon commitment…" - Log-in card subtitle: "host pushes the stored template back into the sensor and asks for a 1:1 match…" - New phase handlers — uploading_template, loading_template, matching — all working-mood spinners with explanatory subline. - Accounts table swaps the Slot column for DID (last 12 chars). - Reset button + dialog updated to say "Clear all accounts" (sensor flash isn't being wiped because we never used it). Tested - typecheck clean across crypto.ts, proof.ts, sensor.ts, bridge.ts - bridge boots, preloads Groth16 keys, opens the serial port (when the R307 is connected) - Legacy {email, slot, ...} entries from the prior schema are detected and skipped at load time — operator re-signs up under the new schema
1 parent c4f96cd commit 4807e17

4 files changed

Lines changed: 170 additions & 116 deletions

File tree

iot/demo/index.html

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -280,18 +280,19 @@ <h1>ZeroAuth — Fingerprint demo</h1>
280280
<section class="hero">
281281
<h2>Email + <em>finger</em>. That's it.</h2>
282282
<p>
283-
Type your email, press a button, then follow the on-screen prompts.
284-
The R307 captures the print, the bridge tells you exactly when to
285-
place and lift your finger, and your login is bound to the slot
286-
the sensor stored your template at.
283+
The sensor captures, the host runs everything else. At signup we
284+
upload the stable template, derive a Poseidon commitment, and
285+
generate a Groth16 proof. At login we download the stored template
286+
back into the sensor, ask for a 1:1 match, and re-verify the proof.
287+
Capacity is bound by disk, not by the sensor's 1000-slot flash.
287288
</p>
288289
</section>
289290

290291
<div class="grid">
291292

292293
<div class="card">
293294
<h3>Sign up</h3>
294-
<p>Two captures. The bridge will prompt you to place your finger, lift it, then place it again. Both scans are combined into one on-sensor template.</p>
295+
<p>Two captures. The bridge combines them into a stable template, uploads it to the host, derives a Poseidon commitment, and generates + verifies a Groth16 proof.</p>
295296
<div>
296297
<label for="signup-email">Email</label>
297298
<input id="signup-email" type="email" placeholder="you@example.com" autocomplete="email" />
@@ -311,7 +312,7 @@ <h3>Sign up</h3>
311312

312313
<div class="card">
313314
<h3>Log in</h3>
314-
<p>Single capture. The sensor runs a 1:N match and we accept iff the slot it returns is the one bound to this email.</p>
315+
<p>Single capture. The host pushes the stored template back into the sensor and asks for a 1:1 match; on success it re-derives the commitment and verifies the proof.</p>
315316
<div>
316317
<label for="login-email">Email</label>
317318
<input id="login-email" type="email" placeholder="you@example.com" autocomplete="email" />
@@ -328,15 +329,15 @@ <h3>Log in</h3>
328329
</div>
329330

330331
<div class="accounts">
331-
<h4>Bound accounts <span style="color:#8e8e8e">(in-memory + JSON mirror)</span></h4>
332+
<h4>Bound accounts <span style="color:#8e8e8e">(host-side; sensor flash unused)</span></h4>
332333
<table id="accounts-table">
333334
<thead>
334-
<tr><th>Email</th><th>Slot</th><th>Created</th></tr>
335+
<tr><th>Email</th><th>DID</th><th>Created</th></tr>
335336
</thead>
336337
<tbody><tr><td class="empty" colspan="3">No accounts yet.</td></tr></tbody>
337338
</table>
338339
<div class="reset">
339-
<button class="ghost" id="reset-btn">Wipe sensor + clear accounts</button>
340+
<button class="ghost" id="reset-btn">Clear all accounts</button>
340341
</div>
341342
</div>
342343

@@ -500,12 +501,12 @@ <h4>Bound accounts <span style="color:#8e8e8e">(in-memory + JSON mirror)</span><
500501
detail: '',
501502
});
502503
return;
503-
case 'storing':
504+
case 'uploading_template':
504505
renderState('signup', {
505506
mood: 'working',
506507
icon: 'i-spinner',
507-
line: 'Storing template on the sensor',
508-
sub: 'Almost there',
508+
line: 'Uploading template to host',
509+
sub: 'sensor flash untouched',
509510
detail: '',
510511
});
511512
renderSteps(0, 2);
@@ -593,12 +594,21 @@ <h4>Bound accounts <span style="color:#8e8e8e">(in-memory + JSON mirror)</span><
593594
detail: '',
594595
});
595596
return;
596-
case 'searching':
597+
case 'loading_template':
597598
renderState('login', {
598599
mood: 'working',
599600
icon: 'i-spinner',
600-
line: 'Matching against stored fingerprints',
601-
sub: '1:N search',
601+
line: 'Pushing stored template into the sensor',
602+
sub: 'down_char → buf2',
603+
detail: '',
604+
});
605+
return;
606+
case 'matching':
607+
renderState('login', {
608+
mood: 'working',
609+
icon: 'i-spinner',
610+
line: '1:1 match — fresh capture vs stored template',
611+
sub: evt.score !== undefined ? `score ${evt.score}` : 'sensor side',
602612
detail: '',
603613
});
604614
return;
@@ -643,7 +653,7 @@ <h4>Bound accounts <span style="color:#8e8e8e">(in-memory + JSON mirror)</span><
643653
const explain =
644654
r.reason === 'no_account' ? 'No account bound to this email — sign up first.' :
645655
r.reason === 'no_match' ? 'Sensor did not recognise this finger.' :
646-
r.reason === 'wrong_finger' ? `Matched a different account's slot${r.score ? ` (score ${r.score})` : ''}.` :
656+
r.reason === 'wrong_finger' ? `Sensor refused the 1:1 match against the stored template${r.score !== undefined ? ` (score ${r.score})` : ''}.` :
647657
r.reason === 'proof_failed' ? 'Groth16 verification failed — refusing to authenticate.' :
648658
'Login rejected.';
649659
renderState('login', {
@@ -685,7 +695,7 @@ <h4>Bound accounts <span style="color:#8e8e8e">(in-memory + JSON mirror)</span><
685695
mood: 'error',
686696
icon: 'i-cross',
687697
line: 'This email is already signed up',
688-
sub: `bound to slot ${r.data.slot}`,
698+
sub: r.data.did ? `did ${r.data.did.slice(-8)}` : '',
689699
detail: 'Use the Log in card to sign in with your fingerprint.',
690700
});
691701
$('login-email').value = email;
@@ -721,7 +731,7 @@ <h4>Bound accounts <span style="color:#8e8e8e">(in-memory + JSON mirror)</span><
721731
}
722732

723733
async function handleReset() {
724-
if (!confirm('Wipe ALL templates on the sensor and clear the accounts map?')) return;
734+
if (!confirm('Clear ALL host-stored accounts? The sensor flash isn\'t touched.')) return;
725735
signupBtn.disabled = true;
726736
loginBtn.disabled = true;
727737
resetBtn.disabled = true;
@@ -730,7 +740,7 @@ <h4>Bound accounts <span style="color:#8e8e8e">(in-memory + JSON mirror)</span><
730740
if (!r.ok) throw new Error(`HTTP ${r.status}`);
731741
resetIndicator('signup');
732742
resetIndicator('login');
733-
renderState('signup', { mood: 'success', icon: 'i-check', line: 'Sensor wiped', sub: 'All accounts cleared', detail: '' });
743+
renderState('signup', { mood: 'success', icon: 'i-check', line: 'Accounts cleared', sub: '', detail: '' });
734744
await refreshAccounts();
735745
} catch (err) {
736746
renderState('signup', { mood: 'error', icon: 'i-cross', line: 'Reset failed', sub: '', detail: err.message });
@@ -750,11 +760,11 @@ <h4>Bound accounts <span style="color:#8e8e8e">(in-memory + JSON mirror)</span><
750760
return;
751761
}
752762
tableBody.innerHTML = arr
753-
.sort((a, b) => a.slot - b.slot)
763+
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
754764
.map((a) => `
755765
<tr>
756766
<td>${escapeHtml(a.email)}</td>
757-
<td>${a.slot}</td>
767+
<td>${escapeHtml((a.did || '').slice(-12))}</td>
758768
<td>${new Date(a.createdAt).toLocaleString()}</td>
759769
</tr>
760770
`)

0 commit comments

Comments
 (0)