313 lines
9.2 KiB
JavaScript
313 lines
9.2 KiB
JavaScript
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,
|
||
};
|
||
}
|