ActionKit Hashing EXPOSED

What's In An AKID?

When ActionKit sends out links they include a special value called an AKID, short for ActionKit Identifier. They look like:

http://docs.actionkit.com/go/210?t=1&akid=2695.103007.Gcg02t

There are three parts to the AKID shown above, separated by periods. The first is "2695" which is the mailing ID. This is optional -- you can have an AKID without a mailing ID which will look like ".103007.Gcg02t". The second part is "103007" which is the user ID for the user emailed the link. The final part, "Gcg02t", is called the hash and it's the topic of this document.

Why Use A Hash?

The function of the hash at the end of AKIDs is to verify that the value (the mailing ID and user ID) have not been tampered with. If someone did try to change user ID 103007 to 103008, the hash code at the end would now be invalid and ActionKit would ignore the AKID. Go ahead and try it -- if you copy a link to a page that recognizes users on your site and modify a part of the AKID you'll find that you're no longer recognized.

This hash is what prevents malicious users from collecting data from your ActionKit pages by cycling through all possible user IDs.

ActionKit's Hashing System

We generate AKIDs using a secure hashing algorithm called SHA-256 (https://en.wikipedia.org/wiki/SHA-2), using a secret key stored on the ActionKit server. We then encode the hash value using Base64's URL safe encoding (https://en.wikipedia.org/wiki/Base64#URL_applications) and truncate it down to six characters.

In pseudo-code the process looks like:

left(base64_urlsafe(sha256(SECRET + "." + CLEARTEXT)), 6)

You can now verify AKIDs on your own server. To do so you need to get your AKID Hash Secret from your ActionKit Config, which is available only to superusers. Click on the gear icon next to your user name in the upper right corner and choose "Configure ActionKit". In the Mailing Settings section, under Mailing Link Hashing, you will find a link labeled "Show secret" which will bring you to a page that shows the AKID Hash Secret. The value will be 64 characters long.

It is important to protect this secret. A hacker could cause you a lot of trouble if they had this value, so don't embed it in any publicly accessible code -- client-side JavaScript, source code on GitHub, tweets, etc. If the secret is exposed, even if you don't think anyone got it, please contact us and we'll generate a new one for you.

Implementation Guarantees

The AKID hashing system is an implementation detail of ActionKit. We reserve the right to change it at any time. If we do change it, we’ll communicate the change via the clients@actionkit.com mailing list.

If at all possible, we will provide 30 days notice before making any such changes. Barring a major break in the underlying SHA256 algorithm, which is unlikely, you can expect to receive 30 days notice prior to any change.

As historical context, we have not had to change the hash format since ActionKit was launched.

How To Verify An AKID

Here's some sample code in Python to verify an AKID:

SECRET = 'XXX YOUR SECRET HERE XXX'

import hashlib
import base64

def verify(akid):
    # pop off the input hash
    chunks = akid.split('.')
    input_hash = chunks.pop()
    cleartext = '.'.join(chunks)

    # run the hashing algorithm
    sha = hashlib.sha256('{0}.{1}'.format(SECRET, cleartext).encode('ascii'))
    raw_hash = sha.digest()
    urlsafe_hash = base64.urlsafe_b64encode(raw_hash).decode('ascii')
    short_hash = urlsafe_hash[:6]

    # compare the results
    return input_hash == short_hash

Here's sample code in Perl:

use Digest::SHA qw(sha256);
use MIME::Base64 qw(encode_base64url);

my $SECRET = 'XXX YOUR SECRET HERE XXX';

sub verify {
    my ($akid) = @_;

    # split off hash from cleartext
    my ($cleartext, $input_hash) = $akid =~ /(.*)\.(.{6})$/;

    # run the hashing algorithm
    my $sha = sha256($SECRET . '.' . $cleartext);
    my $urlsafe_hash  = encode_base64url($sha);
    my $short_hash = substr($urlsafe_hash, 0, 6);

    # compare the results
    return $input_hash eq $short_hash;
}

Here's a node.js Javascript sample (reminder -- don't run this client-side or your secret could be stolen):

var crypto = require('crypto');

var SECRET = 'XXX YOUR SECRET HERE XXX';

function verify(akid) {
    // split off hash from cleartext
    chunks = akid.split('.');
    input_hash = chunks.pop();
    cleartext = chunks.join('.');

    // run the hashing algorithm
    var shasum = crypto.createHash('sha256');
    shasum.update(SECRET + "." + cleartext);
    var sha = shasum.digest('base64');

    // convert from base64 to URL-safe base64
    var urlsafe_hash  = sha.replace(/\+/g, '-')
                           .replace(/\//g, '_')
                           .replace(/=+$/, '');

    var short_hash = urlsafe_hash.substring(0, 6);

    // compare the results
    return(input_hash == short_hash);
}

There are some sample hashes on the same page as your hash secret; reload the page to generate additional samples. We recommend testing your local implementation to confirm that your code successfully validates those hashes and rejects modified data.

Hashing Custom Data

We think AKID hashing is pretty great. If you agree you might want to use it on more than just ActionKit mailing IDs and user IDs. For example, imagine you had IDs for Example.com's awesome example system in a custom user field called example_id. You could send out a hashed version of that ID like:

http://example.com/example?id={{ user.custom_fields.example_id|custom_hash }}

This hash will use the same secret key as your AKIDs, so you can use the same code to verify it on the other end.

Since it's a Django template filter you can also apply it to and arbitrary block of text. So you could include a few variables and hash them together:

http://example.com/example?id={% filter custom_hash %}{{mailing_id }}.{{ user.custom_fields.example_id }}{% endfilter}