UNPKG

joshlei-cookies

Version:

A secure cookie management library with built-in AES encryption for browser environments

346 lines (299 loc) 10.9 kB
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'; let cryptoAPI; const isCryptoAvailable = typeof crypto !== 'undefined' && crypto.subtle; const initCrypto = async () => { if (cryptoAPI) return cryptoAPI; if (isCryptoAvailable) { cryptoAPI = crypto; } else { try { if (typeof window === 'undefined') { const nodeCrypto = await import('crypto'); cryptoAPI = { subtle: { importKey: async (format, keyData, algorithm, extractable, keyUsages) => { return { keyData, algorithm }; }, encrypt: async (algorithm, key, data) => { const cipher = nodeCrypto.createCipher('aes-256-cbc', Buffer.from(key.keyData)); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); return Buffer.from(encrypted, 'hex'); }, decrypt: async (algorithm, key, encryptedData) => { const decipher = nodeCrypto.createDecipher('aes-256-cbc', Buffer.from(key.keyData)); let decrypted = decipher.update(Buffer.from(encryptedData), 'hex', 'utf8'); decrypted += decipher.final('utf8'); return Buffer.from(decrypted, 'utf8'); }, digest: async (algorithm, data) => { const hash = nodeCrypto.createHash('sha256'); hash.update(data); return hash.digest(); } }, getRandomValues: (array) => { const randomBytes = nodeCrypto.randomBytes(array.length); array.set(randomBytes); return array; } }; } else { throw new Error('Node.js crypto not available in browser'); } } catch (nodeError) { cryptoAPI = { subtle: { importKey: async (format, keyData, algorithm, extractable, keyUsages) => { return { keyData, algorithm }; }, encrypt: async (algorithm, key, data) => { const keyBytes = new Uint8Array(key.keyData); const dataBytes = new Uint8Array(data); const encrypted = new Uint8Array(dataBytes.length); for (let i = 0; i < dataBytes.length; i++) { encrypted[i] = dataBytes[i] ^ keyBytes[i % keyBytes.length]; } return encrypted.buffer; }, decrypt: async (algorithm, key, encryptedData) => { const keyBytes = new Uint8Array(key.keyData); const encryptedBytes = new Uint8Array(encryptedData); const decrypted = new Uint8Array(encryptedBytes.length); for (let i = 0; i < encryptedBytes.length; i++) { decrypted[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length]; } return decrypted.buffer; }, digest: async (algorithm, data) => { const dataBytes = new Uint8Array(data); const hash = new Uint8Array(32); let hashValue = 0; for (let i = 0; i < dataBytes.length; i++) { hashValue = ((hashValue << 5) - hashValue + dataBytes[i]) & 0xffffffff; } for (let i = 0; i < 32; i++) { hash[i] = (hashValue >> (i % 32)) & 0xff; } return hash.buffer; } }, getRandomValues: (array) => { for (let i = 0; i < array.length; i++) { array[i] = Math.floor(Math.random() * 256); } return array; } }; console.warn('⚠️ Using fallback crypto implementation. For production use, ensure Web Crypto API or Node.js crypto is available.'); } } return cryptoAPI; }; const CookieManager = { set: (name, value, options = {}) => { if (!isBrowser) { throw new Error('Cookies can only be set in a browser environment'); } let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; if (options.expires) { const date = new Date(); let milliseconds = 0; if (typeof options.expires === 'string') { const match = options.expires.match(/^(\d+)([dmw])$/i); if (!match) { throw new Error('Invalid expires format. Use number (days) or string like "1d", "2w", "3m"'); } const value = parseInt(match[1]); const unit = match[2].toLowerCase(); switch (unit) { case 'd': milliseconds = value * 24 * 60 * 60 * 1000; break; case 'w': milliseconds = value * 7 * 24 * 60 * 60 * 1000; break; case 'm': milliseconds = value * 30 * 24 * 60 * 60 * 1000; break; } } else if (typeof options.expires === 'number') { milliseconds = options.expires * 24 * 60 * 60 * 1000; } else { throw new Error('Expires must be a number (days) or string like "1d", "2w", "3m"'); } date.setTime(date.getTime() + milliseconds); cookieString += `; expires=${date.toUTCString()}`; } if (options.path) { cookieString += `; path=${options.path}`; } else { cookieString += '; path=/'; } if (options.domain) { cookieString += `; domain=${options.domain}`; } if (options.secure) { cookieString += '; secure'; } if (options.sameSite) { cookieString += `; samesite=${options.sameSite}`; } document.cookie = cookieString; }, get: (name) => { if (!isBrowser) { throw new Error('Cookies can only be accessed in a browser environment'); } if (!document.cookie) return null; const cookies = document.cookie.split(';'); for (let cookie of cookies) { cookie = cookie.trim(); if (cookie.indexOf('=') === -1) continue; const [key, ...valueParts] = cookie.split('='); const value = valueParts.join('='); if (decodeURIComponent(key) === name) { return decodeURIComponent(value); } } return null; }, remove: (name, options = {}) => { if (!isBrowser) { throw new Error('Cookies can only be removed in a browser environment'); } let cookieString = `${encodeURIComponent(name)}=; expires=Thu, 01 Jan 1970 00:00:00 UTC`; if (options.path) { cookieString += `; path=${options.path}`; } else { cookieString += '; path=/'; } if (options.domain) { cookieString += `; domain=${options.domain}`; } document.cookie = cookieString; }, }; const importKey = async (rawKey) => { try { const crypto = await initCrypto(); return await crypto.subtle.importKey( 'raw', rawKey, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt'] ); } catch (error) { throw new Error(`Failed to import encryption key: ${error.message}`); } }; const CryptoManager = { encrypt: async (data, rawKey) => { try { const crypto = await initCrypto(); const key = await importKey(rawKey); const encoder = new TextEncoder(); const iv = crypto.getRandomValues(new Uint8Array(16)); const encrypted = await crypto.subtle.encrypt( { name: 'AES-CBC', iv }, key, encoder.encode(data) ); return JSON.stringify({ iv: Array.from(iv), data: Array.from(new Uint8Array(encrypted)), }); } catch (error) { throw new Error(`Encryption failed: ${error.message}`); } }, decrypt: async (encryptedData, rawKey) => { try { const crypto = await initCrypto(); const key = await importKey(rawKey); const { iv, data } = JSON.parse(encryptedData); if (!iv || !data) { throw new Error('Invalid encrypted data format'); } const decrypted = await crypto.subtle.decrypt( { name: 'AES-CBC', iv: new Uint8Array(iv) }, key, new Uint8Array(data) ); return new TextDecoder().decode(decrypted); } catch (error) { throw new Error(`Decryption failed: ${error.message}`); } }, }; let hashedKey = null; const hashKey = async (key) => { const crypto = await initCrypto(); const encoder = new TextEncoder(); const data = encoder.encode(key); const hashBuffer = await crypto.subtle.digest('SHA-256', data); return new Uint8Array(hashBuffer); }; export const jstudio = { setKey: async (key) => { if (typeof key !== 'string') { throw new Error('Secret key must be a string.'); } if (key.length !== 64) { throw new Error('Secret key must be exactly 64 characters long.'); } try { hashedKey = await hashKey(key); } catch (error) { throw new Error(`Failed to set secret key: ${error.message}`); } }, }; export const SetJSCookie = async (name, data, options = {}) => { if (!hashedKey) { throw new Error('Secret key is not set. Use jstudio.setKey() to set it.'); } if (typeof name !== 'string' || name.trim() === '') { throw new Error('Cookie name must be a non-empty string.'); } if (data === undefined || data === null) { throw new Error('Cookie data cannot be undefined or null.'); } try { const encryptedData = await CryptoManager.encrypt(JSON.stringify(data), hashedKey); CookieManager.set(name, encryptedData, options); } catch (error) { throw new Error(`Failed to set cookie: ${error.message}`); } }; export const GetJSCookie = async (name) => { if (!hashedKey) { throw new Error('Secret key is not set. Use jstudio.setKey() to set it.'); } if (typeof name !== 'string' || name.trim() === '') { throw new Error('Cookie name must be a non-empty string.'); } try { const encryptedData = CookieManager.get(name); if (!encryptedData) return null; const decryptedData = await CryptoManager.decrypt(encryptedData, hashedKey); return JSON.parse(decryptedData); } catch (error) { try { CookieManager.remove(name); } catch (removeError) { } return null; } }; export const RemoveJSCookie = (name, options = {}) => { if (typeof name !== 'string' || name.trim() === '') { throw new Error('Cookie name must be a non-empty string.'); } try { CookieManager.remove(name, options); } catch (error) { throw new Error(`Failed to remove cookie: ${error.message}`); } };