78 lines
2.4 KiB
TypeScript
78 lines
2.4 KiB
TypeScript
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('密码错误或备份文件已损坏');
|
|
}
|
|
}
|