mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 01:06:59 +08:00
fix: Fixed go vet issues. (#658)
* Fixed vet ./... errors. * Fixed ESLint issues.
This commit is contained in:
@@ -66,14 +66,12 @@ func TestDatabaseEncryption(t *testing.T) {
|
|||||||
|
|
||||||
// TestHybridEncryption 測試混合加密(前端 → 後端場景)
|
// TestHybridEncryption 測試混合加密(前端 → 後端場景)
|
||||||
func TestHybridEncryption(t *testing.T) {
|
func TestHybridEncryption(t *testing.T) {
|
||||||
em, err := GetEncryptionManager()
|
_, err := GetEncryptionManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("初始化加密管理器失敗: %v", err)
|
t.Fatalf("初始化加密管理器失敗: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模擬前端加密私鑰
|
// 模擬前端加密私鑰
|
||||||
plaintext := "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
// plaintext := "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
||||||
|
|
||||||
// 注意:這裡需要前端的 encryptWithServerPublicKey 實現
|
// 注意:這裡需要前端的 encryptWithServerPublicKey 實現
|
||||||
// 為了測試,我們直接使用後端的加密函數(實際前端使用 Web Crypto API)
|
// 為了測試,我們直接使用後端的加密函數(實際前端使用 Web Crypto API)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
* 生成隨機混淆字串 (用於剪貼簿混淆)
|
* 生成隨機混淆字串 (用於剪貼簿混淆)
|
||||||
*/
|
*/
|
||||||
export function generateObfuscation(): string {
|
export function generateObfuscation(): string {
|
||||||
const array = new Uint8Array(32);
|
const array = new Uint8Array(32)
|
||||||
crypto.getRandomValues(array);
|
crypto.getRandomValues(array)
|
||||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(
|
||||||
|
''
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,47 +28,50 @@ export async function encryptWithServerPublicKey(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// 1. 導入伺服器公鑰
|
// 1. 導入伺服器公鑰
|
||||||
const publicKey = await importRSAPublicKey(serverPublicKeyPEM);
|
const publicKey = await importRSAPublicKey(serverPublicKeyPEM)
|
||||||
|
|
||||||
// 2. 生成隨機 AES 密鑰 (256-bit)
|
// 2. 生成隨機 AES 密鑰 (256-bit)
|
||||||
const aesKey = await crypto.subtle.generateKey(
|
const aesKey = await crypto.subtle.generateKey(
|
||||||
{ name: 'AES-GCM', length: 256 },
|
{ name: 'AES-GCM', length: 256 },
|
||||||
true,
|
true,
|
||||||
['encrypt']
|
['encrypt']
|
||||||
);
|
)
|
||||||
|
|
||||||
// 3. 使用 AES-GCM 加密數據
|
// 3. 使用 AES-GCM 加密數據
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit nonce
|
const iv = crypto.getRandomValues(new Uint8Array(12)) // 96-bit nonce
|
||||||
const encodedText = new TextEncoder().encode(plaintext);
|
const encodedText = new TextEncoder().encode(plaintext)
|
||||||
const encryptedData = await crypto.subtle.encrypt(
|
const encryptedData = await crypto.subtle.encrypt(
|
||||||
{ name: 'AES-GCM', iv },
|
{ name: 'AES-GCM', iv },
|
||||||
aesKey,
|
aesKey,
|
||||||
encodedText
|
encodedText
|
||||||
);
|
)
|
||||||
|
|
||||||
// 4. 導出 AES 密鑰並用 RSA 加密
|
// 4. 導出 AES 密鑰並用 RSA 加密
|
||||||
const exportedAESKey = await crypto.subtle.exportKey('raw', aesKey);
|
const exportedAESKey = await crypto.subtle.exportKey('raw', aesKey)
|
||||||
const encryptedAESKey = await crypto.subtle.encrypt(
|
const encryptedAESKey = await crypto.subtle.encrypt(
|
||||||
{ name: 'RSA-OAEP' },
|
{ name: 'RSA-OAEP' },
|
||||||
publicKey,
|
publicKey,
|
||||||
exportedAESKey
|
exportedAESKey
|
||||||
);
|
)
|
||||||
|
|
||||||
// 5. 組合: [加密的 AES 密鑰長度(4字節)] + [加密的 AES 密鑰] + [IV] + [加密數據]
|
// 5. 組合: [加密的 AES 密鑰長度(4字節)] + [加密的 AES 密鑰] + [IV] + [加密數據]
|
||||||
const result = new Uint8Array(
|
const result = new Uint8Array(
|
||||||
4 + encryptedAESKey.byteLength + iv.length + encryptedData.byteLength
|
4 + encryptedAESKey.byteLength + iv.length + encryptedData.byteLength
|
||||||
);
|
)
|
||||||
const view = new DataView(result.buffer);
|
const view = new DataView(result.buffer)
|
||||||
view.setUint32(0, encryptedAESKey.byteLength, false); // 大端序
|
view.setUint32(0, encryptedAESKey.byteLength, false) // 大端序
|
||||||
result.set(new Uint8Array(encryptedAESKey), 4);
|
result.set(new Uint8Array(encryptedAESKey), 4)
|
||||||
result.set(iv, 4 + encryptedAESKey.byteLength);
|
result.set(iv, 4 + encryptedAESKey.byteLength)
|
||||||
result.set(new Uint8Array(encryptedData), 4 + encryptedAESKey.byteLength + iv.length);
|
result.set(
|
||||||
|
new Uint8Array(encryptedData),
|
||||||
|
4 + encryptedAESKey.byteLength + iv.length
|
||||||
|
)
|
||||||
|
|
||||||
// 6. Base64 編碼
|
// 6. Base64 編碼
|
||||||
return arrayBufferToBase64(result);
|
return arrayBufferToBase64(result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加密失敗:', error);
|
console.error('加密失敗:', error)
|
||||||
throw new Error('加密過程中發生錯誤,請檢查伺服器公鑰是否有效');
|
throw new Error('加密過程中發生錯誤,請檢查伺服器公鑰是否有效')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +83,10 @@ async function importRSAPublicKey(pem: string): Promise<CryptoKey> {
|
|||||||
const pemContents = pem
|
const pemContents = pem
|
||||||
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
||||||
.replace(/-----END PUBLIC KEY-----/, '')
|
.replace(/-----END PUBLIC KEY-----/, '')
|
||||||
.replace(/\s/g, '');
|
.replace(/\s/g, '')
|
||||||
|
|
||||||
// Base64 解碼
|
// Base64 解碼
|
||||||
const binaryDer = base64ToArrayBuffer(pemContents);
|
const binaryDer = base64ToArrayBuffer(pemContents)
|
||||||
|
|
||||||
// 導入為 CryptoKey
|
// 導入為 CryptoKey
|
||||||
return crypto.subtle.importKey(
|
return crypto.subtle.importKey(
|
||||||
@@ -93,14 +98,14 @@ async function importRSAPublicKey(pem: string): Promise<CryptoKey> {
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
['encrypt']
|
['encrypt']
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 二階段輸入 UI ====================
|
// ==================== 二階段輸入 UI ====================
|
||||||
|
|
||||||
export interface TwoStageInputResult {
|
export interface TwoStageInputResult {
|
||||||
encryptedKey: string;
|
encryptedKey: string
|
||||||
obfuscationLog: string[]; // 混淆記錄(用於審計)
|
obfuscationLog: string[] // 混淆記錄(用於審計)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,34 +116,37 @@ export interface TwoStageInputResult {
|
|||||||
export async function twoStagePrivateKeyInput(
|
export async function twoStagePrivateKeyInput(
|
||||||
serverPublicKey: string
|
serverPublicKey: string
|
||||||
): Promise<TwoStageInputResult> {
|
): Promise<TwoStageInputResult> {
|
||||||
const obfuscationLog: string[] = [];
|
const obfuscationLog: string[] = []
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 創建自定義 Modal
|
// 創建自定義 Modal
|
||||||
const modal = createTwoStageModal(async (part1: string, part2: string) => {
|
const modal = createTwoStageModal(async (part1: string, part2: string) => {
|
||||||
try {
|
try {
|
||||||
const fullKey = part1 + part2;
|
const fullKey = part1 + part2
|
||||||
|
|
||||||
// 驗證私鑰格式
|
// 驗證私鑰格式
|
||||||
if (!validatePrivateKeyFormat(fullKey)) {
|
if (!validatePrivateKeyFormat(fullKey)) {
|
||||||
throw new Error('私鑰格式不正確(應為 64 位十六進制或 0x 開頭)');
|
throw new Error('私鑰格式不正確(應為 64 位十六進制或 0x 開頭)')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加密
|
// 加密
|
||||||
const encrypted = await encryptWithServerPublicKey(fullKey, serverPublicKey);
|
const encrypted = await encryptWithServerPublicKey(
|
||||||
|
fullKey,
|
||||||
|
serverPublicKey
|
||||||
|
)
|
||||||
|
|
||||||
// 清除敏感數據
|
// 清除敏感數據
|
||||||
part1 = '';
|
part1 = ''
|
||||||
part2 = '';
|
part2 = ''
|
||||||
|
|
||||||
resolve({ encryptedKey: encrypted, obfuscationLog });
|
resolve({ encryptedKey: encrypted, obfuscationLog })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error)
|
||||||
}
|
}
|
||||||
}, obfuscationLog);
|
}, obfuscationLog)
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,21 +156,21 @@ function createTwoStageModal(
|
|||||||
onSubmit: (part1: string, part2: string) => void,
|
onSubmit: (part1: string, part2: string) => void,
|
||||||
obfuscationLog: string[]
|
obfuscationLog: string[]
|
||||||
): HTMLElement {
|
): HTMLElement {
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div')
|
||||||
modal.style.cssText = `
|
modal.style.cssText = `
|
||||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
background: rgba(0,0,0,0.8); z-index: 10000;
|
background: rgba(0,0,0,0.8); z-index: 10000;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const content = document.createElement('div');
|
const content = document.createElement('div')
|
||||||
content.style.cssText = `
|
content.style.cssText = `
|
||||||
background: #1a1a2e; padding: 2rem; border-radius: 8px;
|
background: #1a1a2e; padding: 2rem; border-radius: 8px;
|
||||||
max-width: 500px; width: 90%; color: white;
|
max-width: 500px; width: 90%; color: white;
|
||||||
`;
|
`
|
||||||
|
|
||||||
let stage = 1;
|
let stage = 1
|
||||||
let part1 = '';
|
let part1 = ''
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
if (stage === 1) {
|
if (stage === 1) {
|
||||||
@@ -190,34 +198,36 @@ function createTwoStageModal(
|
|||||||
background: transparent; border: 1px solid #555; border-radius: 4px;
|
background: transparent; border: 1px solid #555; border-radius: 4px;
|
||||||
color: #888; cursor: pointer;"
|
color: #888; cursor: pointer;"
|
||||||
>取消</button>
|
>取消</button>
|
||||||
`;
|
`
|
||||||
|
|
||||||
const input = content.querySelector('#stage1-input') as HTMLInputElement;
|
const input = content.querySelector('#stage1-input') as HTMLInputElement
|
||||||
const nextBtn = content.querySelector('#stage1-next') as HTMLButtonElement;
|
const nextBtn = content.querySelector('#stage1-next') as HTMLButtonElement
|
||||||
const cancelBtn = content.querySelector('#cancel') as HTMLButtonElement;
|
const cancelBtn = content.querySelector('#cancel') as HTMLButtonElement
|
||||||
|
|
||||||
input.focus();
|
input.focus()
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
nextBtn.disabled = input.value.length < 10;
|
nextBtn.disabled = input.value.length < 10
|
||||||
});
|
})
|
||||||
|
|
||||||
nextBtn.addEventListener('click', async () => {
|
nextBtn.addEventListener('click', async () => {
|
||||||
part1 = input.value;
|
part1 = input.value
|
||||||
input.value = ''; // 立即清除
|
input.value = '' // 立即清除
|
||||||
|
|
||||||
// 生成混淆字串並強制複製
|
// 生成混淆字串並強制複製
|
||||||
const obfuscation = generateObfuscation();
|
const obfuscation = generateObfuscation()
|
||||||
await navigator.clipboard.writeText(obfuscation);
|
await navigator.clipboard.writeText(obfuscation)
|
||||||
obfuscationLog.push(`Stage1: ${new Date().toISOString()}`);
|
obfuscationLog.push(`Stage1: ${new Date().toISOString()}`)
|
||||||
|
|
||||||
alert('⚠️ 已複製混淆字串到剪貼簿\n\n請在任意地方貼上一次(避免監控),然後點擊確定繼續');
|
alert(
|
||||||
stage = 2;
|
'⚠️ 已複製混淆字串到剪貼簿\n\n請在任意地方貼上一次(避免監控),然後點擊確定繼續'
|
||||||
render();
|
)
|
||||||
});
|
stage = 2
|
||||||
|
render()
|
||||||
|
})
|
||||||
|
|
||||||
cancelBtn.addEventListener('click', () => {
|
cancelBtn.addEventListener('click', () => {
|
||||||
modal.remove();
|
modal.remove()
|
||||||
});
|
})
|
||||||
} else if (stage === 2) {
|
} else if (stage === 2) {
|
||||||
content.innerHTML = `
|
content.innerHTML = `
|
||||||
<h2 style="margin-bottom: 1rem;">🔐 安全輸入 - 第二階段</h2>
|
<h2 style="margin-bottom: 1rem;">🔐 安全輸入 - 第二階段</h2>
|
||||||
@@ -243,33 +253,35 @@ function createTwoStageModal(
|
|||||||
background: transparent; border: 1px solid #555; border-radius: 4px;
|
background: transparent; border: 1px solid #555; border-radius: 4px;
|
||||||
color: #888; cursor: pointer;"
|
color: #888; cursor: pointer;"
|
||||||
>← 返回上一步</button>
|
>← 返回上一步</button>
|
||||||
`;
|
`
|
||||||
|
|
||||||
const input = content.querySelector('#stage2-input') as HTMLInputElement;
|
const input = content.querySelector('#stage2-input') as HTMLInputElement
|
||||||
const submitBtn = content.querySelector('#stage2-submit') as HTMLButtonElement;
|
const submitBtn = content.querySelector(
|
||||||
const backBtn = content.querySelector('#back') as HTMLButtonElement;
|
'#stage2-submit'
|
||||||
|
) as HTMLButtonElement
|
||||||
|
const backBtn = content.querySelector('#back') as HTMLButtonElement
|
||||||
|
|
||||||
input.focus();
|
input.focus()
|
||||||
submitBtn.addEventListener('click', async () => {
|
submitBtn.addEventListener('click', async () => {
|
||||||
const part2 = input.value;
|
const part2 = input.value
|
||||||
input.value = ''; // 立即清除
|
input.value = '' // 立即清除
|
||||||
|
|
||||||
obfuscationLog.push(`Stage2: ${new Date().toISOString()}`);
|
obfuscationLog.push(`Stage2: ${new Date().toISOString()}`)
|
||||||
|
|
||||||
modal.remove();
|
modal.remove()
|
||||||
onSubmit(part1, part2);
|
onSubmit(part1, part2)
|
||||||
});
|
})
|
||||||
|
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
stage = 1;
|
stage = 1
|
||||||
render();
|
render()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
render();
|
render()
|
||||||
modal.appendChild(content);
|
modal.appendChild(content)
|
||||||
return modal;
|
return modal
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -277,38 +289,38 @@ function createTwoStageModal(
|
|||||||
*/
|
*/
|
||||||
function validatePrivateKeyFormat(key: string): boolean {
|
function validatePrivateKeyFormat(key: string): boolean {
|
||||||
// EVM 私鑰: 64 位十六進制 (可選 0x 前綴)
|
// EVM 私鑰: 64 位十六進制 (可選 0x 前綴)
|
||||||
const evmPattern = /^(0x)?[0-9a-fA-F]{64}$/;
|
const evmPattern = /^(0x)?[0-9a-fA-F]{64}$/
|
||||||
return evmPattern.test(key);
|
return evmPattern.test(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 工具函數 ====================
|
// ==================== 工具函數 ====================
|
||||||
|
|
||||||
function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string {
|
function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string {
|
||||||
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
|
||||||
let binary = '';
|
let binary = ''
|
||||||
for (let i = 0; i < bytes.byteLength; i++) {
|
for (let i = 0; i < bytes.byteLength; i++) {
|
||||||
binary += String.fromCharCode(bytes[i]);
|
binary += String.fromCharCode(bytes[i])
|
||||||
}
|
}
|
||||||
return btoa(binary);
|
return btoa(binary)
|
||||||
}
|
}
|
||||||
|
|
||||||
function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||||
const binary = atob(base64);
|
const binary = atob(base64)
|
||||||
const bytes = new Uint8Array(binary.length);
|
const bytes = new Uint8Array(binary.length)
|
||||||
for (let i = 0; i < binary.length; i++) {
|
for (let i = 0; i < binary.length; i++) {
|
||||||
bytes[i] = binary.charCodeAt(i);
|
bytes[i] = binary.charCodeAt(i)
|
||||||
}
|
}
|
||||||
return bytes.buffer;
|
return bytes.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 從伺服器獲取公鑰
|
* 從伺服器獲取公鑰
|
||||||
*/
|
*/
|
||||||
export async function fetchServerPublicKey(): Promise<string> {
|
export async function fetchServerPublicKey(): Promise<string> {
|
||||||
const response = await fetch('/api/crypto/public-key');
|
const response = await fetch('/api/crypto/public-key')
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('無法獲取伺服器公鑰');
|
throw new Error('無法獲取伺服器公鑰')
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json()
|
||||||
return data.public_key;
|
return data.public_key
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user