strata-storage
Version:
Zero-dependency universal storage plugin providing a unified API for all storage operations across web, Android, and iOS platforms
174 lines (173 loc) • 6.14 kB
JavaScript
/**
* Encryption Feature - Web Crypto API implementation
* Zero-dependency encryption/decryption for storage values
*/
import { EncryptionError } from "../utils/errors.js";
/**
* Encryption manager using Web Crypto API
*/
export class EncryptionManager {
config;
keyCache = new Map();
constructor(config = {}) {
this.config = {
algorithm: config.algorithm || 'AES-GCM',
keyLength: config.keyLength || 256,
iterations: config.iterations || 100000,
saltLength: config.saltLength || 16,
};
}
/**
* Check if encryption is available
*/
isAvailable() {
return typeof window !== 'undefined' && window.crypto && window.crypto.subtle !== undefined;
}
/**
* Encrypt data
*/
async encrypt(data, password) {
if (!this.isAvailable()) {
throw new EncryptionError('Web Crypto API not available');
}
try {
// Convert data to string
const dataStr = JSON.stringify(data);
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(dataStr);
// Generate salt and IV
const salt = crypto.getRandomValues(new Uint8Array(this.config.saltLength));
const iv = crypto.getRandomValues(new Uint8Array(12)); // 12 bytes for GCM
// Derive key from password
const key = await this.deriveKey(password, salt);
// Encrypt data
const encryptedBuffer = await crypto.subtle.encrypt({
name: this.config.algorithm,
iv: iv,
}, key, dataBuffer);
// Convert to base64 for storage
return {
data: this.bufferToBase64(encryptedBuffer),
salt: this.bufferToBase64(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)),
iv: this.bufferToBase64(iv.buffer.slice(iv.byteOffset, iv.byteOffset + iv.byteLength)),
algorithm: this.config.algorithm,
iterations: this.config.iterations,
};
}
catch (error) {
throw new EncryptionError(`Encryption failed: ${error}`);
}
}
/**
* Decrypt data
*/
async decrypt(encryptedData, password) {
if (!this.isAvailable()) {
throw new EncryptionError('Web Crypto API not available');
}
try {
// Convert from base64
const dataBuffer = this.base64ToBuffer(encryptedData.data);
const salt = this.base64ToBuffer(encryptedData.salt);
const iv = this.base64ToBuffer(encryptedData.iv);
// Derive key from password
const key = await this.deriveKey(password, new Uint8Array(salt), encryptedData.iterations);
// Decrypt data
const decryptedBuffer = await crypto.subtle.decrypt({
name: encryptedData.algorithm,
iv: iv,
}, key, dataBuffer);
// Convert back to original data
const decoder = new TextDecoder();
const decryptedStr = decoder.decode(decryptedBuffer);
return JSON.parse(decryptedStr);
}
catch (error) {
throw new EncryptionError(`Decryption failed: ${error}`);
}
}
/**
* Derive encryption key from password
*/
async deriveKey(password, salt, iterations = this.config.iterations) {
// Check cache
const saltBuffer = salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength);
const cacheKey = `${password}-${this.bufferToBase64(saltBuffer)}-${iterations}`;
if (this.keyCache.has(cacheKey)) {
return this.keyCache.get(cacheKey);
}
// Import password as key material
const encoder = new TextEncoder();
const passwordBuffer = encoder.encode(password);
const keyMaterial = await crypto.subtle.importKey('raw', passwordBuffer, 'PBKDF2', false, [
'deriveBits',
'deriveKey',
]);
// Derive key using PBKDF2
const key = await crypto.subtle.deriveKey({
name: 'PBKDF2',
salt: saltBuffer,
iterations: iterations,
hash: 'SHA-256',
}, keyMaterial, {
name: this.config.algorithm,
length: this.config.keyLength,
}, false, ['encrypt', 'decrypt']);
// Cache the key
this.keyCache.set(cacheKey, key);
return key;
}
/**
* Convert ArrayBuffer to base64
*/
bufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* Convert base64 to ArrayBuffer
*/
base64ToBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
/**
* Clear key cache
*/
clearCache() {
this.keyCache.clear();
}
/**
* Generate a secure random password
*/
generatePassword(length = 32) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
const array = new Uint8Array(length);
crypto.getRandomValues(array);
let password = '';
for (let i = 0; i < length; i++) {
password += chars[array[i] % chars.length];
}
return password;
}
/**
* Hash data using SHA-256
*/
async hash(data) {
if (!this.isAvailable()) {
throw new EncryptionError('Web Crypto API not available');
}
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
return this.bufferToBase64(hashBuffer);
}
}