Obfuscate your Clojure Web-app IDs with Hashids

It is common for web applications to expose database-generated auto-incrementing IDs to the end user, often in URLs. This can sometimes be undesirable. For example: When the user is sent to their homepage, and the url is http://example.com/users/5, some users will realize that they’re one of only 5 people daring enough to have registered for your web application, which doesn’t engender confidence.

Enter Hashids

Hashids is an algorithm to generate “YouTube-like” short strings from numbers. There are of course many ways to solve this problem, but Hashids has some unique features, as well as numerous implementations in a variety of languages.

In the above example, Hashids would encode the ID 5 to something like 23k2js, resulting in a more opaque url of http://example.com/users/23k2js.

The types of data whose counts you might want to hide are dependent on your application, but some other examples are: Orders / Purchases, Support Tickets, Invoices.

Using Hashids.clj in your Clojure web app

To use Hashids in Clojure, I wrote Hashids.clj. For your obfuscating pleasure, here are some step-by-step instructions on getting started with it:

1. Include the Hashids.clj library

Add the Hashids.clj library of to the dependencies section of the project.clj file:

:dependencies [;; ... your other dependencies ...
               [jstrutz/hashids "1.0.1"]]

2. Import the library into your code

I’ve found it easiest to insert the hashid conversion at the database interface layer; it tends to be less error-prone than trying to encode and decode the ids in views and handler parameters.

For our example’s database mapping, let’s use Korma, which is a popular DSL interacting with relational databases. In our example case, our Korma DB entity mappings will reside in todos/db/entities.clj file. In the ns form at the top that file, require Hashids:

(ns todos.db.entities
  (:use korma.core
        [korma.db :only (defdb)])
  (:require [todos.db.schema :as schema]
            [hashids.core :as h]))

3. Configure Hashids.clj

Selecting a Hashids salt customizes the Hashids to your application, and is recommended. For illustrative simplicity, we’ll hardcode it. We’ll also set a minimum length for all generated Hashids.

Again, in your Korma entites.clj file, add:

(def hashids-opts {:salt "this is my salt"
                   :min-length 8})

4. Add some helpers for encoding and decoding DB fields

Next, add a few helpers to make encoding and decoding more convenient for multiple fields.

(defn encode-hashid
  [v]
  (h/encode hashids-opts v))

(defn decode-hashid
  [s]
  (->> s
       (h/decode hashids-opts)
       first))

(defn transform-fields
  [f fields row]
  (let [xform-fields (clojure.set/intersection
                       (-> row keys set)
                           (set fields))]
    (reduce
     (fn [r field]
       (assoc r field (f (field r))))
     row
     xform-fields)))

(def encode-fields
  (partial transform-fields encode-hashid))
(def decode-fields
  (partial transform-fields decode-hashid))

5. Insert the translations into your DB access code

Korma provides a convenient access point to transform the values entering and leaving the database:

(defentity todos
  ;; ...
  ;; Decode Hashids as they go into the DB
  (prepare (partial decode-fields [:id]))

  ;; Encode IDs as they come out of the DB
  (transform (partial encode-fields [:id]))
  ;; ...

We’re only encoding/decoding the id field here, but if you have other foreign keys you’d like to encode, include them in this vector.

Worry about your IDs no more

Hashids is a convinent solution to this small problem which you can drop in to your clojure application. By using it, you can save yourself some hassle, and move on to solving more important problems in your code.

Have questions? Contact me – I’m happy to help.

This post was published on the morning of Thursday, March 26th, 2015