UNPKG

apacuana-sdk-web

Version:

Apacuana SDK for Web

339 lines 14.1 kB
import { ApacuanaWebError, ApacuanaWebErrorCode } from "./errors.js"; const dbName = "cryptoKeysDB"; const storeName = "keys"; const hashAlg = "SHA-512"; const signAlg = "RSASSA-PKCS1-v1_5"; /** * Opens a connection to the IndexedDB database. * @returns A Promise that resolves with the IDBDatabase instance. */ function openDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(dbName, 1); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName, { keyPath: "id" }); } }; request.onerror = (event) => { var _a; return reject(new ApacuanaWebError(`IndexedDB error: ${(_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message}`, ApacuanaWebErrorCode.UNKNOWN_ERROR)); }; request.onsuccess = (event) => { resolve(event.target.result); }; }); } /** * Checks if a record exists in IndexedDB for the given key. * @param keyId The key to look for (e.g., customerId). * @returns A Promise that resolves to true if the record exists, false otherwise. */ export async function checkRecordExists(keyId) { if (!keyId) { return false; } try { const db = await openDB(); if (!db.objectStoreNames.contains(storeName)) { return false; } const transaction = db.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); return await new Promise((resolve, reject) => { // Use count() which is more efficient than get() if we only need to check for existence. const countRequest = store.count(keyId); countRequest.onerror = (event) => { var _a; return reject(new ApacuanaWebError(`Failed to count record: ${(_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message}`, ApacuanaWebErrorCode.UNKNOWN_ERROR)); }; countRequest.onsuccess = () => { // If count is greater than 0, the record exists. resolve(countRequest.result > 0); }; }); } catch (error) { if (error instanceof ApacuanaWebError) throw error; throw new ApacuanaWebError(error instanceof Error ? error.message : "An unknown error occurred during the check process.", ApacuanaWebErrorCode.UNKNOWN_ERROR); } } /** * Stores a value in IndexedDB under a specific field for a given user ID. * This function handles both plain and encrypted values. * @param field The name of the field to store the data under. * @param value The value to store. * @param keyId The user's unique identifier. */ export async function storeValue(field, value, // Cambiado de 'any' a 'unknown' keyId) { if (!field || !keyId) { throw new ApacuanaWebError("Field and keyId are required.", ApacuanaWebErrorCode.VALIDATION_ERROR); } try { const db = await openDB(); const transaction = db.transaction(storeName, "readwrite"); const store = transaction.objectStore(storeName); await new Promise((resolve, reject) => { const getRequest = store.get(keyId); getRequest.onerror = (event) => { var _a; return reject(new ApacuanaWebError(`Failed to get record: ${(_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message}`, ApacuanaWebErrorCode.UNKNOWN_ERROR)); }; getRequest.onsuccess = () => { const item = getRequest.result || { id: keyId }; item[field] = value; const putRequest = store.put(item); putRequest.onerror = (event) => { var _a; return reject(new ApacuanaWebError(`Failed to save record: ${(_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message}`, ApacuanaWebErrorCode.UNKNOWN_ERROR)); }; putRequest.onsuccess = () => resolve(); }; }); } catch (error) { if (error instanceof ApacuanaWebError) throw error; throw new ApacuanaWebError(error instanceof Error ? error.message : "An unknown error occurred during the store process.", ApacuanaWebErrorCode.UNKNOWN_ERROR); } } /** * Retrieves a value from IndexedDB for a given user ID and field. * @param field The name of the field to retrieve. * @param keyId The user's unique identifier. * @returns A Promise that resolves with the retrieved value, or undefined if not found. */ export async function getValue(field, keyId) { if (!field || !keyId) { throw new ApacuanaWebError("Field and keyId are required.", ApacuanaWebErrorCode.VALIDATION_ERROR); } try { const db = await openDB(); if (!db.objectStoreNames.contains(storeName)) { return undefined; } const transaction = db.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); return await new Promise((resolve, reject) => { const getRequest = store.get(keyId); getRequest.onerror = (event) => { var _a; return reject(new ApacuanaWebError(`Failed to get record: ${(_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message}`, ApacuanaWebErrorCode.UNKNOWN_ERROR)); }; getRequest.onsuccess = () => { const item = getRequest.result; resolve(item && item[field] ? item[field] : undefined); }; }); } catch (error) { if (error instanceof ApacuanaWebError) throw error; throw new ApacuanaWebError(error instanceof Error ? error.message : "An unknown error occurred during the get process.", ApacuanaWebErrorCode.UNKNOWN_ERROR); } } /** * Encrypts a value and stores it in IndexedDB. * @param field The name of the field to store the data under. * @param data The data to encrypt and store. * @param keyId The user's unique identifier. * @param password The password used for encryption. */ export async function encryptAndStoreValue(field, data, keyId, password) { // 1. Encriptar los datos const encryptedPayload = await encryptData(data, password); // 2. Usar la función genérica para guardar el objeto encriptado await storeValue(field, encryptedPayload, keyId); } /** * Encrypts data using a password-derived key with AES-GCM. * @param data The data to encrypt (string or ArrayBuffer). * @param password The password to derive the encryption key from. * @returns A Promise that resolves to an object containing the encrypted data, salt, and IV. */ export async function encryptData(data, password) { try { if (!password) { throw new ApacuanaWebError("Password is required for encryption.", ApacuanaWebErrorCode.VALIDATION_ERROR); } if (!data) { throw new ApacuanaWebError("Data is required for encryption.", ApacuanaWebErrorCode.VALIDATION_ERROR); } const enc = new TextEncoder(); const keyMaterial = await window.crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveBits", "deriveKey"]); const salt = window.crypto.getRandomValues(new Uint8Array(16)); const key = await window.crypto.subtle.deriveKey({ name: "PBKDF2", salt: salt, iterations: 100000, hash: { name: hashAlg }, }, keyMaterial, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); const iv = window.crypto.getRandomValues(new Uint8Array(12)); const arrayBuffer = data instanceof ArrayBuffer ? data : isValidBase64(data) ? base64ToArrayBuffer(data) : new TextEncoder().encode(data); const encrypted = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv: iv, }, key, arrayBuffer); return { key: new Uint8Array(encrypted), salt: salt, iv: iv, }; } catch (e) { if (e instanceof ApacuanaWebError) { throw e; } const errorMessage = e instanceof Error ? e.message : "An unknown encryption error occurred"; throw new ApacuanaWebError(errorMessage, ApacuanaWebErrorCode.ENCRYPTION_FAILED); } } function isValidBase64(str) { try { atob(str); return true; } catch (e) { console.log(e); return false; } } function base64ToArrayBuffer(base64) { const binaryString = window.atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } /** * Decrypts data using a password-derived key with AES-GCM. * @param encryptedPayload The object containing encrypted data, salt, and IV. * @param password The password to derive the decryption key from. * @returns A Promise that resolves to the decrypted data as an ArrayBuffer. */ export async function decryptData(encryptedPayload, password) { try { if (!password) { throw new ApacuanaWebError("Password is required for decryption.", ApacuanaWebErrorCode.VALIDATION_ERROR); } if (!encryptedPayload || !encryptedPayload.key || !encryptedPayload.salt || !encryptedPayload.iv) { throw new ApacuanaWebError("Invalid encrypted data payload.", ApacuanaWebErrorCode.VALIDATION_ERROR); } const enc = new TextEncoder(); const keyMaterial = await window.crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]); const key = await window.crypto.subtle.deriveKey({ name: "PBKDF2", salt: new Uint8Array(Object.values(encryptedPayload.salt)), iterations: 100000, hash: { name: "SHA-512" }, }, keyMaterial, { name: "AES-GCM", length: 256 }, true, ["decrypt"]); const decrypted = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv: new Uint8Array(Object.values(encryptedPayload.iv)), }, key, new Uint8Array(Object.values(encryptedPayload.key))); return decrypted; } catch (e) { if (e instanceof ApacuanaWebError) { throw e; } const errorMessage = e instanceof Error ? e.message : "An unknown decryption error occurred"; throw new ApacuanaWebError(errorMessage, ApacuanaWebErrorCode.ENCRYPTION_FAILED); } } /** * Retrieves an encrypted value from IndexedDB and decrypts it. * @param field The field name where the data is stored. * @param keyId The user's unique identifier. * @param password The password for decryption. * @param asString If true, returns the decrypted data as a string. Otherwise, as ArrayBuffer. * @returns The decrypted data, or undefined if not found. */ export async function retrieveAndDecryptValue(field, keyId, password) { try { const encryptedPayload = await getValue(field, keyId); if (!encryptedPayload) { return undefined; // No se encontró el valor } const decryptedBuffer = await decryptData(encryptedPayload, password); return arrayBufferToBase64(decryptedBuffer); } catch (e) { console.log(e, "error"); throw new ApacuanaWebError("Credenciales incorrectas.", ApacuanaWebErrorCode.VALIDATION_ERROR); } } function arrayBufferToBase64(buffer) { // Convertimos el Uint8Array a un array de números estándar const byteArray = Array.from(new Uint8Array(buffer)); // Ahora pasamos el array de números a la función const binary = String.fromCharCode.apply(null, byteArray); return btoa(binary); } export async function signDigest(digest, privateKeyBase64) { const binaryDer = base64ToArrayBuffer(privateKeyBase64); const privateKey = await crypto.subtle.importKey("pkcs8", binaryDer, { name: signAlg, hash: "SHA-256", }, true, ["sign"]); // Firma el digest const signature = await crypto.subtle.sign({ name: signAlg, }, privateKey, base64ToArrayBuffer(digest)); // Convierte la firma a base64 usando la función que ya creamos return arrayBufferToBase64(signature); } /** * Retrieves a full record from IndexedDB for a given key. * @param keyId The key to look for (e.g., customerId). * @returns A Promise that resolves with the record object, or undefined if not found. */ export async function getRecord(keyId) { if (!keyId) { throw new ApacuanaWebError("keyId is required.", ApacuanaWebErrorCode.VALIDATION_ERROR); } try { const db = await openDB(); if (!db.objectStoreNames.contains(storeName)) { return undefined; } const transaction = db.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); return await new Promise((resolve, reject) => { const getRequest = store.get(keyId); getRequest.onerror = (event) => { var _a; return reject(new ApacuanaWebError(`Failed to get record: ${(_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message}`, ApacuanaWebErrorCode.UNKNOWN_ERROR)); }; getRequest.onsuccess = () => { resolve(getRequest.result); }; }); } catch (error) { if (error instanceof ApacuanaWebError) throw error; throw new ApacuanaWebError(error instanceof Error ? error.message : "An unknown error occurred during the get process.", ApacuanaWebErrorCode.UNKNOWN_ERROR); } } //# sourceMappingURL=indexedDB.js.map