How to decrypt cookie with nodejs
Asked Answered
M

1

6

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?

Mcgowen answered 19/1, 2016 at 17:0 Comment(7)
The problem you are facing is that your cipherText for AES block cipher in CBC mode is not block aligned (i.e. it's length is not divisible by the cipher block size -- which is 16 bytes for AES). Try to remove the last eight characters (four hex bytes) from your cookie (i.e. 1224FAE8) -- as they might be part of some message integrity tag. See here for possible decryption and check your plaintext (at least a part of it seems ok for me as it contains valid UTF-16 values)...let me knowPontiff
Have you read this question?Pontiff
@Pontiff I am not on this topic any.. sorryMcgowen
hope this link might help a bit lollyrock.com/articles/nodejs-encryption/,let me know :)Gerius
@Mcgowen (I understand that you are no longer interested in this question, but) don't you have the validation key value which was used? It would be interesting to actually check the authentication tag...Pontiff
@Pontiff unfortunately it was old project and i dont have any environment any more :(Mcgowen
@Mcgowen ok, thank you for responding...good luck with your new projects!Pontiff
P
6

[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!

Pontiff answered 9/10, 2016 at 22:29 Comment(14)
HMACSHA256_HASH_SIZE = 32 That is 32 bytes (256 bits), not 32 bits. The whole point of the HMAC is to verify integrity. It doesn't help you much if you just discard it.Nonconformance
Sorry my bad. Didn't see your last bullet point. Perhaps the padding is off because you didn't cut off enough for the hmac.Nonconformance
@Phil_1984_ You are definitely right -- I misunderstood the source code twice: 1. Signature length is in bytes (I did not cross-check it). 2. SHA-1 HMAC is used together with AES encryption (not SHA256 based one as the default)... Now even the PKCS#7 padding is ok (my fault as well as I overlooked it in the plaintext). Thank you! (It is a pity that the post does not contain the validation key as the answer would be much more useful with HMAC validation)Pontiff
Normally the validation key is the same as the decryption key. Did you try that? I guess with a different validation key it allows for 3rd party validation without decryption capability.Nonconformance
@Phil_1984_ I tried and the authentication tag is different. Having the same key value for encryption and authentication would violate the "single-key-single-use" principle. An interesting discussion for this particular case is herePontiff
Interesting. Do you have any links about "Single key single use principle"? Google is coming up blank for me. I can't think of a reason to have different keys as both encryptor and decryptor need both anyway.Nonconformance
@Phil_1984_ Look for "key separation principle" (I should have used this term), e.g. here, here.Pontiff
Thanks for this. So normally the keys are not identical (i was wrong), but one key is derived from the first key using a known KDF. I still don't see the point in using two completely different keys as it looks like this crypto is doing, but maybe I just can't read ASP/.NETNonconformance
@Phil_1984_ As I write in the answer, I took the validation key here (HMAC keys can be arbitrarily long -- they are hashed if too long). If you could provide some complete test vectors (decKey, authKey, m, modifier, cookie) I would be grateful.Pontiff
@Phil_1984_ Regarding the key derivation, I did not study that, but the correct approach should (IMHO) be to generate two independent keys or have a single master key and derive both auth/dec keys from it. The former approach is used for the AutoGenerate option (see here)Pontiff
@v0id -- I rejected your edit as AFAIK the prepended IV length is equal to the length of the key (see the notes). If you are sure that the IV length is equal to the block size please let me know. Good luck!Pontiff
@vlp: do you know what kind of serialization is used in the decrypted cookie value?Annotate
@Annotate I don't know do you have more test vectors?Pontiff
@Pontiff actually found what I was looking for here: referencesource.microsoft.com/#system.web/Security/…Annotate

© 2022 - 2024 — McMap. All rights reserved.