283 lines
No EOL
8.4 KiB
JavaScript
283 lines
No EOL
8.4 KiB
JavaScript
/**
|
||
* HMAC(Hash-based Message Authentication Code)实现
|
||
* 优先使用微信小程序原生API,如果不可用则使用sjcl库作为备用实现
|
||
*
|
||
* 依赖:
|
||
* - sjcl: Stanford JavaScript Crypto Library
|
||
*/
|
||
|
||
import sjcl from './sjcl.min.js';
|
||
|
||
// 兼容 sjcl.codec.bytes
|
||
if (!sjcl.codec.bytes) {
|
||
sjcl.codec.bytes = {
|
||
fromBits: function (arr) {
|
||
var out = [], bl = sjcl.bitArray.bitLength(arr), i, tmp;
|
||
for (i = 0; i < bl / 8; i++) {
|
||
if ((i & 3) === 0) {
|
||
tmp = arr[i / 4] || 0;
|
||
}
|
||
out.push(tmp >>> 24);
|
||
tmp <<= 8;
|
||
}
|
||
return out;
|
||
},
|
||
toBits: function (bytes) {
|
||
var out = [], i, tmp = 0;
|
||
for (i = 0; i < bytes.length; i++) {
|
||
tmp = tmp << 8 | bytes[i];
|
||
if ((i & 3) === 3) {
|
||
out.push(tmp);
|
||
tmp = 0;
|
||
}
|
||
}
|
||
if (i & 3) {
|
||
out.push(sjcl.bitArray.partial(8 * (i & 3), tmp));
|
||
}
|
||
return out;
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 生成 HMAC-SHA1/SHA256/SHA512 值
|
||
*
|
||
* @param {string} algorithm - 哈希算法 ('sha1', 'sha256', 'sha512')
|
||
* @param {string|Uint8Array} key - 密钥
|
||
* @param {string|Uint8Array} data - 数据
|
||
* @returns {Promise<Uint8Array>} HMAC 结果
|
||
* @throws {Error} 如果算法不支持或参数错误
|
||
*/
|
||
function hmac(algorithm, key, data) {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
// 增强参数校验
|
||
if (!algorithm || typeof algorithm !== 'string') {
|
||
throw new Error('Algorithm must be a non-empty string');
|
||
}
|
||
|
||
const normalizedAlgorithm = algorithm.toLowerCase();
|
||
if (!['sha1', 'sha256', 'sha512'].includes(normalizedAlgorithm)) {
|
||
throw new Error(`Unsupported algorithm: ${algorithm}. Supported: sha1, sha256, sha512`);
|
||
}
|
||
|
||
if (!key || (typeof key !== 'string' && !(key instanceof Uint8Array))) {
|
||
throw new Error('Key must be a non-empty string or Uint8Array');
|
||
}
|
||
|
||
if (!data || (typeof data !== 'string' && !(data instanceof Uint8Array))) {
|
||
throw new Error('Data must be a non-empty string or Uint8Array');
|
||
}
|
||
|
||
// 优先使用微信小程序原生接口
|
||
if (typeof wx !== 'undefined' && wx.createHMAC) {
|
||
try {
|
||
const crypto = wx.createHMAC(normalizedAlgorithm);
|
||
|
||
// 改进数据格式转换
|
||
const keyStr = typeof key === 'string' ? key :
|
||
new TextDecoder().decode(key);
|
||
const dataStr = typeof data === 'string' ? data :
|
||
new TextDecoder().decode(data);
|
||
|
||
crypto.update(keyStr);
|
||
crypto.update(dataStr);
|
||
|
||
const result = crypto.digest();
|
||
if (!result || result.byteLength === 0) {
|
||
throw new Error('HMAC digest returned empty result');
|
||
}
|
||
resolve(result);
|
||
} catch (wxError) {
|
||
console.error('WeChat HMAC API failed:', {
|
||
algorithm: normalizedAlgorithm,
|
||
error: wxError.message
|
||
});
|
||
throw new Error(`WeChat HMAC failed: ${wxError.message}`);
|
||
}
|
||
} else {
|
||
// 使用sjcl库作为备用实现
|
||
const keyBytes = typeof key === 'string' ? new TextEncoder().encode(key) : key;
|
||
const dataBytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
||
|
||
// 根据算法选择对应的哈希函数
|
||
let hashFunction;
|
||
switch (normalizedAlgorithm) {
|
||
case 'sha1':
|
||
hashFunction = sjcl.hash.sha1;
|
||
break;
|
||
case 'sha256':
|
||
hashFunction = sjcl.hash.sha256;
|
||
break;
|
||
case 'sha512':
|
||
hashFunction = sjcl.hash.sha512;
|
||
break;
|
||
default:
|
||
throw new Error(`Unsupported algorithm: ${algorithm}`);
|
||
}
|
||
|
||
const backupResult = backupHMAC(keyBytes, dataBytes, hashFunction);
|
||
resolve(backupResult);
|
||
}
|
||
} catch (error) {
|
||
console.error('HMAC generation failed:', error);
|
||
reject(new Error(`HMAC generation failed: ${error.message}`));
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 备用HMAC实现,使用sjcl库
|
||
*
|
||
* @param {Uint8Array} key - 密钥
|
||
* @param {Uint8Array} data - 数据
|
||
* @param {Function} hashFunction - 哈希算法构造函数
|
||
* @returns {Uint8Array} HMAC 结果
|
||
*/
|
||
function backupHMAC(key, data, hashFunction) {
|
||
try {
|
||
// 将key和data转换为sjcl的bitArray格式
|
||
const keyBits = sjcl.codec.bytes.toBits(Array.from(key));
|
||
const dataBits = sjcl.codec.bytes.toBits(Array.from(data));
|
||
|
||
// 创建HMAC对象并更新数据
|
||
const hmac = new sjcl.misc.hmac(keyBits, hashFunction);
|
||
hmac.update(dataBits);
|
||
|
||
// 获取结果并转换回字节数组
|
||
const result = hmac.digest();
|
||
const byteArray = sjcl.codec.bytes.fromBits(result);
|
||
return new Uint8Array(byteArray);
|
||
} catch (error) {
|
||
console.error('Backup HMAC implementation failed:', error);
|
||
throw new Error(`Backup HMAC implementation failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成 HMAC-SHA1 值(向后兼容)
|
||
*
|
||
* @param {string|Uint8Array} key - 密钥
|
||
* @param {string|Uint8Array} data - 数据
|
||
* @returns {Promise<Uint8Array>} HMAC 结果
|
||
*/
|
||
function sha1(key, data) {
|
||
return hmac('sha1', key, data);
|
||
}
|
||
|
||
/**
|
||
* 生成 HMAC-SHA256 值
|
||
*
|
||
* @param {string|Uint8Array} key - 密钥
|
||
* @param {string|Uint8Array} data - 数据
|
||
* @returns {Promise<Uint8Array>} HMAC 结果
|
||
*/
|
||
function sha256(key, data) {
|
||
return hmac('sha256', key, data);
|
||
}
|
||
|
||
/**
|
||
* 生成 HMAC-SHA512 值
|
||
*
|
||
* @param {string|Uint8Array} key - 密钥
|
||
* @param {string|Uint8Array} data - 数据
|
||
* @returns {Promise<Uint8Array>} HMAC 结果
|
||
*/
|
||
function sha512(key, data) {
|
||
return hmac('sha512', key, data);
|
||
}
|
||
|
||
/**
|
||
* 使用PBKDF2算法派生密钥
|
||
*
|
||
* @param {Uint8Array} password - 原始密码
|
||
* @param {Uint8Array} salt - 盐值
|
||
* @param {number} iterations - 迭代次数(建议至少10000次)
|
||
* @param {number} keyLength - 生成的密钥长度(字节)
|
||
* @param {string} hashAlgorithm - 哈希算法(如'sha256')
|
||
* @returns {Promise<Uint8Array>} 派生出的密钥
|
||
*/
|
||
async function deriveKeyPBKDF2(password, salt, iterations, keyLength, hashAlgorithm = 'sha256') {
|
||
// 参数验证
|
||
if (!(password instanceof Uint8Array) || !(salt instanceof Uint8Array)) {
|
||
throw new Error('Password and salt must be Uint8Array');
|
||
}
|
||
|
||
if (typeof iterations !== 'number' || iterations <= 0) {
|
||
throw new Error('Iterations must be a positive number');
|
||
}
|
||
|
||
if (typeof keyLength !== 'number' || keyLength <= 0) {
|
||
throw new Error('Key length must be a positive number');
|
||
}
|
||
|
||
// 优先使用微信小程序原生加密API
|
||
if (typeof wx !== 'undefined' && wx.derivePBKDF2) {
|
||
return new Promise((resolve, reject) => {
|
||
wx.derivePBKDF2({
|
||
password: password.buffer,
|
||
salt: salt.buffer,
|
||
iterations: iterations,
|
||
keySize: keyLength,
|
||
hashAlgorithm: hashAlgorithm,
|
||
success: (res) => {
|
||
resolve(new Uint8Array(res.key));
|
||
},
|
||
fail: (err) => {
|
||
console.error('PBKDF2 derivation failed:', {
|
||
source: 'WeChat API',
|
||
error: err.errMsg || 'Unknown error'
|
||
});
|
||
reject(new Error(`PBKDF2 derivation failed: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 使用sjcl库的PBKDF2实现
|
||
try {
|
||
// 根据算法选择对应的哈希函数
|
||
let hashFunction;
|
||
switch (hashAlgorithm.toLowerCase()) {
|
||
case 'sha1':
|
||
hashFunction = sjcl.hash.sha1;
|
||
break;
|
||
case 'sha256':
|
||
hashFunction = sjcl.hash.sha256;
|
||
break;
|
||
case 'sha512':
|
||
hashFunction = sjcl.hash.sha512;
|
||
break;
|
||
default:
|
||
throw new Error(`Unsupported hash algorithm: ${hashAlgorithm}`);
|
||
}
|
||
|
||
// 将密码转换为字符串格式
|
||
const passwordStr = typeof password === 'string' ? password : String.fromCharCode.apply(null, password);
|
||
// 将盐值转换为字符串格式
|
||
const saltStr = typeof salt === 'string' ? salt : String.fromCharCode.apply(null, salt);
|
||
|
||
// 使用sjcl的PBKDF2实现
|
||
const key = sjcl.misc.pbkdf2(passwordStr, saltStr, iterations, keyLength * 8, hashFunction);
|
||
|
||
// 将结果转换为Uint8Array
|
||
const keyBuffer = new ArrayBuffer(keyLength);
|
||
const keyData = new Uint8Array(keyBuffer);
|
||
for (let i = 0; i < keyLength; i++) {
|
||
keyData[i] = key[i];
|
||
}
|
||
|
||
return keyData;
|
||
} catch (error) {
|
||
throw new Error(`PBKDF2 derivation failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 导出函数
|
||
export {
|
||
hmac,
|
||
sha1,
|
||
sha256,
|
||
sha512,
|
||
deriveKeyPBKDF2
|
||
}; |