import { writeA7Payload } from '../core/packet'; import { waitForProtocolState } from '../core/stateWait'; import { parseDeviceInfo } from '../parsers'; import { BLE_STAGE } from '../core/state'; /** 生成指定长度的随机密钥(排除易混淆字符 0/O/1/I/l)。 */ 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) {} }; /** 将 MAC 地址去除非十六进制字符并转大写,用于密钥拼接。 */ 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}`; } /** * 将密钥通过A7分包写入 UUID_KEY 特征值。 * @param {object} ble createBleCore 实例 * @param {string} keyText 密钥文本 * @returns {string} 实际写入的密钥值 */ 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; } /** * 写入密钥并等待 UUID_STATE 返回预期状态码。 * 成功后自动设置 AUTHORIZED 阶段和 keyMatched 状态。 * @param {object} ble createBleCore 实例 * @param {string} keyText 密钥文本 * @param {number[]} expectedStateCodes 期望的状态码列表 * @param {object} options 超时等配置 */ async function writeKeyAndExpectStates( ble, keyText, expectedStateCodes, options = {}, ) { // 设备订阅 STATE 后可能先发送 -1(未写密钥)/0(空闲) 等中间态, // 若 rejectOnUnexpected:true 会导致中间态直接 reject,阻断鉴权流程。 const waitPromise = waitForProtocolState(ble, { allowedCodes: expectedStateCodes, rejectOnUnexpected: false, 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); try { 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 }; } catch (error) { // 鉴权超时多为黑盒:密钥不匹配(STATE=8)、F301 订阅失败、或写入丢失均表现为超时。 // 这里带上绑定状态与设备最后上报的 STATE,区分根因方向。 const authState = ble.getState().auth || {}; const lastState = authState.lastStateCode; const bound = authState.bound; const expectedStr = expectedStateCodes.join('/'); const boundHint = (bound === null || bound === undefined) ? '绑定状态未知' : `绑定状态=${bound}`; const lastHint = (lastState === null || lastState === undefined) ? '设备未上报任何 STATE(可能 F301 订阅失败或密钥写入丢失)' : `设备最后上报 STATE=${lastState}(期望 ${expectedStr},可能密钥不匹配)`; throw new Error(`密钥鉴权超时(${boundHint},${lastHint})`); } } /** * 根据设备绑定状态执行鉴权流程: * - bindStatus=0(未绑定):生成随机密钥写入,等待 STATE=5 * - bindStatus=1(已绑定):使用已知密钥匹配,等待 STATE=7 * - 其他状态:尝试用提供的密钥匹配,等待 STATE=7 或 5 */ async function ensureAuthorizedWithDeviceInfo( ble, info, keyText, options = {}, ) { const bindStatus = info.bindStatus !== null ? Number(info.bindStatus) : null; 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, }; } /** * 认证模块工厂:封装设备绑定/密钥匹配全流程。 * @param {object} ble createBleCore 实例 */ export function createAuthModule(ble) { /** * 读取 UUID_MSG 获取设备信息(MAC/绑定状态/产品ID等)。 * 先订阅 protocol:msg 事件,再触发 read,保证不丢通知。 */ async function readDeviceInfo(options = {}) { const { msg } = ble.getState().characteristics; if (!msg || !msg.uuid) { throw new Error('未发现 UUID_MSG 特征值'); } const timeout = Number(options.msgTimeout) || 4000; let timer = null; let off = null; const valuePromise = new Promise((resolve, reject) => { const finish = (callback) => { if (timer) clearTimeout(timer); if (off) off(); callback(); }; off = ble.on('protocol:msg', (payload) => finish(() => resolve(payload))); timer = setTimeout( () => finish(() => reject(new Error('读取 UUID_MSG 超时'))), timeout, ); }); try { await ble.readCharacteristic(msg.uuid); const payload = await valuePromise; const info = parseDeviceInfo(payload.value, ble.getProfile().id); // 打印 MSG 原始字节与解析结果,验证 bindStatus/MAC 是否解析正确 try { const rawBytes = Array.from(payload.value || []); const hex = rawBytes .map((b) => b.toString(16).padStart(2, '0')) .join(' '); console.debug('[BLE][AUTH]', 'MSG raw hex=', hex, 'parsed=', JSON.stringify(info)); } catch (e) { } return info; } catch (error) { if (timer) clearTimeout(timer); if (off) off(); throw error; } } /** * 严格遵循协议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(options); return ensureAuthorizedWithDeviceInfo(ble, info, keyText, options); } /** * 固定规则鉴权:密钥 = 设备MAC(去分隔符) + “0000”。 * 用于”先鉴权再进行 WiFi 配置”的页面流程。 */ async function ensureAuthorizedByMac(options = {}) { ble.setStage(BLE_STAGE.AUTHORIZING); const info = await readDeviceInfo(options); 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, }; }