Files
aiturk-hermes-ide/apps/mobile/src/peer-identity.mjs
T

100 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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()
}
}