feat: 增加本地测评档案与长期追踪
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { EncryptedMindScopeBackup, MindScopeBackup } from '@/lib/assessment-types';
|
||||
|
||||
const ITERATIONS = 250_000;
|
||||
|
||||
function toBase64(bytes: Uint8Array) {
|
||||
let binary = '';
|
||||
bytes.forEach((byte) => { binary += String.fromCharCode(byte); });
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function fromBase64(value: string) {
|
||||
const binary = atob(value);
|
||||
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
||||
}
|
||||
|
||||
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
||||
const copy = new Uint8Array(bytes.byteLength);
|
||||
copy.set(bytes);
|
||||
return copy.buffer;
|
||||
}
|
||||
|
||||
async function deriveKey(password: string, salt: Uint8Array, iterations: number) {
|
||||
const material = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveKey'],
|
||||
);
|
||||
return crypto.subtle.deriveKey(
|
||||
{ name: 'PBKDF2', hash: 'SHA-256', salt: toArrayBuffer(salt), iterations },
|
||||
material,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt'],
|
||||
);
|
||||
}
|
||||
|
||||
export async function encryptBackup(backup: MindScopeBackup, password: string): Promise<EncryptedMindScopeBackup> {
|
||||
if (password.length < 8) throw new Error('备份密码至少需要 8 个字符');
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const key = await deriveKey(password, salt, ITERATIONS);
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: toArrayBuffer(iv) },
|
||||
key,
|
||||
new TextEncoder().encode(JSON.stringify(backup)),
|
||||
);
|
||||
return {
|
||||
format: 'mindscope-encrypted-backup',
|
||||
version: 1,
|
||||
algorithm: 'AES-GCM',
|
||||
iterations: ITERATIONS,
|
||||
salt: toBase64(salt),
|
||||
iv: toBase64(iv),
|
||||
data: toBase64(new Uint8Array(encrypted)),
|
||||
};
|
||||
}
|
||||
|
||||
export async function decryptBackup(backup: EncryptedMindScopeBackup, password: string): Promise<MindScopeBackup> {
|
||||
if (backup.format !== 'mindscope-encrypted-backup' || backup.version !== 1) {
|
||||
throw new Error('不支持的加密备份格式');
|
||||
}
|
||||
try {
|
||||
const salt = fromBase64(backup.salt);
|
||||
const iv = fromBase64(backup.iv);
|
||||
const key = await deriveKey(password, salt, backup.iterations);
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: toArrayBuffer(iv) },
|
||||
key,
|
||||
toArrayBuffer(fromBase64(backup.data)),
|
||||
);
|
||||
return JSON.parse(new TextDecoder().decode(decrypted)) as MindScopeBackup;
|
||||
} catch {
|
||||
throw new Error('密码错误或备份文件已损坏');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user