Skip to main content
Glama
decryptCookie.js12.8 kB
import crypto from 'crypto'; /** * Converts a hex string to a byte array * @param {string} hex - Hex string * @returns {Buffer} - Byte array */ function hexStringToByteArray(hex) { hex = hex.replace(/-/g, '').replace(/\s/g, ''); const bytes = Buffer.alloc(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } return bytes; } /** * Rounds up number of bits to number of bytes * @param {number} numBits - Number of bits * @returns {number} - Number of bytes */ function roundupNumBitsToNumBytes(numBits) { if (numBits < 0) return 0; return Math.floor(numBits / 8) + ((numBits & 7) !== 0 ? 1 : 0); } /** * Checks and removes HMAC-SHA256 hash from the end of the buffer * @param {Buffer} bufHashed - Buffer with hash appended * @param {Buffer} validationKey - Validation key for HMAC * @param {number} hashSize - Size of the hash (32 for SHA256) * @returns {Buffer|null} - Buffer without hash, or null if verification fails */ function checkHashAndRemove(bufHashed, validationKey, hashSize) { if (!bufHashed || bufHashed.length < hashSize) { return null; } const dataLength = bufHashed.length - hashSize; const data = bufHashed.slice(0, dataLength); const hash = bufHashed.slice(dataLength); // Compute HMAC-SHA256 const hmac = crypto.createHmac('sha256', validationKey); hmac.update(data); const computedHash = hmac.digest(); // Constant-time comparison to prevent timing attacks let hashCheckFailed = false; for (let i = 0; i < hashSize; i++) { if (computedHash[i] !== hash[i]) { hashCheckFailed = true; } } if (hashCheckFailed) { return null; } return data; } /** * Checks hash at a specific position in the buffer * @param {Buffer} decryptedCookie - Decrypted cookie buffer * @param {number} hashIndex - Index where hash starts * @param {Buffer} validationKey - Validation key for HMAC * @param {number} hashSize - Size of the hash * @returns {boolean} - True if hash is valid */ function checkHash(decryptedCookie, hashIndex, validationKey, hashSize) { const data = decryptedCookie.slice(0, hashIndex); const hash = decryptedCookie.slice(hashIndex, hashIndex + hashSize); const hmac = crypto.createHmac('sha256', validationKey); hmac.update(data); const computedHash = hmac.digest(); if (computedHash.length !== hashSize) { throw new Error(`Invalid hash length: ${computedHash.length}, expected ${hashSize}`); } // Constant-time comparison let hashCheckFailed = false; for (let i = 0; i < hashSize; i++) { if (computedHash[i] !== hash[i]) { hashCheckFailed = true; } } return !hashCheckFailed; } /** * Decrypts the cookie blob using AES-CBC * @param {Buffer} cookieBlob - Encrypted cookie blob * @param {Buffer} decryptionKey - Decryption key * @param {Buffer} validationKey - Validation key * @param {number} hashSize - Hash size (32 for SHA256) * @returns {Buffer} - Decrypted data */ function decrypt(cookieBlob, decryptionKey, validationKey, hashSize) { // Check and remove hash signature let data = checkHashAndRemove(cookieBlob, validationKey, hashSize); if (!data || data.length === 0) { throw new Error('Signature verification failed'); } // Decrypt using AES-CBC with zero IV // Determine AES algorithm based on key size let algorithm; if (decryptionKey.length === 16) { algorithm = 'aes-128-cbc'; } else if (decryptionKey.length === 24) { algorithm = 'aes-192-cbc'; } else if (decryptionKey.length === 32) { algorithm = 'aes-256-cbc'; } else { // Default to AES-128, use first 16 bytes algorithm = 'aes-128-cbc'; } const iv = Buffer.alloc(16, 0); // Zero IV const key = decryptionKey.length <= 32 ? decryptionKey : decryptionKey.slice(0, 32); const decipher = crypto.createDecipheriv(algorithm, key, iv); let decrypted = Buffer.concat([decipher.update(data), decipher.final()]); // Remove the IV/salt from the beginning const keySize = decryptionKey.length * 8; const ivLength = roundupNumBitsToNumBytes(keySize); const dataLength = decrypted.length - ivLength; if (dataLength < 0) { throw new Error(`Unexpected salt length: ${ivLength}. Total: ${decrypted.length}`); } return decrypted.slice(ivLength); } /** * Reads a 7-bit encoded integer from the buffer * @param {Buffer} buffer - Buffer to read from * @param {number} offset - Starting offset * @returns {{value: number, bytesRead: number}} - The integer value and number of bytes read */ function read7BitEncodedInt(buffer, offset) { let value = 0; let shift = 0; let bytesRead = 0; let byte; do { if (offset + bytesRead >= buffer.length) { throw new Error('Invalid 7-bit encoded integer'); } byte = buffer[offset + bytesRead]; bytesRead++; value |= (byte & 0x7F) << shift; shift += 7; } while ((byte & 0x80) !== 0); return { value, bytesRead }; } /** * Reads a binary string (UTF-16LE) from the buffer * @param {Buffer} buffer - Buffer to read from * @param {number} offset - Starting offset * @returns {{value: string, bytesRead: number}} - The string value and number of bytes read */ function readBinaryString(buffer, offset) { const { value: charCount, bytesRead: lengthBytes } = read7BitEncodedInt(buffer, offset); const stringBytes = charCount * 2; if (offset + lengthBytes + stringBytes > buffer.length) { throw new Error('Invalid binary string length'); } const stringBuffer = buffer.slice(offset + lengthBytes, offset + lengthBytes + stringBytes); let result = ''; for (let i = 0; i < charCount; i++) { const charCode = stringBuffer[i * 2] | (stringBuffer[i * 2 + 1] << 8); result += String.fromCharCode(charCode); } return { value: result, bytesRead: lengthBytes + stringBytes }; } /** * Deserializes a FormsAuthenticationTicket from the buffer * @param {Buffer} serializedTicket - Serialized ticket buffer * @param {number} serializedTicketLength - Length of the ticket (excluding hash) * @returns {Object|null} - Deserialized ticket object or null if invalid */ function deserializeFormsAuthenticationTicket(serializedTicket, serializedTicketLength) { const CURRENT_TICKET_SERIALIZED_VERSION = 0x01; // .NET DateTime ticks epoch: 621355968000000000 ticks = 1970-01-01 00:00:00.0000000 UTC const EPOCH_TICKS = BigInt('621355968000000000'); let offset = 0; try { // Step 1: Read serialized format version (1 byte) if (offset >= serializedTicket.length) return null; const serializedFormatVersion = serializedTicket[offset++]; if (serializedFormatVersion !== CURRENT_TICKET_SERIALIZED_VERSION) { return null; } // Step 2: Read ticket version (1 byte) if (offset >= serializedTicket.length) return null; const ticketVersion = serializedTicket[offset++]; // Step 3: Read ticket issue date (8 bytes - Int64) // .NET DateTime ticks are 100-nanosecond intervals since 0001-01-01 00:00:00.0000000 UTC if (offset + 8 > serializedTicket.length) return null; const ticketIssueDateUtcTicks = serializedTicket.readBigInt64LE(offset); offset += 8; const ticksSinceEpoch = ticketIssueDateUtcTicks - EPOCH_TICKS; const milliseconds = Number(ticksSinceEpoch) / 10000; const ticketIssueDateUtc = new Date(milliseconds); // Step 4: Read spacer (1 byte) if (offset >= serializedTicket.length) return null; const spacer = serializedTicket[offset++]; if (spacer !== 0xfe) { return null; } // Step 5: Read ticket expiration date (8 bytes - Int64) if (offset + 8 > serializedTicket.length) return null; const ticketExpirationDateUtcTicks = serializedTicket.readBigInt64LE(offset); offset += 8; const expirationTicksSinceEpoch = ticketExpirationDateUtcTicks - EPOCH_TICKS; const expirationMilliseconds = Number(expirationTicksSinceEpoch) / 10000; const ticketExpirationDateUtc = new Date(expirationMilliseconds); // Step 6: Read ticket persistence (1 byte) if (offset >= serializedTicket.length) return null; const ticketPersistenceFieldValue = serializedTicket[offset++]; let ticketIsPersistent; if (ticketPersistenceFieldValue === 0) { ticketIsPersistent = false; } else if (ticketPersistenceFieldValue === 1) { ticketIsPersistent = true; } else { return null; } // Step 7: Read ticket username const usernameResult = readBinaryString(serializedTicket, offset); offset += usernameResult.bytesRead; const ticketName = usernameResult.value; // Step 8: Read ticket custom data const userDataResult = readBinaryString(serializedTicket, offset); offset += userDataResult.bytesRead; const ticketUserData = userDataResult.value; // Step 9: Read ticket cookie path const cookiePathResult = readBinaryString(serializedTicket, offset); offset += cookiePathResult.bytesRead; const ticketCookiePath = cookiePathResult.value; // Step 10: Read footer (1 byte) if (offset >= serializedTicket.length) return null; const footer = serializedTicket[offset++]; if (footer !== 0xff) { return null; } // Step 11: Verify we consumed the entire payload if (offset !== serializedTicketLength) { return null; } // Convert UTC dates to local time const ticketIssueDate = new Date(ticketIssueDateUtc); const ticketExpiration = new Date(ticketExpirationDateUtc); return { version: ticketVersion, name: ticketName, issueDate: ticketIssueDate, expiration: ticketExpiration, isPersistent: ticketIsPersistent, userData: ticketUserData, cookiePath: ticketCookiePath, expired: new Date() > ticketExpiration }; } catch (error) { // Invalid ticket format - return null to indicate failure // This matches the C# implementation behavior return null; } } /** * Decrypts an authentication cookie * @param {string} cookieString - Hex-encoded encrypted cookie string * @param {string} decryptionKeyHex - Hex-encoded decryption key * @param {string} validationKeyHex - Hex-encoded validation key * @returns {Object|null} - Decrypted ticket object or null if decryption fails */ function decryptCookie(cookieString) { const validationKeyHex = 'EDAD268C6CA3E5E0160B8D5D4DABFDBF33A5E132D33C943E0AFDB64920D804309074DEFA97962FF473D2C60E04CC3E025F11FEABF053BD1D0F21B6815B225371'; const decryptionKeyHex = '278B0D5F1CF1397E2370BE2C5B62F8724D4879491917DAEA'; const HASH_SIZE = 32; // SHA256 hash size // Convert hex strings to byte arrays let cookieBlob; try { if (cookieString.length % 2 !== 0) { return null; } cookieBlob = hexStringToByteArray(cookieString); } catch (error) { // Invalid hex string format - return null to indicate failure return null; } if (!cookieBlob || cookieBlob.length === 0) { return null; } const decryptionKey = hexStringToByteArray(decryptionKeyHex); const validationKey = hexStringToByteArray(validationKeyHex); // Decrypt the cookie let decryptedBlob; try { decryptedBlob = decrypt(cookieBlob, decryptionKey, validationKey, HASH_SIZE); } catch (error) { throw new Error(`Decryption failed: ${error.message}`); } // Verify hash after decryption const ticketLength = decryptedBlob.length - HASH_SIZE; if (ticketLength < 0) { throw new Error('Invalid decrypted blob length'); } const isValidHash = checkHash(decryptedBlob, ticketLength, validationKey, HASH_SIZE); if (!isValidHash) { throw new Error('Invalid hash generated after decryption'); } // Deserialize the ticket const ticket = deserializeFormsAuthenticationTicket(decryptedBlob, ticketLength); const profileId = ticket.userData.split('|')[0]; const globalCustomerId = ticket.userData.split('|')[1]; return { profileId, globalCustomerId }; } // Export functions for use as a module export { decryptCookie, hexStringToByteArray, deserializeFormsAuthenticationTicket };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/manishgadhock-monotype/monotype-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server