Instantly share code, notes, and snippets.
Last active
March 7, 2022 04:26
-
Star
4
(4)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save akaNightmare/30985da516873f6d8d5b18b1877f325d to your computer and use it in GitHub Desktop.
This file contains hidden or 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
'use strict'; | |
const crypto = require('crypto'); | |
const baseTicks = 621355968000000000; // ticks from 0001 to 1970 year | |
class NetCookieService { | |
/** | |
* @param {String} decryptionKey - key for encrypt/decrypt .NET auth cookie | |
* @param {String} validationKey - validation key for encrypt/decrypt .NET auth cookie | |
*/ | |
constructor(decryptionKey, validationKey) { | |
this.decryptionKey = decryptionKey; | |
this.validationKey = validationKey; | |
} | |
/** | |
* Middleware for expressJs | |
* | |
* @returns {Function} | |
*/ | |
middleware() { | |
const self = this; | |
return (req, res, next) => { | |
const cookieValue = req.cookies['.ncauth']; | |
if (cookieValue) { | |
try { | |
const ticket = self.decode(cookieValue); | |
if (ticket.is_persistent || Date.now() <= ticket.utc_expire_date) { | |
req.user = ticket; | |
} | |
} catch (e) {} | |
} | |
return next(); | |
}; | |
} | |
/** | |
* Decode .NET cookie value | |
* | |
* @param cookie | |
* @returns {{ | |
* utc_expire_date: Number, | |
* is_persistent: Boolean, | |
* username: String, | |
* user_id: Number, | |
* session_id: String, | |
* tfa_status: String, | |
* admin_user_name: String, | |
* user_hash: String, | |
* user_agent_hash: String | |
* }} | |
*/ | |
decode(cookie) { | |
cookie = new Buffer(cookie, 'hex'); | |
cookie = cookie.slice(0, cookie.length - 20); | |
const decipher = crypto.createDecipheriv('aes-192-cbc', new Buffer(this.decryptionKey, 'hex'), new Buffer(16)); | |
let decoded = new Buffer(decipher.update(cookie, 'binary', 'binary') + decipher.final(), 'binary').slice(24); | |
let utcExpireTicks = parseInt(this.__reverseHexString(decoded.slice(11, 19).toString('hex')), 16); | |
// @see: https://stackoverflow.com/questions/7966559/how-to-convert-javascript-date-object-to-ticks | |
utcExpireTicks = Math.floor((utcExpireTicks - baseTicks) / 10000); | |
const isPersistent = parseInt(decoded.slice(19, 20).toString('hex'), 16); | |
decoded = decoded.toString('utf8').replace(/\0/g, ''); | |
decoded = decoded.substr(0, decoded.lastIndexOf('|#]')); | |
decoded = decoded.substr(decoded.indexOf('U:'), decoded.length).split('|#]'); | |
return { | |
utc_expire_date: utcExpireTicks, | |
is_persistent: !!isPersistent, | |
username: this.__parseCookieField(decoded[0]), | |
user_id: parseInt(this.__parseCookieField(decoded[1]), 10), | |
session_id: this.__parseCookieField(decoded[2]), | |
tfa_status: this.__parseCookieField(decoded[3]), | |
admin_user_name: this.__parseCookieField(decoded[4]), | |
user_hash: this.__parseCookieField(decoded[5]), | |
user_agent_hash: this.__parseCookieField(decoded[6]) | |
}; | |
} | |
/** | |
* Encode .NET cookie | |
* | |
* @param {{ | |
* username: String, | |
* user_id: Number, | |
* session_id: String, | |
* tfa_status: String, | |
* admin_user_name: String, | |
* user_hash: String, | |
* user_agent_hash: String | |
* }} userInfo | |
* @param {Boolean} [isPersistent=false] | |
* | |
* @returns {String} | |
*/ | |
encode(userInfo, isPersistent) { | |
const data = [ | |
'U:' + userInfo.username, | |
'I:' + userInfo.user_id, | |
'S:' + userInfo.session_id, | |
'2:' + userInfo.tfa_status, | |
'A:' + userInfo.admin_user_name, | |
'H:' + userInfo.user_hash, | |
'B:' + userInfo.user_agent_hash | |
].join('|#]') + '|#]'; | |
const cipher = crypto.createCipheriv('aes-192-cbc', new Buffer(this.decryptionKey, 'hex'), new Buffer(16)); | |
const ticket = new Buffer( | |
['0x01', '0x02'] | |
.concat(this.__generateNanoSecondsHexList(Date.now())) | |
.concat(['0xfe']) | |
.concat(this.__generateNanoSecondsHexList(Date.now() + 7200000)) // add 2 hours to expiration | |
.concat(['0x0' + (isPersistent ? 1 : 0)]) | |
); | |
const encoded = cipher.update('0'.repeat(24) + ticket.toString('binary') + data + '__some_salt_string__', 'binary', 'hex') + cipher.final('hex'); | |
return encoded + crypto.createHmac('sha1', this.validationKey).update(encoded).digest('hex'); | |
} | |
/** | |
* Return reversed hex string | |
* | |
* @param {String} sourceString | |
* @returns {String} | |
* @private | |
*/ | |
__reverseHexString(sourceString) { | |
return sourceString.match(/.{2}/g).reverse().join(''); | |
} | |
/** | |
* Generate 100 nanoseconds from milliseconds | |
* | |
* @param {Number} milliseconds | |
* @returns {Array} | |
* @private | |
*/ | |
__generateNanoSecondsHexList(milliseconds) { | |
// prepend to hex string '0' if positive or '1' if negative | |
let hex = '0' + (milliseconds * 10000 + baseTicks).toString(16); | |
return this.__reverseHexString(hex).match(/.{2}/g).map(chunk => '0x' + chunk); | |
} | |
/** | |
* @param {String} field | |
* @returns {String} | |
* @private | |
*/ | |
__parseCookieField(field) { | |
return field.replace(/^.+:/, ''); | |
} | |
} | |
module.exports = NetCookieService; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The "aes-256-cbc" version of the above code.