phpsecurityhashids

Making hashids unguessable, and undecodable


I want to generate codes for tickets that are unique and short, so what I did:

$sqids = new Sqids(str_shuffle('ABC...90'), minLength: 6);

$code = $sqids->encode([DB::table('order_ticket')->count()]);

Could str_shuffle() solve the issue of an attacker be able to decode it, and even when attacker decodes and knows id, be able to guess next ticket codes?

Maybe also adding millisecond of purchase time to:

$code = $sqids->encode([floor(microtime(true) * 1000) + DB::table('order_ticket')->count()]);

can prevent someone knowing shuffle output be able to know possible ticket codes?

Am I doing it wrong? Does it really solves the issue of attacker knowing next ticket codes? If not, what are the solutions?


Solution

  • The idea with the Sqid implemention is to provide a nicer alternative to just having incrementing ids for public facing ressources, they are not a secure way to generate keys, as they mention on their frontpage https://sqids.org/.

    This id is basically tied directly to the incrementing ID of size of the ticket table.

    The security of your key generation scheme is only as secure as the randomness you use to generate it. If security in this case is to prevent an attacker from extrapolating the next or any next valid ticket number you should not rely on on either str_shuffle, microtime nor the count of ticket entries.

    Instead you should use a cryptographically secure randomness source such as random_bytes in PHP, since your key space is quite small (6 characters) you can instead handle collisions by checking if the ticket id already exists otherwise re-roll it.

    Your current approach still has potential for collisions as the input is non-determistic even for the same consistent increment in ->count().

    Totally random identifier

    When using a totally random identifier you'd lose the ability to convert it back to your id, in that case you have to store it seperately and look it up seperately.

    $sqids->encode(array_map('ord', str_split(random_bytes(4))));
    

    Something like this should do the trick while allowing you to use this id format. But i would strongly recommend using a different way to validate tickets that has more entropy.

    An even better approach would be to just skip the sqid approach for secrets meant to be secure, and just use the random bytes directly.

    strtr(base64_encode(random_bytes(16)), '+/=', '-_.')
    

    Which makes the base64 random data url safe, but you could even encode it in your prefered character set as well. But any format using a good random source would do the trick.

    Known Id

    If you'd really like to store the ID in the result then you could simply sign it using something like a JWT.

    That way it does not matter if the attacker knows how to get any Id since you can verify if they obtained it directly.

    But here you'd need to do a bit more work, where previously you could just look up a different column.