SEC_Warehouse/utils/ble/modules/auth.js

313 lines
9.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
}