Reputation: 657
In order o be able to create a 6 characters case insensitive reversible ash I used the code I found here: http://web.archive.org/web/20130727034425/http://blog.kevburnsjr.com/php-unique-hash which works with base64 and converted it to work with base36.
However, I'm not able to reverse the hash to its original value as the original code with class with base64 does.
EDIT: In response to the feedback I got here, I understand I didn't use the right term. I know well what hash means, what encryption is, but I just used it because it is how it is presented in the original code that I used for this purpose. I didn't take the time to explain better what I really wanted, but my goal it to convert any integer from 1 to lets say 10.000.000 into a 6 or 8 bytes unique string.
class PseudoCrypt {
/* Key: Next prime greater than 36 ^ n / 1.618033988749894848 */
/* Value: modular multiplicative inverse */
private static $golden_primes = array(
'1' => '1',
'41' => '59',
'2377' => '1677',
'147299' => '187507',
'9132313' => '5952585',
'566201239' => '643566407',
'35104476161' => '22071637057',
'2176477521929' => '294289236153',
'134941606358731' => '88879354792675',
'8366379594239857' => '7275288500431249',
'518715534842869223' => '280042546585394647'
);
/* Ascii : 0 9, A Z, a z */
/* $chars = array_merge(range(48,57), range(65,90), range(97,122)) */
private static $chars36 = array(
0=>48,
1=>49,
2=>50,
3=>51,
4=>52,
5=>53,
6=>54,
7=>55,
8=>56,
9=>57,
10=>65,
11=>66,
12=>67,
13=>68,
14=>69,
15=>70,
16=>71,
17=>72,
18=>73,
19=>74,
20=>75,
21=>76,
22=>77,
23=>78,
24=>79,
25=>80,
26=>81,
27=>82,
28=>83,
29=>84,
30=>85,
31=>86,
32=>87,
33=>88,
34=>89,
35=>90
);
public static function base36($int) {
$key = '';
while($int > 0) {
$mod = $int-(floor($int/36)*36);
$key .= chr(self::$chars36[$mod]);
$int = floor($int/36);
}
return strrev($key);
}
public static function hash($num, $len = 5) {
$ceil = bcpow(36, $len);
$primes = array_keys(self::$golden_primes);
$prime = $primes[$len];
$dec = bcmod(bcmul($num, $prime), $ceil);
$hash = self::base36($dec);
return str_pad($hash, $len, "0", STR_PAD_LEFT);
}
public static function unbase36($key) {
$int = 0;
foreach(str_split(strrev($key)) as $i => $char) {
$dec = array_search(ord($char), self::$chars36);
$int = bcadd(bcmul($dec, bcpow(36, $i)), $int);
}
return $int;
}
public static function unhash36($num, $len = 5) {
$ceil = pow(36, $len);
$prime = self::$golden_primes[$len];
$dec = ($num * $prime)-floor($num * $prime/$ceil)*$ceil;
$hash = self::base36($dec);
return str_pad($hash, $len, “0″, STR_PAD_LEFT);
}
}
echo "<pre>";
foreach(range(1, 100000) as $n) {
echo $n." - ";
$hash = PseudoCrypt::hash($n, 8);
echo $hash." - ";
echo PseudoCrypt::unhash36($hash)."<br/>";
}
Upvotes: 1
Views: 178
Reputation: 145
I made another algorithm.
It's
<?php
class PseudoCrypt1
{
private static $keychars = 'CZPXD5H2FIWB81KE76JY93V4ORLAMT0QSUNG'; // Dictionary of allowed unique symbols, shuffle it for yourself or remove unwanted chars (don't forget to call testParameters after changing)
private static $divider = 19; // Tune divider for yourself (don't forget to call testParameters after changing)
private static $biasDivider = 14; // Tune bias divider for yourself (don't forget to call testParameters after changing)
private static $noise = 23; // Any positive number
public static function testParameters()
{
if (strlen(static::$keychars) < static::$divider + static::$biasDivider - 1) {
throw new Exception('Check your divider and biasDivider. It must be less than keychars length');
}
}
public static function encode(int $i): string
{
if ($i < 0) {
throw new Exception('Expected positive integer');
}
$keychars = static::$keychars;
$i = $i + static::$noise; // add noise to a number
$bias = $i % static::$biasDivider;
$res = '';
while ($i > 0) {
$div = $i % static::$divider;
$i = intdiv($i, static::$divider);
$res .= $keychars[$div + $bias];
}
// Current version of an algorithm is one of these chars (if in the future you will need to identify version)
// Remember this chars on migrating to new algorithm/parameters
$res .= str_shuffle('LPTKEZG')[0];
$res .= $keychars[$bias]; // Encoded bias
return $res;
}
public static function decode($code)
{
$keychars = static::$keychars;
$biasC = substr($code, -1);
$bias = strpos($keychars, $biasC);
$code = substr($code, 0, -2);
$code = str_split(strrev($code));
$val = 0;
foreach ($code as $c) {
$val *= static::$divider;
$val += strpos($keychars, $c) - $bias;
}
return $val - static::$noise;
}
}
Output
36926 -> 7IWFZX
927331 -> F4WIKP2
9021324 -> AT66R7P1
You can test it with this little test (it doesn't include test for uniqueness, but algorithm is unique):
PseudoCrypt1::testParameters();
for ($i = 4000000; $i < 9500000; $i++) {
$hash = PseudoCrypt1::encode($i);
echo $i.':'.strlen($hash).':'.$hash.PHP_EOL;
if ($i != PseudoCrypt1::decode($hash)) {
echo 'FAIL:'.$i.PHP_EOL;
die();
}
}
Upvotes: 0
Reputation: 657
I developed the solution myself, so here am I sharing the code.
The issue to the code in my question was that the $golden_primes array needed to have different values to match the primes based on 36 characters and not 62.
It works perfect generating unique strings (upper case letters and digits), tested with numbers from 1 to 99,999,999,999,999 (up to 9 characters reversible "hash") and didn't get any collision.
Code:
<?php
class PseudoCrypt {
/* Key: Next prime greater than 36 ^ n / 1.618033988749894848 */
/* Value: modular multiplicative inverse */
private static $golden_primes = array(
'1' =>'1',
'23' =>'11',
'809' =>'809',
'28837' => '29485',
'1038073' =>'179017',
'37370153' => '47534873' ,
'1345325473' => '264202849',
'48431716939' => '19727015779',
'1743541808839' =>'1532265214711',
'62767505117101' =>'67935388019749'
);
/* Ascii : 0 9, A Z, a z */
/* $chars = array_merge(range(48,57), range(65,90), range(97,122)) */
private static $chars36 = array(0=>48, 1=>49, 2=>50, 3=>51, 4=>52, 5=>53, 6=>54, 7=>55, 8=>56, 9=>57,10=>65,11=>66,12=>67,13=>68,14=>69,15=>70,16=>71,17=>72,18=>73,19=>74,20=>75,21=>76,22=>77,23=>78,24=>79,25=>80,26=>81,27=>82,28=>83,29=>84,30=>85,31=>86,32=>87,33=>88,34=>89,35=>90);
public static function base36($int) {
$key = "";
while($int > 0) {
$mod = $int-(floor($int/36)*36);
$key .= chr(self::$chars36[$mod]);
$int = floor($int/36);
}
return strrev($key);
}
public static function unbase36($key) {
$int = 0;
foreach(str_split(strrev($key)) as $i => $char) {
$dec = array_search(ord($char), self::$chars36);
$int = bcadd(bcmul($dec, bcpow(36, $i)), $int);
}
return $int;
}
public static function hash($num, $len = 5) {
$ceil = bcpow(36, $len);
$primes = array_keys(self::$golden_primes);
$prime = $primes[$len];
$dec = bcmod(bcmul($num, $prime), $ceil);
$hash = self::base36($dec);
return str_pad($hash, $len, "0", STR_PAD_LEFT);
}
public static function unhash($hash) {
$len = strlen($hash);
$ceil = bcpow(36, $len);
$mmiprimes = array_values(self::$golden_primes);
$mmi = $mmiprimes[$len];
$num = self::unbase36($hash);
$dec = bcmod(bcmul($num, $mmi), $ceil);
return $dec;
}
}
/// Test
echo "<pre>";
foreach(range(99999999990000, 99999999999999) as $n) {
echo $n." - ";
$hash = PseudoCrypt::hash($n, 9);
echo $hash." - Reversed: ";
echo PseudoCrypt::unhash($hash)."<br/>";
}
Upvotes: 1
Reputation: 34113
In order o be able to create a 6 characters case insensitive reversible ash I used the code I found here:
Hashes aren't reversible, by definition.
However, I'm not able to reverse the hash to its original value as the original code with class with base64 does.
Base64 isn't a hash, it's a method of encoding arbitrary data.
What kind of problem are you trying to solve with case-insensitive encoding? Is this for subdomains?
EDIT - to answer a comment:
what I really wanted, but my goal it to convert any integer from 1 to lets say 10.000.000 into a 6 or 8 bytes unique string.
This description sounds dangerously close to encrypting URL parameters, which is an anti-pattern that will inevitably cause severe misery for some developer in the future.
Other than that, the solution is pretty straightforward:
$string = base_convert($integer, 10, 36);
And then to do the reverse:
$integer = base_convert($string, 36, 10);
Upvotes: 0
Reputation: 7518
Don't you want to do a unbase36(..)
in below code instead of base36(..)
?
public static function unhash36($num, $len = 5) {
$ceil = pow(36, $len);
$prime = self::$golden_primes[$len];
$dec = ($num * $prime)-floor($num * $prime/$ceil)*$ceil;
$hash = self::base36($dec); <=== problem
return str_pad($hash, $len, “0″, STR_PAD_LEFT);
}
Upvotes: 0