258 lines
5.8 KiB
JavaScript
258 lines
5.8 KiB
JavaScript
|
|
/**
|
|||
|
|
* 安全存储工具类
|
|||
|
|
* 使用 AES-GCM 加密算法对敏感数据进行加密存储
|
|||
|
|
* 防止 XSS 攻击导致的 Token 泄露
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 加密密钥(从环境变量或固定字符串派生)
|
|||
|
|
* 注意:实际生产环境应该使用更安全的密钥管理方案
|
|||
|
|
*/
|
|||
|
|
const ENCRYPTION_KEY_SOURCE = 'power-leasing-2024-secure-key-v1';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将字符串转换为 ArrayBuffer
|
|||
|
|
* @param {string} str - 要转换的字符串
|
|||
|
|
* @returns {ArrayBuffer}
|
|||
|
|
*/
|
|||
|
|
function str2ab(str) {
|
|||
|
|
const encoder = new TextEncoder();
|
|||
|
|
return encoder.encode(str);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将 ArrayBuffer 转换为字符串
|
|||
|
|
* @param {ArrayBuffer} buffer - 要转换的 ArrayBuffer
|
|||
|
|
* @returns {string}
|
|||
|
|
*/
|
|||
|
|
function ab2str(buffer) {
|
|||
|
|
const decoder = new TextDecoder();
|
|||
|
|
return decoder.decode(buffer);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将 ArrayBuffer 转换为 Base64 字符串
|
|||
|
|
* @param {ArrayBuffer} buffer - 要转换的 ArrayBuffer
|
|||
|
|
* @returns {string}
|
|||
|
|
*/
|
|||
|
|
function arrayBufferToBase64(buffer) {
|
|||
|
|
const bytes = new Uint8Array(buffer);
|
|||
|
|
let binary = '';
|
|||
|
|
for (let i = 0; i < bytes.byteLength; i++) {
|
|||
|
|
binary += String.fromCharCode(bytes[i]);
|
|||
|
|
}
|
|||
|
|
return btoa(binary);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将 Base64 字符串转换为 ArrayBuffer
|
|||
|
|
* @param {string} base64 - Base64 字符串
|
|||
|
|
* @returns {ArrayBuffer}
|
|||
|
|
*/
|
|||
|
|
function base64ToArrayBuffer(base64) {
|
|||
|
|
const binary = atob(base64);
|
|||
|
|
const bytes = new Uint8Array(binary.length);
|
|||
|
|
for (let i = 0; i < binary.length; i++) {
|
|||
|
|
bytes[i] = binary.charCodeAt(i);
|
|||
|
|
}
|
|||
|
|
return bytes.buffer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 派生加密密钥
|
|||
|
|
* @returns {Promise<CryptoKey>}
|
|||
|
|
*/
|
|||
|
|
async function getDerivedKey() {
|
|||
|
|
// 将密钥源字符串转换为 ArrayBuffer
|
|||
|
|
const keyMaterial = str2ab(ENCRYPTION_KEY_SOURCE);
|
|||
|
|
|
|||
|
|
// 导入密钥材料
|
|||
|
|
const baseKey = await crypto.subtle.importKey(
|
|||
|
|
'raw',
|
|||
|
|
keyMaterial,
|
|||
|
|
'PBKDF2',
|
|||
|
|
false,
|
|||
|
|
['deriveBits', 'deriveKey']
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 使用 PBKDF2 派生密钥
|
|||
|
|
// 盐值固定(实际应该存储随机盐值,但为了简化实现使用固定盐)
|
|||
|
|
const salt = str2ab('power-leasing-salt-2024');
|
|||
|
|
|
|||
|
|
return await crypto.subtle.deriveKey(
|
|||
|
|
{
|
|||
|
|
name: 'PBKDF2',
|
|||
|
|
salt: salt,
|
|||
|
|
iterations: 100000,
|
|||
|
|
hash: 'SHA-256'
|
|||
|
|
},
|
|||
|
|
baseKey,
|
|||
|
|
{ name: 'AES-GCM', length: 256 },
|
|||
|
|
false,
|
|||
|
|
['encrypt', 'decrypt']
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 加密数据
|
|||
|
|
* @param {string} plaintext - 明文数据
|
|||
|
|
* @returns {Promise<string>} 加密后的数据(Base64 编码)
|
|||
|
|
*/
|
|||
|
|
async function encrypt(plaintext) {
|
|||
|
|
try {
|
|||
|
|
if (!plaintext || typeof plaintext !== 'string') {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取加密密钥
|
|||
|
|
const key = await getDerivedKey();
|
|||
|
|
|
|||
|
|
// 生成随机 IV(初始化向量)
|
|||
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|||
|
|
|
|||
|
|
// 加密数据
|
|||
|
|
const encrypted = await crypto.subtle.encrypt(
|
|||
|
|
{
|
|||
|
|
name: 'AES-GCM',
|
|||
|
|
iv: iv
|
|||
|
|
},
|
|||
|
|
key,
|
|||
|
|
str2ab(plaintext)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 将 IV 和加密数据组合(IV 不需要保密,可以明文存储)
|
|||
|
|
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|||
|
|
combined.set(iv, 0);
|
|||
|
|
combined.set(new Uint8Array(encrypted), iv.length);
|
|||
|
|
|
|||
|
|
// 转换为 Base64 字符串
|
|||
|
|
return arrayBufferToBase64(combined.buffer);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加密失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 解密数据
|
|||
|
|
* @param {string} ciphertext - 加密后的数据(Base64 编码)
|
|||
|
|
* @returns {Promise<string|null>} 解密后的明文数据
|
|||
|
|
*/
|
|||
|
|
async function decrypt(ciphertext) {
|
|||
|
|
try {
|
|||
|
|
if (!ciphertext || typeof ciphertext !== 'string') {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取加密密钥
|
|||
|
|
const key = await getDerivedKey();
|
|||
|
|
|
|||
|
|
// 解码 Base64
|
|||
|
|
const combined = base64ToArrayBuffer(ciphertext);
|
|||
|
|
|
|||
|
|
// 分离 IV 和加密数据
|
|||
|
|
const iv = combined.slice(0, 12);
|
|||
|
|
const encrypted = combined.slice(12);
|
|||
|
|
|
|||
|
|
// 解密数据
|
|||
|
|
const decrypted = await crypto.subtle.decrypt(
|
|||
|
|
{
|
|||
|
|
name: 'AES-GCM',
|
|||
|
|
iv: iv
|
|||
|
|
},
|
|||
|
|
key,
|
|||
|
|
encrypted
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 转换为字符串
|
|||
|
|
return ab2str(decrypted);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('解密失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 安全存储类
|
|||
|
|
* 提供加密的 localStorage 操作接口
|
|||
|
|
*/
|
|||
|
|
class SecureStorage {
|
|||
|
|
/**
|
|||
|
|
* 安全地设置 localStorage 项(加密存储)
|
|||
|
|
* @param {string} key - 存储键名
|
|||
|
|
* @param {string} value - 要存储的值
|
|||
|
|
* @returns {Promise<boolean>} 是否成功
|
|||
|
|
*/
|
|||
|
|
async setItem(key, value) {
|
|||
|
|
try {
|
|||
|
|
if (!value) {
|
|||
|
|
localStorage.removeItem(key);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加密数据
|
|||
|
|
const encrypted = await encrypt(value);
|
|||
|
|
if (encrypted) {
|
|||
|
|
localStorage.setItem(key, encrypted);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`安全存储失败 [${key}]:`, error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 安全地获取 localStorage 项(解密读取)
|
|||
|
|
* @param {string} key - 存储键名
|
|||
|
|
* @returns {Promise<string|null>} 解密后的值
|
|||
|
|
*/
|
|||
|
|
async getItem(key) {
|
|||
|
|
try {
|
|||
|
|
const encrypted = localStorage.getItem(key);
|
|||
|
|
if (!encrypted) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解密数据
|
|||
|
|
return await decrypt(encrypted);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`安全读取失败 [${key}]:`, error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 移除 localStorage 项
|
|||
|
|
* @param {string} key - 存储键名
|
|||
|
|
*/
|
|||
|
|
removeItem(key) {
|
|||
|
|
try {
|
|||
|
|
localStorage.removeItem(key);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`移除存储失败 [${key}]:`, error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查 localStorage 项是否存在
|
|||
|
|
* @param {string} key - 存储键名
|
|||
|
|
* @returns {boolean}
|
|||
|
|
*/
|
|||
|
|
hasItem(key) {
|
|||
|
|
try {
|
|||
|
|
return localStorage.getItem(key) !== null;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`检查存储失败 [${key}]:`, error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建单例实例
|
|||
|
|
const secureStorage = new SecureStorage();
|
|||
|
|
|
|||
|
|
export default secureStorage;
|
|||
|
|
export { SecureStorage };
|