294 lines
7.7 KiB
TypeScript
294 lines
7.7 KiB
TypeScript
/**
|
|
* 密钥管理模块
|
|
* 负责客户端和服务端密钥的生成、导入、交换和存储
|
|
*/
|
|
|
|
// 密钥存储键名
|
|
export const CLIENT_KEYPAIR_STORAGE_KEY = "cryptio_client_keypair";
|
|
export const SERVER_KEYS_STORAGE_KEY = "cryptio_server_keys";
|
|
|
|
import { bytesToBase64 } from "./base64Utils.js";
|
|
|
|
// 密钥接口
|
|
export interface KeyPair {
|
|
publicKey: string;
|
|
privateKey: string;
|
|
}
|
|
|
|
export interface ServerKeys {
|
|
publicKey: string;
|
|
privateKey: string;
|
|
}
|
|
|
|
/**
|
|
* 生成客户端RSA密钥对
|
|
*/
|
|
export async function generateClientKeyPair(): Promise<KeyPair> {
|
|
// 生成RSA密钥对
|
|
const keyPair = await window.crypto.subtle.generateKey(
|
|
{
|
|
name: "RSA-OAEP",
|
|
modulusLength: 2048,
|
|
publicExponent: new Uint8Array([1, 0, 1]),
|
|
hash: "SHA-256",
|
|
},
|
|
true,
|
|
["encrypt", "decrypt"]
|
|
);
|
|
|
|
// 导出公钥为PEM格式
|
|
const publicKeyBuffer = await window.crypto.subtle.exportKey(
|
|
"spki",
|
|
keyPair.publicKey
|
|
);
|
|
const publicKeyBase64 = bytesToBase64(
|
|
new Uint8Array(publicKeyBuffer)
|
|
);
|
|
const publicKeyPem =
|
|
"-----BEGIN PUBLIC KEY-----\n" +
|
|
publicKeyBase64.match(/.{1,64}/g)!.join("\n") +
|
|
"\n-----END PUBLIC KEY-----";
|
|
|
|
// 导出私钥为PEM格式
|
|
const privateKeyBuffer = await window.crypto.subtle.exportKey(
|
|
"pkcs8",
|
|
keyPair.privateKey
|
|
);
|
|
const privateKeyBase64 = bytesToBase64(
|
|
new Uint8Array(privateKeyBuffer)
|
|
);
|
|
const privateKeyPem =
|
|
"-----BEGIN PRIVATE KEY-----\n" +
|
|
privateKeyBase64.match(/.{1,64}/g)!.join("\n") +
|
|
"\n-----END PRIVATE KEY-----";
|
|
|
|
return {
|
|
publicKey: publicKeyPem,
|
|
privateKey: privateKeyPem,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 获取或生成客户端密钥对
|
|
*/
|
|
export async function getClientKeyPair(): Promise<KeyPair> {
|
|
// 从localStorage获取
|
|
const stored = localStorage.getItem(CLIENT_KEYPAIR_STORAGE_KEY);
|
|
if (stored) {
|
|
try {
|
|
return JSON.parse(stored);
|
|
} catch (e) {
|
|
console.error("Failed to parse stored client keypair:", e);
|
|
}
|
|
}
|
|
|
|
// 生成新的密钥对
|
|
console.log("Generating new client keypair...");
|
|
const keyPair = await generateClientKeyPair();
|
|
|
|
// 保存到localStorage
|
|
localStorage.setItem(CLIENT_KEYPAIR_STORAGE_KEY, JSON.stringify(keyPair));
|
|
|
|
return keyPair;
|
|
}
|
|
|
|
/**
|
|
* 从PEM格式导入公钥
|
|
*/
|
|
export async function importPublicKeyFromPem(pem: string): Promise<CryptoKey> {
|
|
const pemHeader = "-----BEGIN PUBLIC KEY-----";
|
|
const pemFooter = "-----END PUBLIC KEY-----";
|
|
const pemContents = pem
|
|
.substring(
|
|
pem.indexOf(pemHeader) + pemHeader.length,
|
|
pem.indexOf(pemFooter)
|
|
)
|
|
.replace(/\s/g, "");
|
|
|
|
const binaryDer = window.atob(pemContents);
|
|
const binaryDerArray = new Uint8Array(binaryDer.length);
|
|
for (let i = 0; i < binaryDer.length; i++) {
|
|
binaryDerArray[i] = binaryDer.charCodeAt(i);
|
|
}
|
|
|
|
return await window.crypto.subtle.importKey(
|
|
"spki",
|
|
binaryDerArray,
|
|
{
|
|
name: "RSA-OAEP",
|
|
hash: "SHA-256",
|
|
},
|
|
false,
|
|
["encrypt"]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 从PEM格式导入私钥
|
|
*/
|
|
export async function importPrivateKeyFromPem(pem: string): Promise<CryptoKey> {
|
|
const pemHeader = "-----BEGIN PRIVATE KEY-----";
|
|
const pemFooter = "-----END PRIVATE KEY-----";
|
|
const pemContents = pem
|
|
.substring(
|
|
pem.indexOf(pemHeader) + pemHeader.length,
|
|
pem.indexOf(pemFooter)
|
|
)
|
|
.replace(/\s/g, "");
|
|
|
|
const binaryDer = window.atob(pemContents);
|
|
const binaryDerArray = new Uint8Array(binaryDer.length);
|
|
for (let i = 0; i < binaryDer.length; i++) {
|
|
binaryDerArray[i] = binaryDer.charCodeAt(i);
|
|
}
|
|
|
|
return await window.crypto.subtle.importKey(
|
|
"pkcs8",
|
|
binaryDerArray,
|
|
{
|
|
name: "RSA-OAEP",
|
|
hash: "SHA-256",
|
|
},
|
|
false,
|
|
["decrypt"]
|
|
);
|
|
}
|
|
|
|
// In-flight promise to deduplicate concurrent exchangeKeys() calls
|
|
let exchangeKeysPromise: Promise<ServerKeys> | null = null;
|
|
|
|
/**
|
|
* 与服务器交换密钥,使用客户端公钥加密 AES 密钥
|
|
* Deduplicates concurrent calls: if a key exchange is already in progress,
|
|
* subsequent calls will await the same promise.
|
|
*/
|
|
export async function exchangeKeys(): Promise<ServerKeys> {
|
|
// 检查缓存
|
|
const cached = localStorage.getItem(SERVER_KEYS_STORAGE_KEY);
|
|
if (cached) {
|
|
try {
|
|
return JSON.parse(cached);
|
|
} catch (e) {
|
|
console.error("Failed to parse stored server keys:", e);
|
|
}
|
|
}
|
|
|
|
// Deduplicate: reuse in-flight promise if one exists
|
|
if (exchangeKeysPromise) {
|
|
return exchangeKeysPromise;
|
|
}
|
|
|
|
exchangeKeysPromise = performKeyExchange();
|
|
try {
|
|
return await exchangeKeysPromise;
|
|
} finally {
|
|
exchangeKeysPromise = null;
|
|
}
|
|
}
|
|
|
|
async function performKeyExchange(): Promise<ServerKeys> {
|
|
// 获取客户端密钥对
|
|
const clientKeyPair = await getClientKeyPair();
|
|
|
|
// 发送客户端公钥到服务器
|
|
const response = await fetch("/cryptio/exchange_keys", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
client_public_key: clientKeyPair.publicKey,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Key exchange failed: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// 使用客户端私钥解密服务端密钥
|
|
const clientPrivateKey = await importPrivateKeyFromPem(
|
|
clientKeyPair.privateKey
|
|
);
|
|
|
|
// 1. 解密AES密钥
|
|
const encryptedAesKey = Uint8Array.from(
|
|
atob(data.encrypted_aes_key),
|
|
(c) => c.charCodeAt(0)
|
|
);
|
|
const aesKeyBuffer = await window.crypto.subtle.decrypt(
|
|
{
|
|
name: "RSA-OAEP",
|
|
},
|
|
clientPrivateKey,
|
|
encryptedAesKey
|
|
);
|
|
|
|
// 2. 导入AES密钥
|
|
const aesKey = await window.crypto.subtle.importKey(
|
|
"raw",
|
|
aesKeyBuffer,
|
|
{
|
|
name: "AES-CBC",
|
|
length: 256,
|
|
},
|
|
false,
|
|
["decrypt"]
|
|
);
|
|
|
|
// 3. 解密服务端密钥数据
|
|
const iv = Uint8Array.from(atob(data.iv), (c) => c.charCodeAt(0));
|
|
const encryptedData = Uint8Array.from(atob(data.encrypted_data), (c) =>
|
|
c.charCodeAt(0)
|
|
);
|
|
|
|
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
|
{
|
|
name: "AES-CBC",
|
|
iv: iv,
|
|
},
|
|
aesKey,
|
|
encryptedData
|
|
);
|
|
|
|
// 4. 解析JSON
|
|
const decryptedText = new TextDecoder().decode(decryptedBuffer);
|
|
const rawServerKeys = JSON.parse(decryptedText);
|
|
const serverKeys = {
|
|
publicKey: rawServerKeys.server_public_key,
|
|
privateKey: rawServerKeys.server_private_key,
|
|
};
|
|
|
|
// 5. 保存到localStorage
|
|
localStorage.setItem(SERVER_KEYS_STORAGE_KEY, JSON.stringify(serverKeys));
|
|
|
|
return serverKeys;
|
|
}
|
|
|
|
/**
|
|
* 获取服务端密钥
|
|
*/
|
|
export async function getServerKeys(): Promise<ServerKeys> {
|
|
// 先尝试从缓存获取
|
|
const cached = localStorage.getItem(SERVER_KEYS_STORAGE_KEY);
|
|
if (cached) {
|
|
try {
|
|
return JSON.parse(cached);
|
|
} catch (e) {
|
|
console.error("Failed to parse stored server keys:", e);
|
|
}
|
|
}
|
|
|
|
// 如果缓存不存在,抛出错误(SaveImage场景)
|
|
// 或者尝试交换密钥(UploadImage场景)
|
|
throw new Error("Server keys not found. Please upload an image first to exchange keys.");
|
|
}
|
|
|
|
/**
|
|
* 获取服务端密钥(带自动交换)
|
|
*/
|
|
export async function getServerKeysWithExchange(): Promise<ServerKeys> {
|
|
return await exchangeKeys();
|
|
}
|