How to pass Int to Base64 in PHP?
Base64 can store 6 bits for each character used. Assuming we are using int64 or uint64 we use 64 bits, which could be represented in ~11 characters.
I tried to answer this question, but PHP fails to convert the values correctly.
$int = 5460109885665973483;
echo base64_encode($int);
Returns:
NTQ2MDEwOTg4NTY2NTk3MzQ4Mw==
This is incorrect, we are using 26 characters to represent 64 bits! That's insane. I even understand the reason, it uses the value as string, not as int. only the conversion to string makes use of 19 bytes, which therefore (19*8)/6 characters are used by PHP.
However, other languages handle at the byte level, such as Golang:
bt := make([]byte, 8)
binary.BigEndian.PutUint64(bt, 5460109885665973483)
fmt.Print(base64.StdEncoding.EncodeToString(bt))
Returns:
S8Y1Axm4FOs=
S8Y1Axm4FOs=
is exactly 11 characters (ignoring padding), which is exactly the 64-bit representation. in this case you can retrieve the value using the binary.BigEndian.Uint64
after the decode
of Base64.
Which way could I get the same result as Golang in PHP?
2 answers
The best way to do this in PHP is by using the pack. This function will allow you to have a big-endian implementation of byte order.
<?php
$byte_array = pack('J*', 5460109885665973483);
var_export( base64_encode($byte_array) );
// Output: S8Y1Axm4FOs=
To reverse this process, you can use the opposite functionunpack
<?php
$encoded = "S8Y1Axm4FOs=";
$decoded = base64_decode($encoded);
var_export( unpack("J*", $decoded) );
// Output: [ 1 => 5460109885665973483 ]
The J * represents a 64 bit, big endian byte order
The answer of @Valdeir Psr answers the question and solves the problem. However, I had a completely different idea of solving the situation, using bitwise.
I thought of simply dividing the value every 6 bits, then encoding it to base64. This would not approve of side-channel attacks (in the same way as the original PHP), but it would be enough for the purpose, I believe.
I tried to execute this idea, and... it worked. So, I'm sharing here, although I will use pack
.
So just do:
function base64_encode_int64(int $int) : string {
$alfabeto = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
$base64 = '';
for($l = 58; $l > 0; $l -= 6){
$base64 .= $alfabeto[($int >> $l) & 0x3F];
}
$base64 .= $alfabeto[($int << 2) & 0x3F];
return $base64;
}
The last shift should be reversed, because it has only 4 bits, 6 are needed. Then you need to add 2 bits to the end, for this reason the shift "to the opposite side".
To decode we use |
, which is the simplest solution, I believe.
function base64_decode_int64(string $base64) {
$alfabeto = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
$int = 0;
if(mb_strlen($base64, '8bit') !== 11){
return false;
}
for($l = 58; $l > -3; $l -= 6){
$letra = strpos($alfabeto, $base64[(58-$l)/6]);
if($letra === false) {
return false;
}
if($l > 0){
$int |= ($letra) << $l;
}else{
$int |= ($letra) >> 2;
}
}
return $int;
}
I don't believe that strpos
is the best option, plus the amount of if
is in disturbing a little. This was accurate because the entry ($base64
) must use the same dictionary, so it must return false
in case of error, as well as being limited to 11 characters.
The if($l > 0){
I brought into the for
, but I do not believe that it is not ideal. I did this so as not to have to create a new condition outside the loop (duplicate the if($letra)
), but I believe there must be a way to make this "universal", maybe by doing a few shifts before (to the opposite side), no you're .
Now the tests:
echo $int = 5460109885665973483;
echo PHP_EOL;
echo $b64 = base64_encode_int64($int);
echo PHP_EOL;
echo base64_decode_int64($b64);
Returns:
5460109885665973483
S8Y1Axm4FOs
5460109885665973483