feat: release 1.0 - rename to Hearbit AI, fix timestamps, update UI
This commit is contained in:
92
src/utils/backup.ts
Normal file
92
src/utils/backup.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
// Generate a key from a password
|
||||
async function getKey(password: string, salt: Uint8Array): Promise<CryptoKey> {
|
||||
const enc = new TextEncoder();
|
||||
const keyMaterial = await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
enc.encode(password),
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveKey"]
|
||||
);
|
||||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: salt as any,
|
||||
iterations: 100000,
|
||||
hash: "SHA-256"
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
}
|
||||
|
||||
export async function encryptData(data: object, password: string): Promise<string> {
|
||||
const salt = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
const key = await getKey(password, salt);
|
||||
|
||||
const enc = new TextEncoder();
|
||||
const encodedData = enc.encode(JSON.stringify(data));
|
||||
|
||||
const encryptedContent = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: iv
|
||||
},
|
||||
key,
|
||||
encodedData
|
||||
);
|
||||
|
||||
const buffer = new Uint8Array(salt.byteLength + iv.byteLength + encryptedContent.byteLength);
|
||||
buffer.set(salt, 0);
|
||||
buffer.set(iv, salt.byteLength);
|
||||
buffer.set(new Uint8Array(encryptedContent), salt.byteLength + iv.byteLength);
|
||||
|
||||
// Safer binary to string conversion to avoid stack overflow with spread operator
|
||||
let binary = '';
|
||||
const len = buffer.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(buffer[i]);
|
||||
}
|
||||
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
export async function decryptData(base64Data: string, password: string): Promise<any> {
|
||||
try {
|
||||
const binaryString = atob(base64Data.trim());
|
||||
const len = binaryString.length;
|
||||
const buffer = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
buffer[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const salt = buffer.slice(0, 16);
|
||||
const iv = buffer.slice(16, 28);
|
||||
const ciphertext = buffer.slice(28);
|
||||
|
||||
const key = await getKey(password, salt);
|
||||
|
||||
const decryptedContent = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: iv
|
||||
},
|
||||
key,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
const dec = new TextDecoder();
|
||||
return JSON.parse(dec.decode(decryptedContent));
|
||||
} catch (e: any) {
|
||||
console.error("Decryption internal error:", e);
|
||||
// Distinguish between password error (OperationError) and others if possible
|
||||
if (e.name === 'OperationError') {
|
||||
throw new Error("Incorrect password.");
|
||||
}
|
||||
throw new Error(`Import failed: ${e.message || 'Corrupted file'}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user