Skip to content

Instantly share code, notes, and snippets.

@Lachee
Last active November 5, 2021 02:33
Show Gist options
  • Save Lachee/0352ba74d3bb4f051f2e730cb9ae928f to your computer and use it in GitHub Desktop.
Save Lachee/0352ba74d3bb4f051f2e730cb9ae928f to your computer and use it in GitHub Desktop.
Packs / Unpacks a integer ID into a pseudo faux UUID's with signature to verify origins. Formatting is customisable.
<?php
// Required if you are putting in a namespace:
// namespace app\helpers;
// use InvalidArgumentException;
/**
* Packs / Unpacks a integer ID into a pseudo faux UUID's ( 8-8-12 ) with signature to verify origins.
* Not secure, but provides an abstraction layer to hide the true nature of your database.
*
* Will pack the ID into a 64bit digit number, trimming null terminators and padding with seeded random data.
* An ID will always produce the same UUID unless the key is different
*
* Binary Format:
* ```
* | 1 | 2 ... LEN | LEN ... 16 | 17 - 30 |
* |--------------+-----------+------------+--------------------------------|
* | Length of ID | ID | RANDOM PAD | HMAC SIGNATURE (TRIMMED TO 12) |
* |--------------+-----------+------------+--------------------------------|
* ```
*
* @author Lachee
*
*/
class UUID
{
/** Formatting for 0 dashes. For example: `0267d993fc022da92f8f7d0c308c` */
const FORMAT_28 = 0;
/** Formatting for 1 dash. For example: `0267d993fc022da9-2f8f7d0c308c` */
const FORMAT_16_12 = 1;
/** Formatting for 2 dashes. For example: `0267d993-fc022da9-2f8f7d0c308c` */
const FORMAT_8_8_12 = 2;
/** Formatting for 3 dashes, the same as the standard UUID. For example: `0267d993-fc02-2da9-2f8f7d0c308c` */
const FORMAT_8_4_4_12 = 3;
/**
* The default formatting when packing UUIDs
* @var int
*/
public static $defaultFormat = self::FORMAT_8_4_4_12;
/** @param string Default signature key */
public static $defaultKey = 'ThisShouldBeChanged';
/**
* Packs a ID into a Faux UUID
* @param int $id the integer id to pack
* @param string $key key to use for the signature
* @param int $format the formatting to use for the resulting UUID. If false, then the [[UUID::$defaultFormat]] will be used.
* @return string|null the UUID in teh format of 8-4-12. If null is given, then null is returned.
* @throws InvalidArgumentException
*/
public static function pack($id, $key = false, $format = false)
{
if ($id == null)
return null;
if (!is_int($id))
throw new InvalidArgumentException('$id must be a integer');
if ($key === false)
$key = static::$defaultKey;
if ($format === false)
$format = static::$defaultFormat;
$seed = ($id * 223) ^ 78645877;
mt_srand($seed);
try {
$binary = pack('Q', $id);
$len = static::findNull($binary);
$binary = pack('C', $len) . substr($binary, 0, $len);
$bytes = static::randomRightBinaryPad($binary, 8);
$hex = bin2hex($bytes);
$sig = static::sign($hex, $key, 12);
$formatted = [];
switch ($format) {
default:
throw new InvalidArgumentException('Invalid formatting provided');
case self::FORMAT_28:
return $hex . $sig;
case self::FORMAT_16_12:
$formatted = [$hex, $sig];
break;
case self::FORMAT_8_8_12:
$formatted = [substr($hex, 0, 8), substr($hex, 8), $sig];
break;
case self::FORMAT_8_4_4_12:
$formatted = [substr($hex, 0, 8), substr($hex, 8, 4), substr($hex, 12, 4), $sig];
break;
}
return join('-', $formatted);
} finally {
// Reseed the random at the end
static::reseed();
}
}
/**
* Unpacks a UUID into a id
* @param string $uuid an 8-8-12 UUID
* @param string $key the key used to sign the original signature
* @return int|false|null the integer id, otherwise false. If null is given, then null is returned
*/
public static function unpack($uuid, $key = false)
{
// Return null or false uuids
if ($uuid === null || $uuid === false)
return $uuid;
if (!is_string($uuid))
throw new InvalidArgumentException('$uuid must be a string');
if ($key === false)
$key = static::$defaultKey;
$parts = explode('-', $uuid);
if (count($parts) == 1) {
$hex = substr($parts[0], 0, 16);
$sig = substr($parts[0], 16);
} else {
$sig = array_pop($parts);
$hex = join('', $parts);
}
// Check the bytes
$bytes = @hex2bin($hex);
if ($bytes === false) {
// echo 'Invalid HEX' . PHP_EOL;
return false;
}
// Calculate the signature again and compare against what we got
$cmp_sig = static::sign($hex, $key, 12);
if ($cmp_sig !== $sig) {
// echo 'Mismatch Signature' . PHP_EOL;
return false;
}
// Trim and pad the binary data to cut away the 8 random bits
$len = unpack('C', $bytes[0])[1];
$bytes = str_pad(substr($bytes, 1, $len), 8, "\x00");
$results = @unpack('Q', $bytes);
if ($results === false) {
// echo 'Results false' . PHP_EOL;
return false;
}
if (!isset($results[1])) {
// echo 'Missing Results' . PHP_EOL;
return false;
}
return intval($results[1]);
}
/** Reseeds the random component mt_rand */
private static function reseed()
{
[$usec, $sec] = explode(' ', microtime());
$seed = $sec + $usec * 1000000;
mt_srand($seed);
return $seed;
}
/** Right Pads the content with mt_rand binary data */
private static function randomRightBinaryPad($str, $length = 8)
{
$result = $str;
for ($i = strlen($result); $i < $length; $i++) {
$result .= pack('I', mt_rand())[1];
}
return $result;
}
/** Finds the position of null */
private static function findNull($binary)
{
for ($i = 0; $i < strlen($binary); $i++) {
if ($binary[$i] === "\x00")
return $i;
}
}
/** Signs the given content.
* @param string $content to sign
* @param string $key the key to use
* @param int|false $length if set, then the signature will be trimmed to this length
* @return string binary data
*/
private static function sign($content, $key, $length = false)
{
$hash = hash_hmac('sha256', $content, $key, true);
$sig = bin2hex($hash);
return $length === false ? $sig : substr($sig, 0, $length);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment