Decrypting ArcGIS Server Tokens

First, some context:

  • We’re developing some python-based web services - no maps, just data to accompany our AGS map services.
  • We want to use the same security context as ArcGIS Server.
  • We use token-based authentication, for better or worse.

Based on this forum post, if I want to validate a token outside of ArcGIS Server, I have to “use the token and ask ArcGIS Server something trivial”. ‘Bullshit’ I tell myself and those in surrounding cubicles - there must be a more direct method.

So, I start reading up on tokens in the 9.3 docs:

“The shared key for the Token Service is used to encrypt the token. … The server then uses the shared key to decrypt the token. … The shared key ensures that the server has created the token.

Since the shared key is critical to ensuring the identity and authorization of the client, the key must be set to a unique value of proper length. To set the shared key, go to Security-Settings in Manager, and under the Security for GIS Services tab, click the Configure button (when you have chosen ArcGIS Token based Authentication). In the Settings dialog that appears, set the Shared key value. The key should be set to sixteen (16) characters (any characters beyond 16 are not used). It is recommended to use a set of random characters for the key. Any characters may be used, including non-alphanumeric characters. …

The token is encrypted with the key using the encryption method known as Advanced Encryption Standard (AES), also known as Rijndael. The 16 characters in the key represent the 128 bits used for encryption.”

Note the part about the key needing to be 16 characters long.
I didn’t.
And it cost me several hours of my life.
Ours was 10 characters.

So - I got my key and I got my Rijndael method - whatever the hell that is…let me just google “python Rijndael’ and I should have this thing nailed in a few minutes.

After about an hour of fumbling about getting pyCrypto working on my box, (not straightforward)(hint: use pip)(and don’t try importing ‘pycrypto’ like i did - the module is ‘crypto’) and failing at some basic statements, I ran across this extremely useful ESRI forum post.

So, here’s how you’d do it in .Net:

using System;
using ESRI.ArcGIS.TokenService;

namespace AGSTokenDecryptor
{
   class Program
  {
    static void Main(string[] args)
    {
        if (args.Length != 2)
        {
            Console.WriteLine("Usage: AGSTokenDecryptor.exe [token] [secret key]");
            return;
        }

        var token = args[0];
        var key = args[1];
        token = token.Replace("-", "+");
        token = token.Replace("_", "/");

        Console.WriteLine("Decrypting '" + token + "'");
        var result = AesEncryptor.Decrypt(token, key);

        Console.WriteLine("Decrypted: " + result);
    }
  }
}
(Grab the ESRI.ArcGIS.TokenService.dll here: `C:\Windows\assembly\GAC_32\ESRI.ArcGIS.TokenService` And yes, the OP was astute enough to realize that the hyphens and underscores are to be replaced as coded. Converting this to Python should be pretty easy RIGHT??????>>>???? Not so much - I needed to make a trip to .Net reflector-land and decompile the above dll to better understand what was going on. here's the actual AesEncryptor.Decrypt() method:
public static string Decrypt(string text, string password)
{
    RijndaelManaged managed = new RijndaelManaged {
        Mode = CipherMode.CBC,
        Padding = PaddingMode.PKCS7,
        KeySize = 0x80,
        BlockSize = 0x80
    };
    byte[] inputBuffer = Convert.FromBase64String(text);
    byte[] bytes = Encoding.UTF8.GetBytes(password);
    int length = bytes.Length;
    byte[] destinationArray = new byte[0x10];
    if (length > destinationArray.Length)
    {
        length = destinationArray.Length;
    }
    Array.Copy(bytes, destinationArray, length);
    managed.Key = destinationArray;
    managed.IV = m_IV;
    try
    {
        byte[] buffer4 = managed.CreateDecryptor().TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);
        return Encoding.UTF8.GetString(buffer4);
    }
    catch
    {
    }
    return "";
}

And oh yeah, you’ll need the secret ESRI AesEncryptor m_IV variable or “Initialization Vector” as well.

So - armed with the decompiled method and the IV we should now have what we need.

from Crypto.Cipher import AES
from base64 import *

iv =  '\x01'+ '\x02'+ '\x03'+ '\x04'+ '\x05'+ '\x06'+ '\x07'+ '\x08'+
       '\x09'+ '\x0A'+ '\x0B'+ '\x0C'+ '\x0D'+ '\x0E'+ '\x0F' + '\x10'
token = 'jUryRT9UkP8tK+dQKlJZUYWEfY3x2i2eZHYVfnIBxCVMlNfYZ/QcBsAqKPnMlsPM'
token = b64decode(token, '-_')
mode = AES.MODE_CBC
#Make sure your key is 16 chars. if it's shorter pad it with 0s
# longer then trim it at 16.
key =  # padding if needed +('\x00'*6)
cipher = AES.new(key, mode, iv)
print(cipher.decrypt(token))

And here’s your guerilla for the day:

>>> cipher = AES.new(key, mode, iv)
>>> print(cipher.decrypt(token))
darkhelmet:1342637544150: :ref.testing