Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
"sparx64"
],
"require": {
"php": "^8.1",
"paragonie/constant_time_encoding": "^3.1"
"php": "^8.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.93",
Expand Down
74 changes: 2 additions & 72 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Exception/RandflakeIdDecodingErrorException.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class RandflakeIdDecodingErrorException extends RandflakeIdException
public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null)
{
parent::__construct(
"" !== $message ? $message : "Decoding error occurred.",
"" !== $message ? $message : "Failed to decode ID.",
$code,
$previous
);
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/RandflakeIdDecryptionErrorException.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class RandflakeIdDecryptionErrorException extends RandflakeIdException
public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null)
{
parent::__construct(
"" !== $message ? $message : "Decryption error occurred.",
"" !== $message ? $message : "Failed to decrypt ID.",
$code,
$previous
);
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/RandflakeIdEncodingErrorException.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class RandflakeIdEncodingErrorException extends RandflakeIdException
public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null)
{
parent::__construct(
"" !== $message ? $message : "Encoding error occurred.",
"" !== $message ? $message : "Failed to encode ID.",
$code,
$previous
);
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/RandflakeIdEncryptionErrorException.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class RandflakeIdEncryptionErrorException extends RandflakeIdException
public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null)
{
parent::__construct(
"" !== $message ? $message : "Encryption error occurred.",
"" !== $message ? $message : "Failed to encrypt ID.",
$code,
$previous
);
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/RandflakeIdInvalidIdOverflowException.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class RandflakeIdInvalidIdOverflowException extends RandflakeIdException
public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null)
{
parent::__construct(
"" !== $message ? $message : "ID is greater than the maximum possible value.",
"" !== $message ? $message : "ID is greater than maximum possible value.",
$code,
$previous
);
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/RandflakeIdUnsupportedIntSizeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class RandflakeIdUnsupportedIntSizeException extends RandflakeIdException
public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null)
{
parent::__construct(
"" !== $message ? $message : "This library requires a 64-bit PHP installation to function.",
"" !== $message ? $message : "Unsupported integer size. Randflake ID generator requires 64-bit integer support.",
$code,
$previous
);
Expand Down
104 changes: 65 additions & 39 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use Adambean\RandflakeId\Exception\RandflakeIdInvalidSecretException;
use Adambean\RandflakeId\Exception\RandflakeIdResourceExhaustedException;
use Adambean\RandflakeId\Exception\RandflakeIdUnsupportedIntSizeException;
use ParagonIE\ConstantTime\Base32Hex;

/**
* Randflake ID generator.
Expand All @@ -37,6 +36,17 @@
*/
final class Generator
{
/*
* -------------------------------------------------------------------------
* Constants
* -------------------------------------------------------------------------
*/

/** @var string Character set used in Base32Hex strings. */
private const B32HEX_CHARS = "0123456789abcdefghijklmnopqrstuv";



/*
* -------------------------------------------------------------------------
* Variables
Expand Down Expand Up @@ -149,7 +159,7 @@ public function __construct(
throw new RandflakeIdInvalidNodeException();
}

if ("" === ($secret = trim($secret)) || strlen($secret) !== RandflakeId::SECRET_LENGTH) {
if (strlen($secret) !== RandflakeId::SECRET_LENGTH) {
throw new RandflakeIdInvalidSecretException();
}

Expand Down Expand Up @@ -266,6 +276,43 @@ private function makeNewRawId(): string
return $this->intToString($id);
}

/**
* Decode a mathematical Base32Hex string into an unsigned numeric string.
*
* @param non-empty-string $idEncoded
*
* @return numeric-string
*
* @throws RandflakeIdDecodingErrorException
* If decoding fails or the decoded ID is out of valid range.
*/
private function decodeBase32HexToNumericString(string $idEncoded): string
{
RandflakeId::assertBase32HexStringId($idEncoded);

$idEncoded = strtolower(trim($idEncoded));
$idDecodedNum = "0";
$length = strlen($idEncoded);
for ($i = 0; $i < $length; $i++) {
$charCode = ord($idEncoded[$i]);
if ($charCode >= 48 && $charCode <= 57) {
$value = $charCode - 48;
} elseif ($charCode >= 97 && $charCode <= 118) {
$value = $charCode - 87;
} else {
throw new RandflakeIdDecodingErrorException("Invalid base32hex character.");
}

$idDecodedNum = bcadd(bcmul($idDecodedNum, "32", 0), strval($value), 0);
}

if (bccomp($idDecodedNum, $this->numMaxMinus1, 0) > 0) {
throw new RandflakeIdDecodingErrorException("Decoded ID is out of valid range.");
}

return $idDecodedNum;
}



/*
Expand Down Expand Up @@ -412,14 +459,7 @@ public function isNumericStringIdValid(string $id): void
*/
public function isEncodedStringIdValid(string $id): void
{
RandflakeId::assertBase32HexStringId($id);

$idDecoded = Base32Hex::decode(strtolower($id));
if ("" === ($idDecoded = trim($idDecoded))) {
throw new RandflakeIdDecodingErrorException("Failed to decode ID.");
}

RandflakeId::addNullPaddingToPackedId($idDecoded);
$this->decodeBase32HexToNumericString($id);
}

/**
Expand Down Expand Up @@ -487,14 +527,14 @@ public function encryptId(string $idRaw): string
{
$this->isNumericStringIdValid($idRaw);

$idRawBytes = pack("J", $this->stringToInt($idRaw));
$idRawBytes = pack("P", $this->stringToInt($idRaw));

$idEncrypted = $this->secretBox?->encrypt($idRawBytes);
if (null === $idEncrypted) {
throw new RandflakeIdEncryptionErrorException("Failed to encrypt ID.");
}

$idEncryptedBytes = unpack("J", $idEncrypted);
$idEncryptedBytes = unpack("P", $idEncrypted);
if (!is_array($idEncryptedBytes) || !isset($idEncryptedBytes[1]) || !is_int($idEncryptedBytes[1])) {
throw new RandflakeIdEncryptionErrorException("Failed to unpack encrypted ID.");
}
Expand Down Expand Up @@ -524,14 +564,14 @@ public function decryptId(string $idEncrypted): string
{
$this->isNumericStringIdValid($idEncrypted);

$idEncryptedBytes = pack("J", $this->stringToInt($idEncrypted));
$idEncryptedBytes = pack("P", $this->stringToInt($idEncrypted));

$idDecrypted = $this->secretBox?->decrypt($idEncryptedBytes);
if (null === $idDecrypted) {
throw new RandflakeIdDecryptionErrorException("Failed to decrypt ID.");
}

$idDecryptedBytes = unpack("J", $idDecrypted);
$idDecryptedBytes = unpack("P", $idDecrypted);
if (!is_array($idDecryptedBytes) || !isset($idDecryptedBytes[1]) || !is_int($idDecryptedBytes[1])) {
throw new RandflakeIdDecryptionErrorException("Failed to unpack decrypted ID.");
}
Expand All @@ -557,14 +597,19 @@ public function encodeId(string $idPlain): string
{
$this->isNumericStringIdValid($idPlain);

if (bccomp($idPlain, $this->numMaxMinus1, 0) === 1) {
throw new RandflakeIdEncodingErrorException("ID is too large.");
if (bccomp($idPlain, "0", 0) === 0) {
return "0";
}

$idPlainBytes = pack("J", $this->stringToInt($idPlain));
$idEncoded = "";
$idRemaining = $idPlain;
while (bccomp($idRemaining, "0", 0) > 0) {
$remainder = bcmod($idRemaining, "32", 0);
$idEncoded = self::B32HEX_CHARS[(int) $remainder] . $idEncoded;
$idRemaining = bcdiv($idRemaining, "32", 0);
}

$idEncoded = Base32Hex::encodeUnpadded($idPlainBytes);
if ("" === ($idEncoded = strtolower(trim($idEncoded)))) {
if ("" === $idEncoded) {
throw new RandflakeIdEncodingErrorException("Failed to encode ID.");
}

Expand All @@ -584,26 +629,7 @@ public function encodeId(string $idPlain): string
*/
public function decodeId(string $idEncoded): string
{
$this->isEncodedStringIdValid($idEncoded);

$idDecoded = Base32Hex::decode(strtolower($idEncoded));
if ("" === ($idDecoded = trim($idDecoded))) {
throw new RandflakeIdDecodingErrorException("Failed to decode ID.");
}

$idDecoded = RandflakeId::addNullPaddingToPackedId($idDecoded);

$idDecodedBytes = unpack("J", $idDecoded);
if (!is_array($idDecodedBytes) || !isset($idDecodedBytes[1]) || !is_int($idDecodedBytes[1])) {
throw new RandflakeIdDecodingErrorException("Failed to unpack decoded ID.");
}

$idDecodedNum = $this->intToString($idDecodedBytes[1]);
if (bccomp($idDecodedNum, "0", 0) < 0 || bccomp($idDecodedNum, $this->numMaxMinus1, 0) > 0) {
throw new RandflakeIdDecodingErrorException("Decoded ID is out of valid range.");
}

return $idDecodedNum;
return $this->decodeBase32HexToNumericString($idEncoded);
}

/**
Expand Down
Loading
Loading