Obfuscating and Shortening Sequential ids with HashIds

Sequential ids are extremely beneficial behind the scenes. RDMS generally offer a auto incremented numerical primary key. The RDMS handles uniqueness and all locking involved in allocating ids. It also stores keys in a very compact data format which saves disk space and keeps indices small.

Why Obfuscate?

Using sequential ids can give away some information about your applications.

Estimated counts - If all of your entities are auto incrementing a competitor could estimate how many users or how much content you have. This isn't a huge deal and could be negated by setting an arbitrary initial auto increment value.

Parameter Injection - If all of your urls are formatted /user/{userId} anyone can easily just change the id in the url. Ideally you have authorization in place to prevent sensitive data from being accessed. This is just an extra precation.

Automated Scraping - Using parameter injection it would be easy to write a script from 0 to some max id and scrape all data from your website without obfuscated ids. Obuscated ids won't prevent this if all ids are public somehow but will make them work a little harder.

HashIds

Hashids is a small open-source library that generates short, unique, non-sequential ids from numbers. We will be using the Java implementation. Hashids is very small so you can use it directly or write your own wrapper class.

public class HashIds {
/*
* The salt is important so that your ids cannot be guessed.
* If you used a default hash an attacker could generate all possible ids
* which defeats the purpose of obfuscating the ids and making them non sequential.
*/
private static final Hashids hashids = new Hashids("Your Salt Here", 3);
public static String encode(long... ids) {
return hashids.encode(ids);
}
public static long[] decodeArray(String hash) {
return hashids.decode(hash);
}
public static long decode(String hash) {
return hashids.decode(hash)[0];
}
}

/*
* It's useful to create helpers for params you know you will use over and over.
* It will reduce boilerpalte, typos, and ensure you use common defaults.
* You could even add validation at this step if you wanted.
*/
private static Long userId(HttpServerExchange exchange) {
return Exchange.pathParams()
.pathParam(exchange, "userId")
.map(HashIds::decode)
.orElse(null);
}