feat: release 1.0 - rename to Hearbit AI, fix timestamps, update UI

This commit is contained in:
michael.borak
2026-01-20 10:14:07 +01:00
parent 768574709f
commit cd08e1c144
69 changed files with 1369 additions and 545 deletions

92
src/utils/backup.ts Normal file
View 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'}`);
}
}