Instantly share code, notes, and snippets.
Last active
July 16, 2020 09:20
-
Save byrnedo/a3efad132bf4cef83c1f7d29b67a22d9 to your computer and use it in GitHub Desktop.
Javascript module to encrypt, decrtypt, serialize and deserialize a DES encrypted cookie from a Dotnet Aspnet application
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const crypto = require('crypto'); | |
const Long = require('mongodb').Long; | |
const ticksUntilEpoch = "621355968000000000"; | |
const millisInTick = "10000"; | |
const secsInTick = "10000000"; | |
const decrypt = (cookieValue, decryptionKey, validationKey) => { | |
// Parameters | |
const hmacSize = 20; | |
// Make buffers for input | |
const cookieBuffer = Buffer.from(cookieValue, 'hex'); | |
const decryptionKeyBuffer = Buffer.from(decryptionKey, 'hex'); | |
const validationKeyBuffer = Buffer.from(validationKey, 'hex'); | |
// Parse cookie | |
// cipher | |
const cipherText = Buffer.alloc(cookieBuffer.length - hmacSize); | |
// copy from input[0:ciptherTextLength] to cipherText | |
let curOffset = cookieBuffer.copy(cipherText, 0, 0, cipherText.length); | |
// alloc hmac buf | |
const hmac = Buffer.alloc(hmacSize); | |
// copy from input[cipherTextLength:+=hmacLength] to hmacBuf | |
curOffset += cookieBuffer.copy(hmac, 0, curOffset, curOffset + hmac.length); | |
// Verify HMAC | |
const h = crypto.createHmac('sha1', validationKeyBuffer); | |
h.update(cipherText); | |
const expectedHmac = h.digest(); | |
if (!expectedHmac.equals(hmac)) { // Note: Requires nodejs v0.11.13 | |
throw 'Cookie integrity error'; | |
} | |
// Decrypt | |
// iv same length as key | |
const zeroIv = Buffer.from("0000000000000000", 'hex'); | |
const c = crypto.createDecipheriv('des', decryptionKeyBuffer, zeroIv); | |
const plaintext = Buffer.concat([c.update(cipherText), c.final()]); | |
// Strip prepended IV (which is the same length as decryption key) | |
const res = Buffer.alloc(plaintext.length - decryptionKeyBuffer.length); | |
plaintext.copy(res, 0, decryptionKeyBuffer.length, plaintext.length); | |
return res; | |
}; | |
/** | |
* | |
* @param {Buffer} raw | |
* @param {string} encryptionKey | |
* @param {string} validationKey | |
*/ | |
const encrypt = (raw, encryptionKey, validationKey) => { | |
const keyBuffer = Buffer.from(encryptionKey, 'hex'); | |
const validationKeyBuffer = Buffer.from(validationKey, 'hex'); | |
// Decrypt | |
// iv same length as key | |
const iv = crypto.randomBytes(8); | |
let h = crypto.createHmac('sha1', validationKeyBuffer); | |
// contents hmac | |
h.update(raw); | |
const hmacdContents = Buffer.concat([raw, h.digest()]); | |
const c = crypto.createCipheriv('des', keyBuffer, iv); | |
const encrypted = c.update(hmacdContents); | |
const final = c.final(); | |
const cipherText = Buffer.concat([encrypted, final]); | |
const ivAndCipher = Buffer.concat([iv, cipherText]); | |
h = crypto.createHmac('sha1', validationKeyBuffer); | |
h.update(ivAndCipher); | |
const hmacdCipherText = Buffer.concat([ivAndCipher, h.digest()]); | |
return hmacdCipherText; | |
}; | |
/** | |
* | |
* @param {Buffer} buffer | |
* @param {number} offset | |
*/ | |
const readLong = (buffer, offset) => { | |
const low = buffer.readInt32LE(offset); | |
const high = buffer.readInt32LE(offset + 4); | |
const l = Long.fromBits(low, high); | |
return l; | |
}; | |
/** | |
* | |
* @param {Long} long | |
* @param {Buffer} buffer | |
* @param {number} offset | |
*/ | |
const writeLong = (long, buffer, offset) => { | |
buffer.writeInt32LE(long.getLowBits(), offset); | |
buffer.writeInt32LE(long.getHighBits(), offset + 4); | |
return offset + 8; | |
}; | |
/** | |
* | |
* @param {Long} l | |
*/ | |
const ticksToUnixTS = (l) => { | |
return l.subtract(Long.fromString(ticksUntilEpoch)).div(Long.fromString(secsInTick)).toNumber(); | |
}; | |
/** | |
* @param {number} ts | |
* @returns {Long} | |
*/ | |
const unixTSToTicks = (ts) => { | |
const l = Long.fromNumber(ts); | |
return l.multiply(Long.fromString(secsInTick)).add(Long.fromString(ticksUntilEpoch)); | |
}; | |
/** | |
* | |
* @param {Date} date | |
*/ | |
const dateToTicks = (date) => { | |
const millisUnix = date.getTime(); | |
const l = Long.fromNumber(millisUnix); | |
const epochTicks = l.multiply(Long.fromString(millisInTick)); | |
const ret = epochTicks.add(Long.fromString(ticksUntilEpoch)); | |
return ret; | |
}; | |
/** | |
* @param {Long} ticks | |
*/ | |
const ticksToDate = (ticks) => { | |
const millisUnix = ticks.subtract(Long.fromString(ticksUntilEpoch)).div(Long.fromNumber(millisInTick)).toNumber(); | |
return new Date(millisUnix); | |
}; | |
/** | |
* | |
* @param {Buffer} buffer | |
* @param {number} offset | |
*/ | |
const readSharpString = (buffer, offset) => { | |
let numChars = 0; | |
let shift = 0; | |
let byte; | |
let headerBytesRead = 0; | |
do { | |
headerBytesRead++; | |
byte = buffer[offset++]; | |
numChars |= (byte & 0x7F) << shift; | |
shift += 7; | |
} | |
while (byte >= 0x80); | |
const numBytes = numChars * 2 + headerBytesRead; | |
const ret = { | |
numChars: numChars, | |
numBytes, | |
value: buffer.slice(offset, offset + numBytes).toString('utf16le'), | |
original: buffer.slice(offset - headerBytesRead, offset + numBytes).toString('hex') | |
}; | |
return ret; | |
}; | |
/** | |
* | |
* @param {Buffer} buffer | |
* @param {byte} byte | |
* @param {number} offset | |
*/ | |
const _write7BitEncodedInt = (buffer, byte, offset) => { | |
while (byte >= 0x80) { | |
buffer[offset++] = (byte | 0x80); | |
byte >>= 7; | |
} | |
buffer[offset++] = byte; | |
return offset; | |
}; | |
/** | |
* | |
* @param {string} value | |
*/ | |
const writeSharpString = (buffer, value, offset) => { | |
const bytes = Buffer.from(value, 'utf16le'); | |
const headerBytes = _write7BitEncodedInt(buffer, value.length, offset) - offset; | |
offset += headerBytes; | |
bytes.forEach((b, i) => { | |
buffer[offset + i] = b; | |
}); | |
return offset + bytes.length; | |
}; | |
/** | |
* Serialized ticket format version number: 1 byte | |
* FormsAuthenticationTicket.Version: 1 byte | |
* FormsAuthenticationTicket.IssueDateUtc: 8 bytes | |
* {spacer}: 1 byte | |
* FormsAuthenticationTicket.ExpirationUtc: 8 bytes | |
* FormsAuthenticationTicket.IsPersistent: 1 byte | |
* FormsAuthenticationTicket.Name: 1+ bytes (1+ length prefix, 0+ payload) | |
* FormsAuthenticationTicket.UserData: 1+ bytes (1+ length prefix, 0+ payload) | |
* FormsAuthenticationTicket.CookiePath: 1+ bytes (1+ length prefix, 0+ payload) | |
* {footer}: 1 byte | |
* | |
* https://referencesource.microsoft.com/#system.web/Security/FormsAuthenticationTicketSerializer.cs | |
* | |
* @param {string} data | |
*/ | |
const deserialize = (data) => { | |
const bytes = Buffer.from(data); | |
const ret = {}; | |
let offset = 0; | |
ret.sVer = bytes.readInt8(offset); | |
offset++; | |
ret.tVer = bytes.readInt8(offset); | |
offset++; | |
const lg = readLong(bytes, offset); | |
ret.issueTicks = lg.toString(); | |
offset += 8; | |
offset++; // spacer | |
ret.expireTicks = readLong(bytes, offset).toString(); | |
offset += 8; | |
ret.isPersistent = bytes.readInt8(offset); | |
offset++; | |
const nameRead = readSharpString(bytes, offset); | |
ret.name = nameRead.value; | |
offset += nameRead.numBytes; | |
const uDataRead = readSharpString(bytes, offset); | |
ret.uData = uDataRead.value; | |
offset += uDataRead.numBytes; | |
const uPathRead = readSharpString(bytes, offset); | |
ret.uPath = uPathRead.value; | |
offset += uPathRead.numBytes; | |
// ret.footer = bytes.slice(offset, 1) | |
return ret; | |
}; | |
/** | |
* | |
* Serialized ticket format version number: 1 byte | |
* FormsAuthenticationTicket.Version: 1 byte | |
* FormsAuthenticationTicket.IssueDateUtc: 8 bytes | |
* {spacer}: 1 byte | |
* FormsAuthenticationTicket.ExpirationUtc: 8 bytes | |
* FormsAuthenticationTicket.IsPersistent: 1 byte | |
* FormsAuthenticationTicket.Name: 1+ bytes (1+ length prefix, 0+ payload) | |
* FormsAuthenticationTicket.UserData: 1+ bytes (1+ length prefix, 0+ payload) | |
* FormsAuthenticationTicket.CookiePath: 1+ bytes (1+ length prefix, 0+ payload) | |
* {footer}: 1 byte | |
* | |
* @param {*} param0 | |
*/ | |
const serialize = ({ | |
issueTicks, | |
expireTicks, | |
isPersistent, | |
name, | |
userData, | |
path | |
}) => { | |
let offset = 0; | |
const bytes = Buffer.alloc(1024); | |
bytes[offset] = Number('0x01'); | |
offset++; | |
bytes[offset] = Number('0x01'); | |
offset++; | |
offset = writeLong(Long.fromString(issueTicks), bytes, offset); | |
if (offset !== 10) { | |
throw new Error("assert of index 10 failed"); | |
} | |
// spacer | |
bytes[offset] = Number('0xFE'); | |
offset++; | |
offset = writeLong(Long.fromString(expireTicks), bytes, offset); | |
offset = bytes.writeInt8(isPersistent ? 1 : 0, offset); | |
offset = writeSharpString(bytes, name, offset); | |
offset = writeSharpString(bytes, userData, offset); | |
offset = writeSharpString(bytes, path, offset); | |
// closer | |
bytes[offset] = Number('0xFF'); | |
offset++; | |
return bytes.slice(0, offset); | |
}; | |
module.exports = { | |
decrypt, | |
encrypt, | |
serialize, | |
deserialize, | |
writeLong, | |
readLong, | |
writeSharpString, | |
readSharpString, | |
unixTSToTicks, | |
ticksToUnixTS, | |
dateToTicks, | |
ticksToDate, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment