165 lines
No EOL
3.7 KiB
JavaScript
165 lines
No EOL
3.7 KiB
JavaScript
/**
|
||
* 加密工具函数
|
||
* 为微信小程序环境优化的加密工具集
|
||
*/
|
||
|
||
const { decode: base32Decode } = require('./base32.js');
|
||
|
||
/**
|
||
* 常量时间比较两个字符串
|
||
* 防止时间侧信道攻击
|
||
*
|
||
* @param {string} a - 第一个字符串
|
||
* @param {string} b - 第二个字符串
|
||
* @returns {boolean} 是否相等
|
||
*/
|
||
function constantTimeEqual(a, b) {
|
||
if (typeof a !== 'string' || typeof b !== 'string') {
|
||
return false;
|
||
}
|
||
|
||
if (a.length !== b.length) {
|
||
return false;
|
||
}
|
||
|
||
let result = 0;
|
||
for (let i = 0; i < a.length; i++) {
|
||
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
||
}
|
||
return result === 0;
|
||
}
|
||
|
||
/**
|
||
* 安全的整数解析
|
||
* 带有范围检查
|
||
*
|
||
* @param {number|string} value - 要解析的值
|
||
* @param {number} min - 最小值(包含)
|
||
* @param {number} max - 最大值(包含)
|
||
* @returns {number|null} 解析后的整数,如果无效则返回null
|
||
*/
|
||
function safeIntegerParse(value, min, max) {
|
||
let num;
|
||
|
||
if (typeof value === 'number') {
|
||
num = value;
|
||
} else if (typeof value === 'string') {
|
||
num = parseInt(value, 10);
|
||
} else {
|
||
return null;
|
||
}
|
||
|
||
if (!Number.isInteger(num)) {
|
||
return null;
|
||
}
|
||
|
||
if (num < min || num > max) {
|
||
return null;
|
||
}
|
||
|
||
return num;
|
||
}
|
||
|
||
/**
|
||
* 生成密码学安全的随机字节
|
||
*
|
||
* @param {number} length - 需要的字节数
|
||
* @returns {Uint8Array} 随机字节数组
|
||
*/
|
||
function getRandomBytes(length) {
|
||
if (!Number.isInteger(length) || length <= 0) {
|
||
throw new Error('Length must be a positive integer');
|
||
}
|
||
|
||
// 优先使用微信小程序的随机数API
|
||
if (typeof wx !== 'undefined' && wx.getRandomValues) {
|
||
const array = new Uint8Array(length);
|
||
wx.getRandomValues({
|
||
length: length,
|
||
success: (res) => {
|
||
array.set(new Uint8Array(res.randomValues));
|
||
},
|
||
fail: () => {
|
||
// 如果原生API失败,回退到Math.random
|
||
for (let i = 0; i < length; i++) {
|
||
array[i] = Math.floor(Math.random() * 256);
|
||
}
|
||
}
|
||
});
|
||
return array;
|
||
}
|
||
|
||
// 回退到Math.random(不够安全,但作为降级方案)
|
||
const array = new Uint8Array(length);
|
||
for (let i = 0; i < length; i++) {
|
||
array[i] = Math.floor(Math.random() * 256);
|
||
}
|
||
return array;
|
||
}
|
||
|
||
/**
|
||
* 生成指定长度的随机Base32密钥
|
||
*
|
||
* @param {number} length - 密钥长度(字节)
|
||
* @returns {string} Base32编码的密钥
|
||
*/
|
||
function generateSecretKey(length = 20) {
|
||
if (!Number.isInteger(length) || length < 16 || length > 64) {
|
||
throw new Error('Key length must be between 16 and 64 bytes');
|
||
}
|
||
|
||
const bytes = getRandomBytes(length);
|
||
return require('./base32.js').encode(bytes);
|
||
}
|
||
|
||
/**
|
||
* 验证Base32密钥的有效性和强度
|
||
*
|
||
* @param {string} key - Base32编码的密钥
|
||
* @returns {boolean} 密钥是否有效且足够强
|
||
*/
|
||
function validateBase32Secret(key) {
|
||
try {
|
||
// 检查是否是有效的Base32
|
||
if (!require('./base32.js').isValid(key)) {
|
||
return false;
|
||
}
|
||
|
||
// 解码密钥
|
||
const decoded = base32Decode(key);
|
||
|
||
// 检查最小长度(128位/16字节)
|
||
if (decoded.length < 16) {
|
||
return false;
|
||
}
|
||
|
||
// 检查是否全为0或1(弱密钥)
|
||
let allZeros = true;
|
||
let allOnes = true;
|
||
|
||
for (const byte of decoded) {
|
||
if (byte !== 0) allZeros = false;
|
||
if (byte !== 255) allOnes = false;
|
||
if (!allZeros && !allOnes) break;
|
||
}
|
||
|
||
if (allZeros || allOnes) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
const { deriveKeyPBKDF2 } = require('./hmac.js');
|
||
|
||
module.exports = {
|
||
constantTimeEqual,
|
||
safeIntegerParse,
|
||
getRandomBytes,
|
||
generateSecretKey,
|
||
validateBase32Secret,
|
||
deriveKeyPBKDF2
|
||
}; |