118 lines
3.4 KiB
JavaScript
118 lines
3.4 KiB
JavaScript
'use strict'
|
|
|
|
// Device identity for the LAN sync: id, pairing code, paired devices. Factory
|
|
// over the config file, one per launcher state.
|
|
|
|
const crypto = require('crypto')
|
|
const { readJson, writeJson } = require('./paths')
|
|
|
|
// The code is read out and typed; the alphabet skips 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
|
|
}
|
|
|
|
// base36 timestamp + random padding: unique across machines.
|
|
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) {
|
|
// Read once, kept in memory: the beacon calls identity() constantly.
|
|
let cached = null
|
|
|
|
function readConfig() {
|
|
if (cached) return cached
|
|
const cfg = readJson(configFile, {})
|
|
cached = cfg && typeof cfg === 'object' ? cfg : {}
|
|
return cached
|
|
}
|
|
|
|
// Id and code are generated on first use and kept forever.
|
|
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
|
|
}
|
|
|
|
// Rotations return the whole identity.
|
|
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 paired device remembers its code; a rejected code is forgotten.
|
|
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, 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 }
|
|
}
|
|
|
|
// Constant-time code comparison.
|
|
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 }
|