'use strict' // Device identity for the LAN sync: the id this install is known by, the code // other devices must type to pair with it, and the list of paired devices. // Exported as a factory over the config file, so every launcher state (and // every test device) gets its own identity. const crypto = require('crypto') const { readJson, writeJson } = require('./paths') // The sync code is read out on one machine and typed on the other, so the // alphabet skips the characters people mix up (0/O, 1/I/L). const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' const ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789' function randomChars(alphabet, count) { let out = '' while (out.length < count) { for (const byte of crypto.randomBytes(count)) { if (out.length >= count) break out += alphabet[byte % alphabet.length] } } return out } // 24 characters: the current timestamp in base36 (unique per millisecond, // 8 chars until 2089) plus random padding, so two machines generating an id // within the same millisecond still get different ids. function makeDeviceId() { const stamp = Date.now().toString(36) return (stamp + randomChars(ID_ALPHABET, Math.max(0, 24 - stamp.length))).slice(0, 24) } function makeSyncCode() { return randomChars(CODE_ALPHABET, 8) } function createIdentity(configFile) { // The identity file is read once and then kept in memory: the beacon calls // identity() every couple of seconds (and every pairing call in between), // and a synchronous read per call is pure overhead. Nothing else writes // this file (the ipc handlers all go through these functions), so the cache // cannot go stale. let cached = null function readConfig() { if (cached) return cached const cfg = readJson(configFile, {}) cached = cfg && typeof cfg === 'object' ? cfg : {} return cached } // The id and the code are generated on FIRST use and then kept forever: // other devices remember the id (they pair with it) and the code (so it // only has to be typed once). function identity() { const cfg = readConfig() let changed = false if (typeof cfg.deviceId !== 'string' || cfg.deviceId.length !== 24) { cfg.deviceId = makeDeviceId() changed = true } if (typeof cfg.syncCode !== 'string' || cfg.syncCode.length !== 8) { cfg.syncCode = makeSyncCode() changed = true } if (!cfg.known || typeof cfg.known !== 'object') { cfg.known = {} changed = true } if (changed) writeJson(configFile, cfg) return cfg } // Both rotations return the whole identity ({ deviceId, syncCode, known }), // so callers get a consistent view of this device after the change. function rotateDeviceId() { const cfg = identity() const previous = cfg.deviceId cfg.deviceId = makeDeviceId() writeJson(configFile, cfg) if (cfg.deviceId === previous) return rotateDeviceId() return cfg } function rotateSyncCode() { const cfg = identity() const previous = cfg.syncCode cfg.syncCode = makeSyncCode() writeJson(configFile, cfg) if (cfg.syncCode === previous) return rotateSyncCode() // never a no-op rotation return cfg } // A device the user synced with successfully: its code is stored next to // its id, so the next sync needs no typing. A rejected code is forgotten // again, which is what drives the "type the code again" flow. function knownDevice(id) { return identity().known[id] || null } function knownPeers() { return identity().known || {} } function rememberDevice(id, code, name) { const cfg = identity() const stored = String(code || '').trim().toUpperCase() cfg.known = { ...cfg.known, [id]: { code: stored, syncCode: stored, name: name || '', at: Date.now() } } writeJson(configFile, cfg) } function forgetDevice(id) { const cfg = identity() const known = { ...cfg.known } delete known[id] cfg.known = known writeJson(configFile, cfg) } return { identity, rotateDeviceId, rotateSyncCode, knownDevice, knownPeers, rememberDevice, forgetDevice, makeDeviceId, makeSyncCode } } // Compare two typed codes without leaking timing: the codes are short, but a // device may only verify once per two seconds anyway. function sameCode(a, b) { const x = Buffer.from(String(a == null ? '' : a).trim().toUpperCase()) const y = Buffer.from(String(b == null ? '' : b).trim().toUpperCase()) return x.length === y.length && x.length > 0 && crypto.timingSafeEqual(x, y) } module.exports = { createIdentity, sameCode, makeDeviceId, makeSyncCode }