joshlei-cookies
Version:
A secure cookie management library with built-in AES encryption for browser environments
346 lines (299 loc) • 10.9 kB
JavaScript
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}`);
}
};