// Standard WebRTC identities; private key material stays inside RTCCertificate. // Pairing must persist the public fingerprint separately in its trusted record. const DATABASE = 'aiturk-mobile-peer-identities-v1' const STORE = 'certificates' const failure = () => new Error('Cihaz kimliği doğrulanamadı. Mevcut eşleşmeyi kullanmayın; yeniden açık eşleştirme gerekli.') function deviceKey(deviceId) { if (typeof deviceId !== 'string' || !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(deviceId)) throw failure() return deviceId.toLowerCase() } export function normalizeFingerprint(value) { if (typeof value !== 'string' || !/^(?:[0-9a-f]{2}:){31}[0-9a-f]{2}$/i.test(value)) throw failure() return value.toUpperCase() } export function parsePhoneIdentity(raw, pairingId) { if (typeof raw !== 'string' || raw.length > 700) throw failure() let value try { value = JSON.parse(raw) } catch { throw failure() } if (value?.kind !== 'aiturk-phone-identity' || value.version !== 1 || value.pairingId !== pairingId) throw failure() return { deviceId: deviceKey(value.deviceId), fingerprint: normalizeFingerprint(value.fingerprint) } } function identity(certificate) { if (!(certificate instanceof RTCCertificate) || !Number.isFinite(certificate.expires) || certificate.expires <= Date.now()) throw failure() const fingerprints = certificate.getFingerprints().filter(item => item.algorithm === 'sha-256') if (fingerprints.length !== 1) throw failure() return { certificate, fingerprint: normalizeFingerprint(fingerprints[0].value), expires: certificate.expires } } async function database() { return new Promise((resolve, reject) => { let blocked = false const request = indexedDB.open(DATABASE, 1) request.onupgradeneeded = () => request.result.createObjectStore(STORE) request.onerror = () => reject(failure()) request.onblocked = () => { blocked = true; reject(failure()) } request.onsuccess = () => { if (blocked) { request.result.close(); return } request.result.onversionchange = () => request.result.close() resolve(request.result) } }) } // Call only during an explicit NEW device pairing, never to repair a missing key. // The read/write transaction chooses one winner across simultaneous windows. export async function createDeviceIdentity(deviceId) { const key = deviceKey(deviceId) const candidate = await RTCPeerConnection.generateCertificate({ name: 'ECDSA', namedCurve: 'P-256' }) const db = await database() try { return await new Promise((resolve, reject) => { const tx = db.transaction(STORE, 'readwrite'), store = tx.objectStore(STORE) let selected const read = store.get(key) read.onsuccess = () => { try { selected = identity(read.result === undefined ? candidate : read.result) if (read.result === undefined) store.add(candidate, key) } catch { tx.abort() } } tx.oncomplete = () => resolve(selected) tx.onabort = tx.onerror = () => reject(failure()) }) } finally { db.close() } } // Never generates or replaces a certificate. A lost key or changed public pin // requires a new explicit pairing, not trust-on-first-use during reconnect. export async function loadDeviceIdentity(deviceId, expectedFingerprint) { const key = deviceKey(deviceId), expected = normalizeFingerprint(expectedFingerprint) const db = await database() try { const certificate = await new Promise((resolve, reject) => { const tx = db.transaction(STORE), request = tx.objectStore(STORE).get(key) tx.oncomplete = () => resolve(request.result) tx.onabort = tx.onerror = () => reject(failure()) }) const result = identity(certificate) if (result.fingerprint !== expected) throw failure() return result } finally { db.close() } } // Application data channels use a single bundled DTLS identity. Check every // advertised fingerprint before setRemoteDescription; native DTLS then verifies // that the actual peer possesses the matching private key. export function assertRemoteIdentity(sdp, trustedFingerprint) { const expected = normalizeFingerprint(trustedFingerprint) if (typeof sdp !== 'string' || sdp.length > 131072) throw failure() const lines = sdp.split(/\r?\n/).filter(line => line.startsWith('a=fingerprint:')) if (!lines.length) throw failure() for (const line of lines) { const match = /^a=fingerprint:sha-256 ((?:[0-9a-f]{2}:){31}[0-9a-f]{2})$/i.exec(line) if (!match || normalizeFingerprint(match[1]) !== expected) throw failure() } }