How to pass Int to Base64 in PHP?

9

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've tried answer this question , but PHP fails to convert the values correctly.

$int = 5460109885665973483;
echo base64_encode($int);

Return:

NTQ2MDEwOTg4NTY2NTk3MzQ4Mw==

This is incorrect, we are using 26 characters to represent 64 bits! This is insane. I even understand the reason, it uses the value as a string, not as int. But the conversion to string does use 19 bytes, which therefore (19 * 8) / 6 characters are used by PHP.

However, other languages handle byte-level, such as Golang:

bt := make([]byte, 8)
binary.BigEndian.PutUint64(bt, 5460109885665973483)

fmt.Print(base64.StdEncoding.EncodeToString(bt))

Return:

S8Y1Axm4FOs=

The S8Y1Axm4FOs= is exactly 11 characters (ignoring padding), which is exactly the 64-bit representation. In this case you can retrieve the value using binary.BigEndian.Uint64 after decode Base64.

Which way could I get the same Golang result in PHP?

    
asked by anonymous 31.12.2017 / 21:56

2 answers

10

The best way to do this in PHP is to use pack . This function will allow you to have a big-endian byte-order implementation.

<?php

$byte_array = pack('J*', 5460109885665973483);    
var_export( base64_encode($byte_array) );

// Output: S8Y1Axm4FOs=

To reverse this process, you can use the opposite function unpack

<?php

$encoded = "S8Y1Axm4FOs=";

$decoded = base64_decode($encoded);

var_export( unpack("J*", $decoded) );

// Output: [ 1 => 5460109885665973483 ]
  

J represents a 64 bit, big endian byte order

    
31.12.2017 / 22:32
3

The answer from @Valdeir Psr answers the question and solves the problem. However, I had a completely different idea of resolving 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 would be sufficient for the purpose, I believe.

I tried to execute this idea, and ... it worked. So I am sharing here, although I will use pack .

So, just do it:

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 must be inverted , because it has only 4 bits, it is necessary 6. Then we need to add 2 bits at the end, hence the offset " / p>

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 do not believe strpos is the best option, plus the amount of if is in a bit of a nuisance. This was necessary because the input ( $base64 ) should use the same dictionary, so it should return false in case of error, in addition to being limited in 11 characters.

The if($l > 0){ I brought in for , but I do not think it's ideal. I did this so I did not have to create a new condition outside of the loop (duplicate if($letra) ), but I believe there should be a way to make it "universal", maybe doing some shifts before (to the opposite side), I do not know .

Now the tests:

echo $int = 5460109885665973483;
echo PHP_EOL;
echo $b64 = base64_encode_int64($int);
echo PHP_EOL;
echo base64_decode_int64($b64);

Return:

5460109885665973483
S8Y1Axm4FOs 
5460109885665973483

Try this here

    
02.01.2018 / 20:01