5.5 KiB
5.5 KiB
Base58/Base64 Number Encoding Schema in Detail
Base64 and Base58 are two algorithms for encoding byte arrays in plain text. I initially assumed these were two instances of the same "Base N" algorithm. This is not the case. These are two fundamentally different algorithms.
tldr
-spec b64_enc(Bytes) -> Base64
when Bytes :: binary().
Base64 :: string().
%% @doc
%% Encode a byte array into a base64 string
%% @end
%% general case: at least 3 bytes (24 bits) remaining
%% encode into 4 characters
b64_enc(<<A:6, B:6, C:6, D:6, Rest/binary>>) ->
CA = b64_int2char(A),
CB = b64_int2char(B),
CC = b64_int2char(C),
CD = b64_int2char(D),
[CA, CB, CC, CD | b64_enc(Rest)],
%% terminal case: 2 bytes (= 16 bits) remaining
%% encode into 3 characters and a single padding character
b64_enc(<<A:6, B:6, C:4>>) ->
CA = b64_int2char(A),
CB = b64_int2char(B),
CC = b64_int2char(C bsl 2),
[CA, CB, CC, $=];
%% terminal case: 1 bytes (= 8 bits) remaining
%% encode into 2 characters and two padding characters
b64_enc(<<A:6, B:2>>) ->
CA = b64_int2char(A),
CB = b64_int2char(B bsl 4),
[CA, CB, $=, $=];
%% terminal case: empty byte array
b64_enc(<<>>) ->
[].
-spec b64_dec(Base64) -> Bytes
when Base64 :: string(),
Bytes :: binary().
%% @doc
%% Decode a base64 string into a byte array
%% @end
b64_dec(Base64_String) ->
b64_dec(Base64_String, <<>>).
%% terminal case: two equals signs at the end (decode 1 byte)
b64_dec([W, X, $=, $=], Acc) ->
NW = b64_char2int(W),
NX = b64_char2int(X),
<<LastByte:8, 0:4>> = <<NW:6, NX:6>>,
<<Acc/binary, LastByte:8>>;
%% terminal case: one equals sign at the end (decode 2 bytes)
b64_dec([W, X, Y, $=], Acc) ->
NW = b64_char2int(W),
NX = b64_char2int(X),
NY = b64_char2int(Y),
<<B1:8, B2:8, 0:2>> = <<NW:6, NX:6, NY:6>>,
<<Acc/binary, B1:8, B2:8>>;
%% terminal case: end of string
b64_dec([], Acc) ->
Acc;
%% general case: 4 or more chars remaining (decode 3 bytes)
b64_dec([W, X, Y, Z | Rest], Acc) ->
NW = b64_char2int(W),
NX = b64_char2int(X),
NY = b64_char2int(Y),
NZ = b64_char2int(Z),
NewAcc = <<Acc/binary, NW:6, NX:6, NY:6, NZ:6>>,
b64_dec(Rest, NewAcc).
-spec b58_enc(Bytes) -> Base58
when Bytes :: binary(),
Base58 :: string().
%% @doc
%% Encode a bytestring into base58 notation
b58_enc(Bytes) ->
%% grab leading 0s
{ZerosBase58, Rest} = split_zeros(Bytes, []),
NBitsInRest = bit_size(Rest),
<<RestBigNum:NBitsInRest>> = Rest,
RestBase58 = b58_enc(RestBigNum, []),
ZerosBase58 ++ RestBase58.
-spec split_zeros(Bytes, B58_Zeros_Acc) -> {B58_Zeros, Rest}
when Bytes :: binary(),
B58_Zeros_Acc :: string(),
B58_Zeros :: string(),
Rest :: binary().
%% @private
%% Base58 thinks of your byte array as a big integer, and therefore has no way
%% to distinguish between say <<1,2,3>> and <<0, 0, 0, 1, 2, 3>>. To resolve
%% this, we prepend ASCII `1`s at the beginning of the result, one for each
%% leading zero byte in the input byte array.
%%
%% The ASCII `0` (numeral zero) character is not used in order to avoid
%% ambiguity with ASCII `O` (uppercase letter O)
split_zeros(<<0:8, Rest/binary>>, B58_Zeros) ->
split_zeros(Rest, [$1 | B58_Zeros]);
split_zeros(Rest, B58_Zeros) ->
{B58_Zeros, Rest}.
-spec b58_enc(BytesBigNum, Base58Acc) -> Base58
when BytesBigNum :: integer(),
Base58Acc :: [0..57],
Base58 :: string().
%% @private
%% Encode a number into base58 notation using the standard quotient-remainder
%% algorithm you would use for any other base.
b58_enc(0, Acc) ->
lists:map(fun b58_int2char/1, Acc);
b58_enc(BitNum, Acc) ->
Q = BitNum div 58,
R = BitNum rem 58,
b58_enc(Q, [R | Acc]).
-spec b58_dec(Base58) -> DecodedBytes
when Base58 :: string(),
DecodedBytes :: binary().
%% @doc
%% Decode a Base58-encoded string into a bytestring
%% @end
%% this works by parsing the string as an integer and then converting the
%% integer to "base 256" (256 = 2^8)
b58_dec(Str) ->
%% the number of leading 1 in the input plain-text string tells us the
%% number of leading zeros in the output byte array
{LeadingZeros, RestStr} = split_ones(Str, <<>>),
%% you could make this more efficient by converting this to a single pass
%% over the input string. steps shown separately because this is tutorial
%% code
RestNs = lists:map(fun b58_char2int/1, RestStr),
RestBytes = b58_dec(RestNs, 0),
<<LeadingZeros/binary, RestBytes/binary>>.
%% this is basically the oppsite of split_zeros/2 above
split_ones([$1 | Rest], LeadingZeros) ->
split_ones(Rest, <<LeadingZeros/binary, 1>>);
split_ones(B58Str, LeadingZeros) ->
{LeadingZeros, B58Str}.
%% this parses the input symbols into a big integer
b58_dec([N | Ns], Acc) ->
NewAcc = (Acc*58) + N,
b58_dec(Ns, NewAcc);
%% then converts that big integer into "base 256" (i.e. a byte array)
b58_dec([], FinalAccN) ->
bignum_to_binary_bige(FinalAccN, <<>>).
%% in the encode step, we were converting the number to a base58 "string"
%% here we are doing essentially the same thing, but instead converting the
%% number to a "base 256 string" (i.e. byte array)
bignum_to_binary_bige(0, Acc) ->
Acc;
bignum_to_binary_bige(N, Acc) ->
Q = N div 256,
R = N rem 256,
NewAcc = <<R, Acc/binary>>,
bignum_to_binary_bige(Q, NewAcc).