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.
For more information take a look at Auto-Incrementing IDs: Giving your Data Away. As mentioned in the blog post Hashids is not meant to be secure (see Cryptanalysis of hashids). It just adds some extra layers of obfuscation. If security is your biggest concern don't use HashIds.
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];
}
}
HashIds with Undertow
Let's look at an example reusing some code from our Query and Path Parameters in Undertow post. Another live example can be found at https://www.deckhandhq.com/offers/west-marine-tech-baseball-hat-gray/kE7B. The final path is an id encoded with Hashids.
/*
* 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);
}
private static void obfuscatedIdRoute(HttpServerExchange exchange) {
// Using the above helper
Long userId = ParametersServer.userId(exchange);
// This is just to show the raw value.
String rawUserIdParam = Exchange.pathParams()
.pathParam(exchange, "userId")
.orElse(null);
Exchange.body().sendText(exchange, "UserId: " + userId + " hashed: " + rawUserIdParam);
}
private static final HttpHandler ROUTES = new RoutingHandler()
.get("/hello", ParametersServer::queryParam)
.get("/hello/{name}/{num}", ParametersServer::pathParam)
.get("/users/{userId}", ParametersServer::obfuscatedIdRoute)
;
public static void main(String[] args) {
// Just some examples for obfuscated parameters.
LongStream.range(100_000_000, 100_000_003).forEach( id -> {
log.debug("id: " + id + " hashed: " + HashIds.encode(id));
});
SimpleServer server = SimpleServer.simpleServer(ROUTES);
server.start();
}
2017-03-19 14:05:18.587 [main] DEBUG c.s.e.u.parameters.ParametersServer - id: 100000000 hashed: EQejyW
2017-03-19 14:05:18.594 [main] DEBUG c.s.e.u.parameters.ParametersServer - id: 100000001 hashed: oYa86m
2017-03-19 14:05:18.594 [main] DEBUG c.s.e.u.parameters.ParametersServer - id: 100000002 hashed: KQzREN
2017-03-19 14:05:18.872 [main] DEBUG c.s.common.undertow.SimpleServer - ListenerInfo{protcol='http', address=/0:0:0:0:0:0:0:0:8080, sslContext=null}
curl localhost:8080/users/EQejyW
UserId: 100000000 hashed: EQejyW
curl localhost:8080/users/oYa86m
UserId: 100000001 hashed: oYa86m
curl localhost:8080/users/KQzREN
UserId: 100000002 hashed: KQzREN
Success! We can now obfuscate our ids fairly well and keep them shorter without much effort.