198 lines
5.7 KiB
JavaScript
198 lines
5.7 KiB
JavaScript
import { writeA7Payload } from '../core/packet'
|
||
import { waitForProtocolState } from '../core/stateWait'
|
||
import { parseDeviceInfo } from '../parsers'
|
||
import { BLE_STAGE } from '../core/state'
|
||
|
||
function generateRandomKey(length = 16) {
|
||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'
|
||
let result = ''
|
||
|
||
for (let index = 0; index < length; index += 1) {
|
||
const random = Math.floor(Math.random() * chars.length)
|
||
result += chars[random]
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
const authLog = (...args) => { try { console.debug('[BLE][AUTH]', ...args) } catch (e) {} }
|
||
|
||
function normalizeMacForKey(macText = '') {
|
||
return String(macText || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
|
||
}
|
||
|
||
/**
|
||
* 按协议约定生成设备密钥:MAC(去分隔符) + 固定后缀。
|
||
* 默认后缀为 "0000",对应 12 位 MAC + 4 位后缀 = 16 位密钥。
|
||
*/
|
||
export function buildKeyFromMac(macText, suffix = '0000', expectedLength = 16) {
|
||
const normalizedMac = normalizeMacForKey(macText)
|
||
const normalizedSuffix = String(suffix || '').trim()
|
||
|
||
if (!normalizedSuffix) {
|
||
throw new Error('密钥后缀不能为空')
|
||
}
|
||
|
||
const requiredMacLength = expectedLength - normalizedSuffix.length
|
||
if (requiredMacLength <= 0) {
|
||
throw new Error('密钥长度配置异常')
|
||
}
|
||
|
||
if (normalizedMac.length !== requiredMacLength) {
|
||
throw new Error(`设备 MAC 长度异常,期望 ${requiredMacLength} 位,实际 ${normalizedMac.length} 位`)
|
||
}
|
||
|
||
return `${normalizedMac}${normalizedSuffix}`
|
||
}
|
||
|
||
async function writePacketsToKeyCharacteristic(ble, keyText) {
|
||
const { key } = ble.getState().characteristics
|
||
|
||
if (!key || !key.uuid) {
|
||
throw new Error('未发现 UUID_KEY 特征值')
|
||
}
|
||
|
||
const profile = ble.getProfile()
|
||
const keyValue = String(keyText || '').trim()
|
||
|
||
if (keyValue.length !== profile.auth.keyLength) {
|
||
throw new Error(`密钥长度必须为 ${profile.auth.keyLength}`)
|
||
}
|
||
|
||
await writeA7Payload(ble, key.uuid, keyValue, profile.packet)
|
||
|
||
return keyValue
|
||
}
|
||
|
||
async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options = {}) {
|
||
const waitPromise = waitForProtocolState(ble, {
|
||
allowedCodes: expectedStateCodes,
|
||
rejectOnUnexpected: true,
|
||
timeout: options.timeout || 6000,
|
||
timeoutMessage: '等待 UUID_STATE 超时',
|
||
unexpectedMessage: '密钥流程状态异常'
|
||
})
|
||
authLog('STATE wait before writeKey, expect=', expectedStateCodes)
|
||
const keyValue = await writePacketsToKeyCharacteristic(ble, keyText)
|
||
authLog('key written, length=', keyValue)
|
||
const stateCode = Number((await waitPromise).stateCode)
|
||
authLog('STATE received for KEY=', stateCode)
|
||
ble.setStage(BLE_STAGE.AUTHORIZED)
|
||
ble.setAuthState({ keyMatched: true, lastStateCode: stateCode })
|
||
return { success: true, stateCode, key: keyValue }
|
||
}
|
||
|
||
async function ensureAuthorizedWithDeviceInfo(ble, info, keyText, options = {}) {
|
||
const bindStatus = Number(info.bindStatus)
|
||
const profile = ble.getProfile()
|
||
|
||
ble.setAuthState({
|
||
bound: Number.isNaN(bindStatus) ? null : bindStatus,
|
||
keyMatched: false
|
||
})
|
||
|
||
if (bindStatus === 0) {
|
||
const keyToBind = String(keyText || '').trim() || generateRandomKey(profile.auth.keyLength)
|
||
const authResult = await writeKeyAndExpectStates(ble, keyToBind, [5], options)
|
||
|
||
return {
|
||
mode: 'BIND',
|
||
deviceInfo: info,
|
||
authResult,
|
||
key: keyToBind
|
||
}
|
||
}
|
||
|
||
if (bindStatus === 1) {
|
||
const keyToMatch = String(keyText || '').trim()
|
||
if (!keyToMatch) {
|
||
throw new Error('设备已绑定,必须提供已绑定密钥用于匹配')
|
||
}
|
||
|
||
const authResult = await writeKeyAndExpectStates(ble, keyToMatch, [7], options)
|
||
|
||
return {
|
||
mode: 'MATCH',
|
||
deviceInfo: info,
|
||
authResult,
|
||
key: keyToMatch
|
||
}
|
||
}
|
||
|
||
const fallbackKey = String(keyText || '').trim()
|
||
if (!fallbackKey) {
|
||
throw new Error(`无法识别的绑定状态: ${info.bindStatus},且未提供密钥`)
|
||
}
|
||
|
||
const fallbackResult = await writeKeyAndExpectStates(ble, fallbackKey, [7, 5], options)
|
||
|
||
return {
|
||
mode: 'UNKNOWN_BIND_STATE_FALLBACK',
|
||
deviceInfo: info,
|
||
authResult: fallbackResult,
|
||
key: fallbackKey
|
||
}
|
||
}
|
||
|
||
export function createAuthModule(ble) {
|
||
async function readDeviceInfo() {
|
||
const { msg } = ble.getState().characteristics
|
||
|
||
if (!msg || !msg.uuid) {
|
||
throw new Error('未发现 UUID_MSG 特征值')
|
||
}
|
||
|
||
const valuePromise = new Promise((resolve) => {
|
||
const off = ble.on('protocol:msg', (payload) => {
|
||
off()
|
||
resolve(payload)
|
||
})
|
||
})
|
||
|
||
await ble.readCharacteristic(msg.uuid)
|
||
const payload = await valuePromise
|
||
|
||
return parseDeviceInfo(payload.value)
|
||
}
|
||
|
||
/**
|
||
* 严格遵循协议1.2绑定流程:
|
||
* 1) 读取 UUID_MSG 的 bindStatus;
|
||
* 2) bindStatus=0 写随机/指定密钥并等待 STATE=5;
|
||
* 3) bindStatus=1 写已知密钥并等待 STATE=7。
|
||
*/
|
||
async function ensureAuthorized(keyText, options = {}) {
|
||
ble.setStage(BLE_STAGE.AUTHORIZING)
|
||
const info = await readDeviceInfo()
|
||
return ensureAuthorizedWithDeviceInfo(ble, info, keyText, options)
|
||
}
|
||
|
||
/**
|
||
* 固定规则鉴权:密钥 = 设备MAC(去分隔符) + "0000"。
|
||
* 用于“先鉴权再进行 WiFi 配置”的页面流程。
|
||
*/
|
||
async function ensureAuthorizedByMac(options = {}) {
|
||
ble.setStage(BLE_STAGE.AUTHORIZING)
|
||
|
||
const info = await readDeviceInfo()
|
||
const profile = ble.getProfile()
|
||
const keyByMac = buildKeyFromMac(info.mac, options.suffix || '0000', profile.auth.keyLength)
|
||
|
||
const authResult = await ensureAuthorizedWithDeviceInfo(ble, info, keyByMac, options)
|
||
|
||
return {
|
||
...authResult,
|
||
key: keyByMac,
|
||
keyRule: 'MAC+0000'
|
||
}
|
||
}
|
||
|
||
return {
|
||
readDeviceInfo,
|
||
ensureAuthorized,
|
||
ensureAuthorizedByMac,
|
||
buildKeyFromMac,
|
||
generateRandomKey
|
||
}
|
||
}
|