How to decrypt a .NET Forms Authentication Cookie in Node.js - node.js

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:
“Purpose” is passed to the crypto routines that describe purpose, we need to provide the same string to decrypt it
Changes to how Message Authentication Code is stored (MAC)
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:
I need to deal with DES and not AES (as thats what has been set in the Web.Config file) with different IV and key sizes.
Im dealing with a forms authentication ticket and not owin auth or anti forgery token.
Im taking a stab at what the "label" is, im setting it as ">Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware\x11ApplicationCookie\x02v1" but not sure thats correct (cant find the source code for this)
Im not sure I need to do any padding / base64 decoding of the encrypted data, because its in hex format (i basically get the DATA part from IV + DATA + HMAC and set it as a blob and pass to decipher.update)

Related

How to encrypt and decrypt a number using same key?

I need a way to encrypt a number (say 3423234234) using a secret key, which can also be decrypted using that same key.
const encrypted = encrypt(number, key)
const decrypted = decrypt(encrypted, key)
I tried CryptoJS using AES, DES, Rabbit and RC4 algorithms, but it gives me a long encrypted value with special characters.
I want some encrypted value like MongoDB's ObjectId (which contains only alphanumeric characters like 1575866cab3f22f0c8510451f293f405, and should not exceed 20 or 40 characters).
I figured out a way.
const decrypt = function (num) {
const base = "tGVwFAPiJqh3maNHdZfby9eIQXcr2zvgD57YMn8KWCxkSUujpB10sL6oRTl4OE"
num = num.toString()
let lastChar, baseConverted = 0, index=0;
while(num !== ''){
lastChar = num.slice(-1);
baseConverted += base.indexOf(lastChar) * Math.pow(62,index);
index++;
num = num.slice(0, num.length-1);
}
return baseConverted;
};
const encrypt = function (num) {
const base = "tGVwFAPiJqh3maNHdZfby9eIQXcr2zvgD57YMn8KWCxkSUujpB10sL6oRTl4OE"
// console.log(base.length)
let mod, baseConverted = '';
while(num){
mod = num%62;
baseConverted = base.slice(mod, mod+1)+baseConverted;
num = Math.floor(num/62);
}
return baseConverted;
};
module.exports = {
encrypt,
decrypt
}
The reason I wanted it because I want to encrypt all the id's of a resource before sharing it with client.
It will decrypt the keys when request is received.

AES 256 GCM encryption decryption does not work on android device

I'm working on a react-native project and trying to encrypt and decrypt the photo by crypto and using aes-256-gcm algorithm. This code works well on simulator both android and ios, these is no issue on device when I'm debugging as well, but as soon as I stop Remote JS Debugging on android device, this error'll appear: unsupported state or unable to authenticate data.
I'm completely confuse and I don't know how I can fix this issue.
I should mention that this code'll work correctly for small data and string, only there is problem with large file.
Here is my code:
key = crypto.randomBytes(32);
static encryptFile = inData => {
let iv = Buffer.from(crypto.randomBytes(16));
let cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(inData, "binary", "hex");
encrypted += cipher.final("hex");
let cipherTag = cipher.getAuthTag();
encrypted += "," + iv.toString("hex") + "," + cipherTag.toString("hex");
return encrypted;
};
static decryptFile = inEncData => {
let encParts = inEncData.split(",");
let currentIV = Buffer.from(encParts[1], "hex");
let currentTag = Buffer.from(encParts[2], "hex");
let decipher = crypto.createDecipheriv(algorithm, key, currentIV);
decipher.setAuthTag(currentTag);
let decrypted = decipher.update(encParts[0], "hex");
let decryptedFinal = decipher.final();
Buffer.concat([decrypted, decryptedFinal]);
return decrypted;
};
I read image file by rn-fetch-blob and pass it to encryptFile method:
let res = await RNFetchBlob.fs.readFile(filePath, "ascii");
let enc = convertor.encryptFile(res);
let dec = convertor.decryptFile(enc);
And the error occurs on decryptFile method.
Just add utf-8 with hex encoding parameters shown below:
let encrypted = cipher.update(inData, "utf8", "hex");
encrypted += cipher.final("hex");
let decrypted = decipher.update(encParts[0], "hex","utf8");
let decryptedFinal = decipher.final('utf8');

NodeJS | Crypto Encryption is not producing correct results

I have a tricky problem to resolve. Not sure how to explain it correctly but will try my best. So here is what I am trying to do: I am trying to use a 3rd Party API, which wants me to encrypt a value and submits it. I successfully achieved it through C# code using the following block:
public string Encrypt(byte[] dataToEncrypt, byte[] keyBytes)
{
AesManaged tdes = new AesManaged();
tdes.KeySize = 256;
tdes.BlockSize = 128;
tdes.Key = keyBytes;
tdes.Mode = CipherMode.ECB;
tdes.Padding = PaddingMode.PKCS7;
ICryptoTransform crypt = tdes.CreateEncryptor();
byte[] cipher = crypt.TransformFinalBlock(dataToEncrypt, 0, dataToEncrypt.Length);
tdes.Clear();
return Convert.ToBase64String(cipher, 0, cipher.Length);
}
Now, I am trying to achieve the same in Node. I wrote the following function.
encrypt(buffer){
var buffbytes = new Buffer('my app key goes here to be used as password','utf8'); //converts the app key into buffer stream
return this.encrypt_key(new Buffer(buffer,'utf8'), buffbytes);
},
encrypt_key(buffer, keybytes){
var cipher = crypto.createCipher('aes-128-ecb',keybytes);
var crypted = cipher.update(buffer,'utf8','base64');
crypted = crypted+ cipher.final('base64');
return crypted;
},
This encryption code works fine. It encrypts it fine, but it doesn't encrypt it similar to what c# code does. When I take the encrypted text from C# code, and inject the encrypted result into the API call, it passes through fine, but when I use my encrypted result into the API call, it fails mentioning that the format of my key is incorrect.
I would like to know if these code blocks are same or not. I assume it is same, because both code using 128 bit AES, ECB Cipher and default padding for Crypto Node module is PKCS5 which is same as PKCS7 for 128 bit encryption. Please Help!
Edit: 9/19/2017
Fixed as per #smarx solution:
encrypt(buffer){
var buffbytes = new Buffer(helper.Constants.AppKey,'utf8'); //converts the app key into buffer stream
return this.encrypt_key(new Buffer(buffer,'utf8'), helper.Constants.AppKey);
},
encrypt_key(buffer, key){
var cipher = crypto.createCipheriv('aes-256-ecb',key,new Buffer(0));
var crypted = cipher.update(buffer,'utf8','base64');
crypted = crypted+ cipher.final('base64');
console.log('printed: ', crypted);
return crypted;
},
In your Node.js code, you're using the wrong cipher algorithm. Use aes-256-ecb, since you're using a 256-bit key. Also, be sure to use createCipheriv, since createCipher expects a password from which it derives an encryption key.
One-line fix:
const cipher = crypto.createCipheriv('aes-256-ecb', key, new Buffer(0));
The below two programs produce identical output (Q9VZ73VKhW8ZvdcBzm05mw==).
C#:
var key = System.Text.Encoding.UTF8.GetBytes("abcdefghijklmnopqrstuvwxyz123456");
var data = System.Text.Encoding.UTF8.GetBytes("Hello, World!");
var aes = new AesManaged {
Key = key,
Mode = CipherMode.ECB,
};
Console.WriteLine(Convert.ToBase64String(
aes.CreateEncryptor().TransformFinalBlock(data, 0, data.Length)));
Node.js:
const crypto = require('crypto');
const key = 'abcdefghijklmnopqrstuvwxyz123456';
const data = 'Hello, World!';
const cipher = crypto.createCipheriv('aes-256-ecb', key, new Buffer(0));
console.log(cipher.update(data, 'utf-8', 'base64') + cipher.final('base64'));

"Unsupported state or unable to authenticate data" with aes-128-gcm in Node

I'm trying to implement encrypt/decrypt functions using aes-128-gcm as provided by node crypto. From my understanding, gcm encrypts the ciphertext but also hashes it and provides this as an 'authentication tag'. However, I keep getting the error: "Unsupported state or unable to authenticate data".
I'm not sure if this is an error in my code - looking at the encrypted ciphertext and auth tag, the one being fetched by the decrypt function is the same as the one produced by the encrypt function.
function encrypt(plaintext) {
// IV is being generated for each encryption
var iv = crypto.randomBytes(12),
cipher = crypto.createCipheriv(aes,key,iv),
encryptedData = cipher.update(plaintext),
tag;
// Cipher.final has been called, so no more encryption/updates can take place
encryptedData += cipher.final();
// Auth tag must be generated after cipher.final()
tag = cipher.getAuthTag();
return encryptedData + "$$" + tag.toString('hex') + "$$" + iv.toString('hex');
}
function decrypt(ciphertext) {
var cipherSplit = ciphertext.split("$$"),
text = cipherSplit[0],
tag = Buffer.from(cipherSplit[1], 'hex'),
iv = Buffer.from(cipherSplit[2], 'hex'),
decipher = crypto.createDecipheriv(aes,key,iv);
decipher.setAuthTag(tag);
var decryptedData = decipher.update(text);
decryptedData += decipher.final();
}
The error is being thrown by decipher.final().
In case if someone still tries to get a working example of encryption and decryption process.
I've left some comments that should be taken into consideration.
import * as crypto from 'crypto';
const textToEncode = 'some secret text'; // utf-8
const algo = 'aes-128-gcm';
// Key bytes length depends on algorithm being used:
// 'aes-128-gcm' = 16 bytes
// 'aes-192-gcm' = 24 bytes
// 'aes-256-gcm' = 32 bytes
const key = crypto.randomBytes(16);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algo, key, iv);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(textToEncode, 'utf-8')),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
console.info('Value encrypted', {
valueToEncrypt: textToEncode,
encryptedValue: encrypted.toString('hex'),
authTag: authTag.toString('hex'),
});
// It's important to use the same authTag and IV that were used during encoding
const decipher = crypto.createDecipheriv(algo, key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
console.info('Value decrypted', {
valueToDecrypt: encrypted.toString('hex'),
decryptedValue: decrypted.toString('utf-8'),
});
I managed to fix this: the issue was that I wasn't specifying an encoding type for cipher.final() and I was returning it within a String, so it wasn't returning a Buffer object, which decipher.final() was expecting.
To fix, I add 'utf-8' to 'hex' encoding parameters within my cipher.update and cipher.final, and vice versa in decipher.
Edited to add code example - note this is from 2018, so may be outdated now.
function encrypt(plaintext) {
// IV is being generated for each encryption
var iv = crypto.randomBytes(12),
cipher = crypto.createCipheriv(aes,key,iv),
encryptedData = cipher.update(plaintext, 'utf-8', 'hex'),
tag;
// Cipher.final has been called, so no more encryption/updates can take place
encryptedData += cipher.final('hex');
// Auth tag must be generated after cipher.final()
tag = cipher.getAuthTag();
return encryptedData + "$$" + tag.toString('hex') + "$$" + iv.toString('hex');
}
function decrypt(ciphertext) {
var cipherSplit = ciphertext.split("$$"),
text = cipherSplit[0],
tag = Buffer.from(cipherSplit[1], 'hex'),
iv = Buffer.from(cipherSplit[2], 'hex'),
decipher = crypto.createDecipheriv(aes, key, iv);
decipher.setAuthTag(tag);
var decryptedData = decipher.update(text, 'hex', 'utf-8');
decryptedData += decipher.final('utf-8');
}

How to decrypt cookie with nodejs

I am trying to make run this
function hex2a(hex) {
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
}
//Raw cookie
var cookie = "B417B464CA63FE780584563D2DA4709B03F6195189044C26A29770F3203881DD90B1428139088D945CF6807CA408F201DABBADD59CE1D740F853A894692273F1CA83EC3F26493744E3D25D720374E03393F71E21BE2D96B6110CB7AC12E44447FFBD810D3D57FBACA8DF5249EB503C3DFD255692409F084650EFED205388DD8C08BF7B941E1AC1B3B70B9A8E09118D756BEAFF25834E72357FD40E80E76458091224FAE8";
//decryptionKey from issuers <machineKey>
var deckey = "FFA87B82D4A1BEAA15C06F6434A7EB2251976A838784E134900E6629B9F954B7";
var crypto = require('crypto');
var ivc = cookie, iv, cipherText, ivSize = 16, res = "";
ivc = new Buffer(ivc, 'hex');
iv = new Buffer(ivSize);
cipherText = new Buffer(ivc.length - ivSize);
ivc.copy(iv, 0, 0, ivSize);
ivc.copy(cipherText, 0, ivSize);
c = crypto.createDecipheriv('aes-256-cbc', hex2a(deckey), iv.toString('binary'));
res = c.update(cipherText, "binary", "utf8");
res += c.final('utf8');
console.log(res);
In this Q&A, it mentions about differences about node js versions, I tried that apply that one but with out success:
res = c.update(cipherText, "binary", "utf8");
line result such result
�sJ舸=�X7D������G����}x���T
and
res += c.final('utf8');
gives this error
0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length
nodejs version: 4.1.2 and crypto version 0.0.3
How can I properly decrypt cookie with this algorith or can you suggest any other?
[Assuming you are trying to decrypt a .NET framework cookie]:
(Note: This answer was completely rewritten as things were not as simple as it seemed)
The encryption schema is described here, citing interesting parts:
VERIFY + DECRYPT DATA (fEncrypt = false, signData = true)
Input: buf represents ciphertext to decrypt, modifier represents data to be removed from the end of the plaintext (since it's not really plaintext data)
Input (buf): E(iv + m + modifier) + HMAC(E(iv + m + modifier))
Output: m
The 'iv' in the above descriptions isn't an actual IV. Rather, if ivType = > IVType.Random, we'll prepend random bytes ('iv') to the plaintext before feeding it to the crypto algorithms. Introducing randomness early in the algorithm prevents users from inspecting two ciphertexts to see if the plaintexts are related. If ivType = IVType.None, then 'iv' is simply an empty string. If ivType = IVType.Hash, we use a non-keyed hash of the plaintext.
The 'modifier' in the above descriptions is a piece of metadata that should be encrypted along with the plaintext but which isn't actually part of the plaintext itself. It can be used for storing things like the user name for whom this plaintext was generated, the page that generated the plaintext, etc. On decryption, the modifier parameter is compared against the modifier stored in the crypto stream, and it is stripped from the message before the plaintext is returned.
Which is (hopefully) implemented with the following script:
// Input
var cookie = "B417B464CA63FE780584563D2DA4709B03F6195189044C26A29770F3203881DD90B1428139088D945CF6807CA408F201DABBADD59CE1D740F853A894692273F1CA83EC3F26493744E3D25D720374E03393F71E21BE2D96B6110CB7AC12E44447FFBD810D3D57FBACA8DF5249EB503C3DFD255692409F084650EFED205388DD8C08BF7B941E1AC1B3B70B9A8E09118D756BEAFF25834E72357FD40E80E76458091224FAE8";
var decryptionKey = "FFA87B82D4A1BEAA15C06F6434A7EB2251976A838784E134900E6629B9F954B7";
var validationKey = "A5326FFC9D3B74527AECE124D0B7BE5D85D58AFB12AAB3D76319B27EE57608A5A7BCAB5E34C7F1305ECE5AC78DB1FFEC0A9435C316884AB4C83D2008B533CFD9";
// Parameters
var hmacSize=20
// Make buffers for input
var cookieBuffer = new Buffer(cookie, 'hex');
var decryptionKeyBuffer = new Buffer(decryptionKey, 'hex');
var validationKeyBuffer = new Buffer(validationKey, 'hex');
// Parse cookie
var curOffset=0;
var cipherText = new Buffer(cookieBuffer.length - hmacSize);
curOffset+=cookieBuffer.copy(cipherText, 0, curOffset, curOffset+cipherText.length);
var hmac = new Buffer(hmacSize);
curOffset+=cookieBuffer.copy(hmac, 0, curOffset, curOffset+hmac.length);
// Verify HMAC
var crypto = require('crypto');
var h = crypto.createHmac('sha1', validationKeyBuffer);
h.update(cipherText);
var expectedHmac = h.digest();
console.log('Expected HMAC: ' + expectedHmac.toString('hex'));
console.log('Actual HMAC: ' + hmac.toString('hex'));
//if(!expectedHmac.equals(hmac)) { // Note: Requires nodejs v0.11.13
// throw 'Cookie integrity error';
//}
// Decrypt
var zeroIv = new Buffer("00000000000000000000000000000000", 'hex');
var c = crypto.createDecipheriv('aes-256-cbc', decryptionKeyBuffer, zeroIv);
var plaintext = Buffer.concat([c.update(cipherText), c.final()]);
// Strip IV (which is the same length as decryption key -- see notes below)
var res = new Buffer(plaintext.length-decryptionKeyBuffer.length);
plaintext.copy(res, 0, decryptionKeyBuffer.length, plaintext.length);
// Output
console.log('HEX: ' + res.toString('hex'));
console.log('UTF-8: ' + res.toString('utf8'));
Giving result:
Expected HMAC: 88e332b9a27b8f6f8d805ae718c562c1c8b721ed
Actual HMAC: 6beaff25834e72357fd40e80e76458091224fae8
HEX: 010112ea9a47b2f2ce08fe121e7d78b6f2ce0801085400650073007400550073006500720016540065007300740020007400650073007400730073006f006e002c00200072006f006c0066007a006f007200012f00ff1d892908d9c497bd804f5f22eab043ff6368702c
UTF-8: ��G���}x�TestUserTest testsson, rolfzor/���ė��O_"��C�chp,
Some (random) notes about this code:
it assumes that AES is used for encryption and HMAC-SHA1 is used for authentication
as the used authentication key is not known, the integrity check condition is commented out and verification key from this very related question is used (which is the reason for authentication tag mismatch)
the padding used for AES encryption is PKCS#7
the 'modifier' field is assumed empty. If this is not the case you would have to check it and remove it from the plaintext
for production environment you definitely should check the authentication tag (otherwise you would expose yourself to nasty attacks)
to avoid even nastier attacks, the authentication tag should be tested for equality in constant time (which might be tricky to implement in nodejs). Please note that the commented-out code is very probably vulnerable to timing-attacks.
the IV length is equal to the key length (see here for the reason)
Disclaimer: I did not study the original .NET code thoroughly, nor am I a crypto expert so please do validate my thoughts
Good luck!

Resources