mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-20 03:30:50 +08:00
feat(security): implement secure storage for sensitive data and migrate existing keys
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
// 提供 API 基础地址规范化、请求封装、配置缓存以及统一数据加载能力
|
// 提供 API 基础地址规范化、请求封装、配置缓存以及统一数据加载能力
|
||||||
|
|
||||||
import { STATUS_UPDATE_INTERVAL_MS, DEFAULT_API_PORT } from '../utils/constants.js';
|
import { STATUS_UPDATE_INTERVAL_MS, DEFAULT_API_PORT } from '../utils/constants.js';
|
||||||
|
import { secureStorage } from '../utils/secure-storage.js';
|
||||||
|
|
||||||
export const connectionModule = {
|
export const connectionModule = {
|
||||||
// 规范化基础地址,移除尾部斜杠与 /v0/management
|
// 规范化基础地址,移除尾部斜杠与 /v0/management
|
||||||
@@ -28,16 +29,18 @@ export const connectionModule = {
|
|||||||
setApiBase(newBase) {
|
setApiBase(newBase) {
|
||||||
this.apiBase = this.normalizeBase(newBase);
|
this.apiBase = this.normalizeBase(newBase);
|
||||||
this.apiUrl = this.computeApiUrl(this.apiBase);
|
this.apiUrl = this.computeApiUrl(this.apiBase);
|
||||||
localStorage.setItem('apiBase', this.apiBase);
|
secureStorage.setItem('apiBase', this.apiBase);
|
||||||
localStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段
|
secureStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段
|
||||||
this.updateLoginConnectionInfo();
|
this.updateLoginConnectionInfo();
|
||||||
},
|
},
|
||||||
|
|
||||||
// 加载设置(简化版,仅加载内部状态)
|
// 加载设置(简化版,仅加载内部状态)
|
||||||
loadSettings() {
|
loadSettings() {
|
||||||
const savedBase = localStorage.getItem('apiBase');
|
secureStorage.migratePlaintextKeys(['apiBase', 'apiUrl', 'managementKey']);
|
||||||
const savedUrl = localStorage.getItem('apiUrl');
|
|
||||||
const savedKey = localStorage.getItem('managementKey');
|
const savedBase = secureStorage.getItem('apiBase');
|
||||||
|
const savedUrl = secureStorage.getItem('apiUrl');
|
||||||
|
const savedKey = secureStorage.getItem('managementKey');
|
||||||
|
|
||||||
if (savedBase) {
|
if (savedBase) {
|
||||||
this.setApiBase(savedBase);
|
this.setApiBase(savedBase);
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import { secureStorage } from '../utils/secure-storage.js';
|
||||||
|
|
||||||
export const loginModule = {
|
export const loginModule = {
|
||||||
async checkLoginStatus() {
|
async checkLoginStatus() {
|
||||||
const savedBase = localStorage.getItem('apiBase');
|
// 将旧的明文缓存迁移为加密格式
|
||||||
const savedKey = localStorage.getItem('managementKey');
|
secureStorage.migratePlaintextKeys(['apiBase', 'apiUrl', 'managementKey']);
|
||||||
|
|
||||||
|
const savedBase = secureStorage.getItem('apiBase');
|
||||||
|
const savedKey = secureStorage.getItem('managementKey');
|
||||||
const wasLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
const wasLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
||||||
|
|
||||||
if (savedBase && savedKey && wasLoggedIn) {
|
if (savedBase && savedKey && wasLoggedIn) {
|
||||||
@@ -75,7 +80,7 @@ export const loginModule = {
|
|||||||
try {
|
try {
|
||||||
this.setApiBase(apiBase);
|
this.setApiBase(apiBase);
|
||||||
this.managementKey = managementKey;
|
this.managementKey = managementKey;
|
||||||
localStorage.setItem('managementKey', this.managementKey);
|
secureStorage.setItem('managementKey', this.managementKey);
|
||||||
|
|
||||||
await this.testConnection();
|
await this.testConnection();
|
||||||
|
|
||||||
@@ -97,7 +102,7 @@ export const loginModule = {
|
|||||||
this.stopStatusUpdateTimer();
|
this.stopStatusUpdateTimer();
|
||||||
|
|
||||||
localStorage.removeItem('isLoggedIn');
|
localStorage.removeItem('isLoggedIn');
|
||||||
localStorage.removeItem('managementKey');
|
secureStorage.removeItem('managementKey');
|
||||||
|
|
||||||
this.showLoginPage();
|
this.showLoginPage();
|
||||||
},
|
},
|
||||||
@@ -127,7 +132,7 @@ export const loginModule = {
|
|||||||
this.hideLoginError();
|
this.hideLoginError();
|
||||||
|
|
||||||
this.managementKey = managementKey;
|
this.managementKey = managementKey;
|
||||||
localStorage.setItem('managementKey', this.managementKey);
|
secureStorage.setItem('managementKey', this.managementKey);
|
||||||
|
|
||||||
await this.login(this.apiBase, this.managementKey);
|
await this.login(this.apiBase, this.managementKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -186,8 +191,8 @@ export const loginModule = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadLoginSettings() {
|
loadLoginSettings() {
|
||||||
const savedBase = localStorage.getItem('apiBase');
|
const savedBase = secureStorage.getItem('apiBase');
|
||||||
const savedKey = localStorage.getItem('managementKey');
|
const savedKey = secureStorage.getItem('managementKey');
|
||||||
const loginKeyInput = document.getElementById('login-management-key');
|
const loginKeyInput = document.getElementById('login-management-key');
|
||||||
const apiBaseInput = document.getElementById('login-api-base');
|
const apiBaseInput = document.getElementById('login-api-base');
|
||||||
|
|
||||||
@@ -216,7 +221,7 @@ export const loginModule = {
|
|||||||
const saveKey = (val) => {
|
const saveKey = (val) => {
|
||||||
if (val.trim()) {
|
if (val.trim()) {
|
||||||
this.managementKey = val;
|
this.managementKey = val;
|
||||||
localStorage.setItem('managementKey', this.managementKey);
|
secureStorage.setItem('managementKey', this.managementKey);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const saveKeyDebounced = this.debounce(saveKey, 500);
|
const saveKeyDebounced = this.debounce(saveKey, 500);
|
||||||
|
|||||||
128
src/utils/secure-storage.js
Normal file
128
src/utils/secure-storage.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// 简单的浏览器端加密存储封装
|
||||||
|
// 仅用于避免本地缓存中明文暴露敏感值,无法替代服务端安全控制
|
||||||
|
|
||||||
|
const ENC_PREFIX = 'enc::v1::';
|
||||||
|
const SECRET_SALT = 'cli-proxy-api-webui::secure-storage';
|
||||||
|
|
||||||
|
const encoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;
|
||||||
|
const decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder() : null;
|
||||||
|
|
||||||
|
let cachedKeyBytes = null;
|
||||||
|
|
||||||
|
function encodeText(text) {
|
||||||
|
if (encoder) return encoder.encode(text);
|
||||||
|
const result = new Uint8Array(text.length);
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
result[i] = text.charCodeAt(i) & 0xff;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeText(bytes) {
|
||||||
|
if (decoder) return decoder.decode(bytes);
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
result += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKeyBytes() {
|
||||||
|
if (cachedKeyBytes) return cachedKeyBytes;
|
||||||
|
try {
|
||||||
|
const host = typeof window !== 'undefined' ? window.location.host : 'unknown-host';
|
||||||
|
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown-ua';
|
||||||
|
cachedKeyBytes = encodeText(`${SECRET_SALT}|${host}|${ua}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Secure storage fallback to plain text:', error);
|
||||||
|
cachedKeyBytes = encodeText(SECRET_SALT);
|
||||||
|
}
|
||||||
|
return cachedKeyBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function xorBytes(data, keyBytes) {
|
||||||
|
const result = new Uint8Array(data.length);
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
result[i] = data[i] ^ keyBytes[i % keyBytes.length];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBase64(bytes) {
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBase64(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(value) {
|
||||||
|
if (value === null || value === undefined) return value;
|
||||||
|
try {
|
||||||
|
const keyBytes = getKeyBytes();
|
||||||
|
const encrypted = xorBytes(encodeText(String(value)), keyBytes);
|
||||||
|
return `${ENC_PREFIX}${toBase64(encrypted)}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Secure storage encode fallback:', error);
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decode(payload) {
|
||||||
|
if (payload === null || payload === undefined) return payload;
|
||||||
|
if (!payload.startsWith(ENC_PREFIX)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const encodedBody = payload.slice(ENC_PREFIX.length);
|
||||||
|
const encrypted = fromBase64(encodedBody);
|
||||||
|
const decrypted = xorBytes(encrypted, getKeyBytes());
|
||||||
|
return decodeText(decrypted);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Secure storage decode fallback:', error);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const secureStorage = {
|
||||||
|
setItem(key, value, { encrypt = true } = {}) {
|
||||||
|
if (typeof localStorage === 'undefined') return;
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const storedValue = encrypt ? encode(value) : String(value);
|
||||||
|
localStorage.setItem(key, storedValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
getItem(key, { decrypt = true } = {}) {
|
||||||
|
if (typeof localStorage === 'undefined') return null;
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
if (raw === null) return null;
|
||||||
|
return decrypt ? decode(raw) : raw;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(key) {
|
||||||
|
if (typeof localStorage === 'undefined') return;
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
},
|
||||||
|
|
||||||
|
migratePlaintextKeys(keys = []) {
|
||||||
|
if (typeof localStorage === 'undefined') return;
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
if (raw && !String(raw).startsWith(ENC_PREFIX)) {
|
||||||
|
this.setItem(key, raw, { encrypt: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user