cookie-to-guard
Version:
Library for protecting cookies from tampering
246 lines (215 loc) • 9.15 kB
JavaScript
/**
* CookieGuard - Advanced cookie protection library
* @version 1.0.0
* @license MIT
*/
// Генерация HMAC подписи (более безопасный вариант)
const generateHMAC = (message, secret) => {
// В реальной реализации используйте криптографические библиотеки
// Это упрощенный пример для демонстрации
let hash = 0;
for (let i = 0; i < message.length; i++) {
const char = message.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash |= 0; // Преобразование в 32-битное целое
}
return hash.toString(36) + secret;
};
class CookieGuard {
/**
* @param {Object} options - Configuration options
* @param {string} options.cookieName - Name of cookie to protect
* @param {string} [options.redirectUrl] - Redirect URL when tampering detected
* @param {Function} [options.onDetect] - Callback when tampering detected
* @param {boolean} [options.autoReset=true] - Auto-reset cookie on tampering
* @param {string} [options.domain] - Cookie domain
* @param {boolean} [options.secure=false] - Secure cookie flag
* @param {string} [options.sameSite='Lax'] - SameSite cookie attribute
*/
constructor(options) {
if (!options || !options.cookieName) {
throw new Error('CookieGuard: cookieName is required');
}
this.config = {
cookieName: options.cookieName,
signatureName: `${options.cookieName}_sig`,
redirectUrl: options.redirectUrl || null,
onDetect: options.onDetect || null,
autoReset: options.autoReset !== false,
domain: options.domain || null,
secure: options.secure || false,
sameSite: options.sameSite || 'Lax',
secret: options.secret || this._generateSecret()
};
this._initialize();
}
// ... (остальные методы остаются аналогичными, но с улучшениями)
/**
* Устанавливает защищенную куку
* @param {string} value - Значение куки
* @param {Object} [options] - Дополнительные параметры
* @param {number} [options.days=365] - Срок жизни в днях
* @param {string} [options.path='/'] - Путь куки
*/
setSecureCookie(value, options = {}) {
const { days = 365, path = '/' } = options;
const signature = generateHMAC(value, this.config.secret);
let cookieString = `${this.config.cookieName}=${encodeURIComponent(value)}; ` +
`expires=${new Date(Date.now() + days * 864e5).toUTCString()}; ` +
`path=${path}; ` +
`SameSite=${this.config.sameSite}; `;
if (this.config.domain) cookieString += `domain=${this.config.domain}; `;
if (this.config.secure) cookieString += 'Secure; ';
document.cookie = cookieString;
// Устанавливаем подпись
document.cookie = `${this.config.signatureName}=${encodeURIComponent(signature)}; ` +
cookieString.split('; ').slice(1).join('; ');
}
_initialize() {
if (this.initialized) return;
// Проверяем куки при загрузке
this._verifyCookie();
// Устанавливаем обработчик для проверки куки перед отправкой на сервер
if (typeof window !== 'undefined') {
const originalFetch = window.fetch;
const that = this;
window.fetch = async function(...args) {
that._verifyCookie();
return originalFetch.apply(this, args);
};
// Также можно перехватывать XMLHttpRequest, но для современного кода fetch достаточно
}
this.initialized = true;
}
/**
* Генерация секретного ключа для подписи
* @returns {string}
* @private
*/
_generateSecretKey() {
// В реальном приложении лучше использовать более сложную логику генерации
return 'cg_' + Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
/**
* Получает значение куки по имени
* @param {string} name - Имя куки
* @returns {string|null}
* @private
*/
_getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(name + '=')) {
return decodeURIComponent(cookie.substring(name.length + 1));
}
}
return null;
}
/**
* Устанавливает куки
* @param {string} name - Имя куки
* @param {string} value - Значение куки
* @param {number} days - Срок жизни в днях
* @private
*/
_setCookie(name, value, days = 365) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = 'expires=' + date.toUTCString();
document.cookie = name + '=' + encodeURIComponent(value) + ';' + expires + ';path=/';
}
/**
* Удаляет куки
* @param {string} name - Имя куки
* @private
*/
_deleteCookie(name) {
document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
}
/**
* Создает подпись для значения куки
* @param {string} value - Значение куки
* @returns {string}
* @private
*/
_createSignature(value) {
// В реальном приложении следует использовать более надежный алгоритм подписи
return value.split('').reverse().join('') + this.secretKey;
}
/**
* Проверяет подпись куки
* @param {string} value - Значение куки
* @param {string} signature - Подпись
* @returns {boolean}
* @private
*/
_verifySignature(value, signature) {
return this._createSignature(value) === signature;
}
/**
* Проверяет целостность куки
* @private
*/
_verifyCookie() {
const cookieValue = this._getCookie(this.cookieName);
const signatureValue = this._getCookie(this.cookieName + '_sig');
if (cookieValue && signatureValue) {
if (!this._verifySignature(cookieValue, signatureValue)) {
this._handleCookieTampering();
}
} else if (cookieValue && !signatureValue) {
// Куки есть, но подписи нет - возможно, первое использование
this._setCookie(this.cookieName + '_sig', this._createSignature(cookieValue));
}
}
/**
* Обработка обнаружения изменения куки
* @private
*/
_handleCookieTampering() {
if (this.onDetect && typeof this.onDetect === 'function') {
this.onDetect();
}
if (this.autoReset) {
this._deleteCookie(this.cookieName);
this._deleteCookie(this.cookieName + '_sig');
}
if (this.redirectUrl) {
window.location.href = this.redirectUrl;
}
}
/**
* Устанавливает защищенное значение куки
* @param {string} value - Значение куки
* @param {number} [days=365] - Срок жизни в днях
*/
setSecureCookie(value, days = 365) {
this._setCookie(this.cookieName, value, days);
this._setCookie(this.cookieName + '_sig', this._createSignature(value), days);
}
/**
* Получает защищенное значение куки
* @returns {string|null}
*/
getSecureCookie() {
this._verifyCookie();
return this._getCookie(this.cookieName);
}
/**
* Удаляет защищенную куки
*/
deleteSecureCookie() {
this._deleteCookie(this.cookieName);
this._deleteCookie(this.cookieName + '_sig');
}
}
// Экспорт для разных сред
if (typeof module !== 'undefined' && module.exports) {
module.exports = CookieGuard;
} else if (typeof define === 'function' && define.amd) {
define([], () => CookieGuard);
} else if (typeof window !== 'undefined') {
window.CookieGuard = CookieGuard;
}