Implement security patches: CSP headers, console disabling, key rotation, clipboard security, network blocking, log cleanup, and PGP validation

This commit is contained in:
LC mac
2026-02-12 02:24:06 +08:00
parent 20cf558e83
commit 6c6379fcd4
11 changed files with 3365 additions and 135 deletions

View File

@@ -29,8 +29,12 @@ function bytesToBase64(bytes: Uint8Array): string {
* @private
*/
let sessionKey: CryptoKey | null = null;
let keyCreatedAt = 0;
let keyOperationCount = 0;
const KEY_ALGORITHM = 'AES-GCM';
const KEY_LENGTH = 256;
const KEY_ROTATION_INTERVAL = 5 * 60 * 1000; // 5 minutes
const MAX_KEY_OPERATIONS = 1000; // Rotate after N operations
/**
* An object containing encrypted data and necessary metadata for decryption.
@@ -55,21 +59,39 @@ export interface EncryptedBlob {
* This function must be called before any encryption or decryption can occur.
* @returns A promise that resolves to the generated or existing CryptoKey.
*/
/**
* Get or create session key with automatic rotation.
* Key rotates every 5 minutes or after 1000 operations.
*/
export async function getSessionKey(): Promise<CryptoKey> {
if (sessionKey) {
return sessionKey;
const now = Date.now();
const shouldRotate =
!sessionKey ||
(now - keyCreatedAt) > KEY_ROTATION_INTERVAL ||
keyOperationCount > MAX_KEY_OPERATIONS;
if (shouldRotate) {
if (sessionKey) {
// Note: CryptoKey cannot be explicitly zeroed, but dereferencing helps GC
const elapsed = now - keyCreatedAt;
console.debug?.(`Rotating session key (age: ${elapsed}ms, ops: ${keyOperationCount})`);
sessionKey = null;
}
const key = await window.crypto.subtle.generateKey(
{
name: KEY_ALGORITHM,
length: KEY_LENGTH,
},
false, // non-exportable
['encrypt', 'decrypt'],
);
sessionKey = key;
keyCreatedAt = now;
keyOperationCount = 0;
}
const key = await window.crypto.subtle.generateKey(
{
name: KEY_ALGORITHM,
length: KEY_LENGTH,
},
false, // non-exportable
['encrypt', 'decrypt'],
);
sessionKey = key;
return key;
return sessionKey!;
}
/**
@@ -78,7 +100,10 @@ export async function getSessionKey(): Promise<CryptoKey> {
* @returns A promise that resolves to an EncryptedBlob.
*/
export async function encryptJsonToBlob<T>(data: T): Promise<EncryptedBlob> {
if (!sessionKey) {
const key = await getSessionKey(); // Ensures key exists and handles rotation
keyOperationCount++; // Track operations for rotation
if (!key) {
throw new Error('Session key not initialized. Call getSessionKey() first.');
}
@@ -90,7 +115,7 @@ export async function encryptJsonToBlob<T>(data: T): Promise<EncryptedBlob> {
name: KEY_ALGORITHM,
iv: new Uint8Array(iv),
},
sessionKey,
key,
plaintext,
);
@@ -108,7 +133,10 @@ export async function encryptJsonToBlob<T>(data: T): Promise<EncryptedBlob> {
* @returns A promise that resolves to the original decrypted object.
*/
export async function decryptBlobToJson<T>(blob: EncryptedBlob): Promise<T> {
if (!sessionKey) {
const key = await getSessionKey(); // Ensures key exists and handles rotation
keyOperationCount++; // Track operations for rotation
if (!key) {
throw new Error('Session key not initialized or has been destroyed.');
}
if (blob.v !== 1 || blob.alg !== 'A256GCM') {
@@ -123,7 +151,7 @@ export async function decryptBlobToJson<T>(blob: EncryptedBlob): Promise<T> {
name: KEY_ALGORITHM,
iv: new Uint8Array(iv),
},
sessionKey,
key,
new Uint8Array(ciphertext),
);
@@ -137,6 +165,18 @@ export async function decryptBlobToJson<T>(blob: EncryptedBlob): Promise<T> {
*/
export function destroySessionKey(): void {
sessionKey = null;
keyOperationCount = 0;
keyCreatedAt = 0;
}
// Auto-clear session key when page becomes hidden
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.debug?.('Page hidden - clearing session key for security');
destroySessionKey();
}
});
}
/**