apacuana-sdk-web
Version:
Apacuana SDK for Web
339 lines • 14.1 kB
JavaScript
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