100 lines
4.5 KiB
JavaScript
100 lines
4.5 KiB
JavaScript
// 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()
|
||
}
|
||
}
|