Files
MindScope/lib/backup-crypto.ts

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