I am trying to see if its possible to decrypt a .NET Forms Authentication Cookie in Node.js
The cookie is generated via .NET Framework 4.6
If i check the machineKey value - it uses settings:
decryptionKey="xxxxxxxxxxxxxxxx" validation="SHA1" decryption="DES"
And if i look at the .NET source i believe it defaults to the cbc des cipher: https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/mscorlib/system/security/cryptography/descryptoserviceprovider.cs#L90
With IV key length of 8 bytes and hmac size of 20 bytes
In the case of the ASPX cookie it has format:
IV + DATA + HMAC
So if i try something like this below (which when decrypted should be a binary FormsAuthenticationTicket):
const COOKIE_CONTENTS = '86F1EDAAE112A4E56EB1DAA75411F07E8D82F648A87F13E8386735610....REDACTED FULL VALUE';
const decryptionKey = "xxxxxxxxxxxxxxxx";
const algorithm = 'des-cbc';
const key = Buffer.from(decryptionKey, "hex");
let cookie = COOKIE_CONTENTS;
let blob = Buffer.from(cookie, 'hex');
const ivSize = 8;
const hmacSize = 20;
let iv = blob.slice(0, ivSize);
let hmac = blob.slice(blob.length - hmacSize);
let encrypted = blob.slice(ivSize, blob.length - hmacSize);
console.log("Len (cookie):", cookie.length);
console.log("Len (blob):", blob.length);
console.log("IV:", iv, "len:", iv.length);
console.log("HMAC:", hmac, "len:", hmac.length);
console.log("Encrypted:", encrypted, "len:", encrypted.length);
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = Buffer.from(decipher.update(encrypted, 'binary', 'binary') + decipher.final('binary'), 'binary');
And it works!
However when the token is generated with httpRuntime setting:
<httpRuntime targetFramework="4.5" enableVersionHeader="false" maxRequestLength="10240" />
I get a failure
error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
And unfortunately this is the auth tokens i need to decrypt as they are on staging and production systems.
This may be due to the way .NET has changed the way auth is done when you opt into targetFramework="4.5"
It has .NET 4.5 “cryptographic improvements”
See: https://devblogs.microsoft.com/dotnet/cryptographic-improvements-in-asp-net-4-5-pt-2/
I think the rundown is that:
Some of the .NET source code is referenced here: https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.Web/Security/Cryptography/Purpose.cs
and also I can see the 4.5 setting block here: https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.Web/Security/FormsAuthentication.cs#L155
and the code eventually ends up here: https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.Web/Security/Cryptography/MachineKeyDataProtectorFactory.cs#L25-L29
but its hard to follow after that. Seems as though 4.5 mode bipasses the DES settings we have passed to it, so who knows what cipher its actually using when in this mode?
Unfortunately I cant just remove the targetFramework="4.5" part as this will mean all user tokens will fail after this is rolled out, meaning all users will need to login again which is not acceptable.
Does anyone know more details on how this can be done with a 4.5 “cryptographic improvements” token? Any ideas on what I am missing with these crypto settings - it would be great if this can be done in Node.js
UPDATE:
I have tried what @Sebastian has mentioned and tried to port over some of the python code without success (I believe i have written the node code correctly, please let me know if i'm missing something)
eg:
function writeUnsignedInt(v, buf, offset) {
buf.writeInt32BE(v, offset);
}
// conversion of this: https://lowleveldesign.org/2014/11/11/decrypting-asp-net-identity-cookies/
function deriveKey(key, label, context, keyLengthInBits) {
let labelCount = 0;
let contextCount = 0;
if (label) {
labelCount = label.length;
}
if (context) {
contextCount = context.length;
}
const buffer = Buffer.alloc((4 + labelCount + 1 + contextCount + 4));
if (labelCount > 0) {
buffer.write(label, 4, 'ascii');
}
if (contextCount > 0) {
buffer.write(context, 5 + labelCount, 'ascii');
}
writeUnsignedInt(keyLengthInBits, buffer, 5 + labelCount + contextCount);
let destOffset = 0;
let value = parseInt(keyLengthInBits / 8, 10);
let resultBuffer = Buffer.alloc(value);
let num = 1;
while(value > 0) {
writeUnsignedInt(num, buffer, 0);
var hmac = crypto.createHmac('sha512', key);
let bufferString = buffer.toString();
let hashedData = hmac.update(bufferString);
let generatedHmac = hashedData.digest('hex');
let count = Math.min(value, generatedHmac.length);
resultBuffer.write(generatedHmac.substring(0, count), destOffset);
destOffset += count;
value -= count;
num += 1;
}
return resultBuffer.toString();
}
const algorithm = 'des-cbc';
let key = Buffer.from(decryptionKey, "hex");
let blob = Buffer.from(cookie, 'hex');
const ivSize = 8;
const hmacSize = 20;
let iv = blob.slice(0, ivSize);
let hmac = blob.slice(blob.length - hmacSize);
let encrypted = blob.slice(ivSize, blob.length - hmacSize);
let dkey = deriveKey(key, 'FormsAuthentication.Ticket', '>Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware\x11ApplicationCookie\x02v1', 64);
const decipher = crypto.createDecipheriv(algorithm, dkey, iv);
let decrypted = Buffer.from(decipher.update(encrypted, 'binary', 'binary') + decipher.final('binary'), 'binary');
I think the main differences is:
DATA part from IV + DATA + HMAC and set it as a blob and pass to decipher.update)