Last active
November 5, 2021 02:33
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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