Decrypt rememberMe cookie generated in Java

580 views
Skip to first unread message

Tauren Mills

unread,
Jan 14, 2013, 6:52:39 AM1/14/13
to nod...@googlegroups.com
I'm trying to figure out how to decrypt a rememberMe cookie in node that was generated by the java-based Apache Shiro security framework. If anyone has done something similar and can offer any advice, please let me know!

Below is my attempt to decrypt the cookie. The code includes a real cookie and the key that was used to encrypt it. So it should be able to be decrypted.

var decrypt = function (input, password, callback) {
    // Convert input from base64 to binary string
    var edata = new Buffer(input, 'base64').toString('binary'); 

    // Decode password from Base64
    var key = new Buffer(password, 'base64').toString('binary'); 

    // Decipher encrypted data
    var decipher = crypto.createDecipher('aes-128-cbc', key);
    var decrypted = decipher.update(edata, 'binary') + decipher.final('binary');
    var plaintext = new Buffer(decrypted, 'binary').toString('utf8');

    // Return decrypted text
    callback(plaintext);
};

// Real rememberMe for 'testuser' using key on development environment
var rememberMe = 'jVIfq39P7KmDZDEI5vY7+wlhe0dA2J7wd5Sak9AIeVXfuyB6KtGNMIg3LLNLELhQ7BjaO+k5XjoHX7CepC3+YeP9/s4F+dZcfs69UMLZEo1rk1knf/bXK3/90q2ksVQuIVFVtKy0OYU22f1eQX01SHw6btK2sZ+WBmFNDYzJAcX2kSTgENgIqSrRqH/W9ora1NaOlxKy5+VKs7qU1AocLUmoO5AKqg3EaXs99PjykzadD8Wc/kCIz5tBmpQbxjC/By7f7Aqs7U2nxxkzXo68TTDLtZu4u4XhcVvk7+goCWYZT35zN3pWkoOLiMsy4pH5DVRnaOEdCE4NGUKOnomcrvEdChkZoNE+Q7FYPBtz1mEf5EXsNOOl5iAa2etVbmN9VtWDlfsvOCKq2KHBcR+EWYILOEFAGjEUKWS6pz0ISKl8ftqX8LC3E/m3t4aAJMRIWXdf+K6EQ8EYbAfVjRC0xnDRzmAHxdF5RHj27vQI14znuvOg8oynoj/5TliOq3xxpzUdgnCbxQBEquqDC4IO8Wpaftm7NZt/b/KP6+jo7NNXqoliicgsc0iaHc7PVkny9Xrp34Da5lUS3UFaJLGHAQ==';
var key = 'UaNG2oxPGFh1kC4QS0/1Rw==';

decrypt(rememberMe, key, function(output) {
    console.log(output);
});

When this code is run, I get the following error:

/Users/tauren/temp/crypto.js:140
    var decrypted = decipher.update(edata, 'binary') + decipher.final('bin
                                                                    ^
TypeError: DecipherFinal fail
  at decrypt (/Users/tauren/temp/crypto.js:140:69)
  at Object.<anonymous> (/Users/tauren/temp/crypto.js:151:5)
  at Module._compile (module.js:449:26)
  at Object.Module._extensions..js (module.js:467:10)
  at Module.load (module.js:356:32)
  at Function.Module._load (module.js:312:12)
  at Module.runMain (module.js:492:10)
  at process.startup.processNextTick.process._tickCallback (node.js:244:9)

By looking at the Shiro code, it appears they do the following to create the cookie:
  1. Serialize the unencrypted value to a byte array
  2. Encrypt the byte array using the AES cipher with a key generated by the AesCipherService
  3. Base64 encode the encrypted byte array
I'm trying to the the reverse, but something must be missing. Here's some things I'm unsure about:
  • The AES cipher is used in the java code, but which cipher is equivalent in node?. Is it aes-256-cbc, aes-128-cbc, aes-128-ecb, etc?
  • Should I be using crypto.createDecipher or crypto.createDecipheriv (with IV at end)?
  • I'm unsure if I need to be dealing with padding (PKCS?)
  • What about string encoding ('utf8', 'ascii', etc.)?
  • What about salts?
The comments below make me think AES 128 is used. This code generated the key used above:

public static void main(String[] args) {
  //AES algorithm only supports key sizes of 128, 192, and 256 bits.
  //192 and 256 require that the JCE Unlimited Strength
  //Jurisdiction Policy files to be installed.

  AesCipherService cipherService = new AesCipherService();
  Key key = cipherService.generateNewKey();
  String base64 = new SimpleByteSource(key.getEncoded()).toBase64();
  System.out.print(base64);
}

Any advice would be very much appreciated!

Thanks,
Tauren

Tauren Mills

unread,
Jan 15, 2013, 4:28:58 AM1/15/13
to nod...@googlegroups.com
I think I've got it decrypting now. The Shiro author gave me some tips:

When the cookie is read from a request, the process is reversed:

byte[] combined = Base64.decrypt(cookieValue);
//The first 128 bits (16 bytes) are the prepended IV:
byte[] iv = combined[0]..combined[15];
//The remaining is the actual encrypted data:
byte[] encrypted = combined[16]..combined[combined.length-1];
byte[] serializedPrincipals = decrypt(encrypted, key, iv);
PrincipalCollection principals = jdkDeserialize(serializedPrincipals);

In Shiro 1.1.0, the AesCipherService's default settings for byte array
encryption were:

Key size: 128 (bits)
Block size: 128 (bits)
Mode of operation: CFB
Padding scheme: PKCS5
Initialization Vector size: 128 (bits)
Autogenerate and prefix IVs: true

When I changed my code to the following, I was able to see Java class names in the output:

var decrypt = function (input, password, callback) {
    // Decode input from base64 to binary string
    var decoded = new Buffer(input, 'base64').toString('binary'); 

    // The first 128 bits (16 bytes) are the prepended IV
    var iv = decoded.slice(0,16);

    // The remaining is the actual encrypted data
    var data = decoded.slice(16);

    // Decode password from Base64
    var key = new Buffer(password, 'base64').toString('binary'); 

    // Decipher encrypted data
    var decipher = crypto.createDecipheriv('aes-128-cfb', key, iv);
    decipher.setAutoPadding(false);       
    var decrypted = decipher.update(data, 'binary') + decipher.final('binary');
    var plaintext = new Buffer(decrypted, 'binary').toString('ascii');

    // Return decrypted text
    callback(plaintext);
};

var key = 'UaNG2oxPGFh1kC4QS0/1Rw==';

// Real rememberMe for 'testuser' using key on development environment
var rememberMe = 'jVIfq39P7KmDZDEI5vY7+wlhe0dA2J7wd5Sak9AIeVXfuyB6KtGNMIg3LLNLELhQ7BjaO+k5XjoHX7CepC3+YeP9/s4F+dZcfs69UMLZEo1rk1knf/bXK3/90q2ksVQuIVFVtKy0OYU22f1eQX01SHw6btK2sZ+WBmFNDYzJAcX2kSTgENgIqSrRqH/W9ora1NaOlxKy5+VKs7qU1AocLUmoO5AKqg3EaXs99PjykzadD8Wc/kCIz5tBmpQbxjC/By7f7Aqs7U2nxxkzXo68TTDLtZu4u4XhcVvk7+goCWYZT35zN3pWkoOLiMsy4pH5DVRnaOEdCE4NGUKOnomcrvEdChkZoNE+Q7FYPBtz1mEf5EXsNOOl5iAa2etVbmN9VtWDlfsvOCKq2KHBcR+EWYILOEFAGjEUKWS6pz0ISKl8ftqX8LC3E/m3t4aAJMRIWXdf+K6EQ8EYbAfVjRC0xnDRzmAHxdF5RHj27vQI14znuvOg8oynoj/5TliOq3xxpzUdgnCbxQBEquqDC4IO8Wpaftm7NZt/b/KP6+jo7NNXqoliicgsc0iaHc7PVkny9Xrp34Da5lUS3UFaJLGHAQ==';

decrypt(rememberMe, key, function(output) {
    console.log(output);
});

However, the data is a byte array of serialized Java objects, so it isn't very useful. If only I could figure out how to parse and find the ID number from it (should contain 1834 somewhere)!

��sr2org.apache.shiro.subject.SimplePrincipalCollection�X%JLrealmPrincipalstLjava/util/Map;xpsrjava.util.LinkedHashMap4�N\l��Z
                                                                                                                              accessOrderxrjava.util.HashMap���`�F
loadFactorI thresholdxp?@
                             t
SprtzRealmsrjava.util.LinkedHashSet�l�Z��*xrjava.util.HashSet�D�����4xpw
                                                                        ?@srjava.lang.Long;���̏#�Jvaluexrjava.lang.Number���
                                                                                                                           ���xp*xxwq~x

I'm not quite sure where to go from here. But maybe the decryption steps above will help someone else.

Tauren


Tauren Mills

unread,
Jan 27, 2013, 3:59:07 AM1/27/13
to nod...@googlegroups.com
I forgot to report back previously, but I was able to get this working. I posted details in my blog:

Maybe this will prove helpful to someone in the future. I'm certainly open to any improvements or suggestions. 

Tauren


Reply all
Reply to author
Forward
0 new messages