Base32とは

ご無沙汰しております。

最近多忙で更新できていませんでした。

なにをしていたかと言うと、

  • LCNEMの法人登記
  • LCNEMの銀行口座開設
  • LCNEMウォレットの開発

とかですね。

LCNEMウォレットはAndroid版の開発を進めていたんですが、iOSとAndroidそれぞれの保守をするとコストがかかるという理由で、Web版のみ公開することにしました。

今はAngular使ってWeb版開発してます。

AngularどころかTypeScriptすら今まで使ったこと無かったんですが、ここまでくるのに2日かかりませんでした。

さて脱線しましたが、今日はBase32の話をします。

Base32とは32種類の英数字のみを用いて、バイナリデータやマルチバイト文字を表すためのエンコード方式です。

RFC4648

Base32は、例えばNEMブロックチェーンのアドレスにも使われています。

NEMのアドレスは、以下のような手順で求めます。最後にBase32エンコードされています。

  1. 公開鍵を用意する
  2. SHA-3(256bit)で、公開鍵のハッシュ値を求める
  3. Ripemd(160bit)で、2.のハッシュ値を求める
  4. 3.の前にネットワークタイプを追加
  5. SHA-3(256bit)で、4.のハッシュ値を求める
  6. 5.の前半4バイトを4.の後ろに追加する
  7. 6.をBase32でエンコードする

Base32には、アルファベットA~Zと数字2~7を使います。

似たようなエンコード方式としてBase64がありますが、以下のような違いがあります。

  1. Base32には大文字小文字の区別がいらない
  2. Base32にはOやIとややこしい0,1がない
  3. Base32のほうがBase64よりも使える文字が少ないため、エンコード後のデータは大きくなる

です。ここで、1.と2.は、NEMのアドレスのように、紛らわしくてはいけないものに向いています。なのでBase32が使われているんでしょうね。

Base32の仕組み

次に仕組みを説明します。

  1. データを5バイト=40ビットごとに区切りをつける。余りが出ても良い。
  2. データを5ビットごとに区切りをつける。余りが出た場合、残りも5ビットになるように二進数の0を後ろに足す。
  3. 5ビットの値を10進数に変換して、対応表をもとに変換する。
  4. 2.で埋めてもなお1.に余りが出た場合、5ビットごとに=で埋めて、余りをなくす。

ちょっとよくわからないですね。わかりやすい例を挙げます。

ここに16進数表記で

01 02 03 04 05 FF FE FD

というデータがあるとします。まずこれを手順1.に従って5バイトごとに区切りましょう。

  • 01 02 03 04 05
  • FF FE FD

ではこれを、2進数表記にします。

  • 00000001 00000010 00000011 00000100 00000101
  • 11111111 11111110 11111101

になりますね。次に手順2.に従って5ビットごとに区切ります。

  • 00000 00100 00001 00000 00110 00001 00000 00101
  • 11111 11111 11111 01111 1101

最後1つだけ0の余りが出てしまいました。手順2に従い、5ビットちょうどになるように0で埋めます。

  • 00000 00100 00001 00000 00110 00001 00000 00101
  • 11111 11111 11111 01111 11010

これを10進数表記に変えます。

  • 0 4 1 0 6 1 0 5
  • 31 31 31 15 26

手順3に従い、変換します。0~25が’A’~’Z’に、26~31が’2’~’7’に対応します。

  • A E B A G B A F
  • 7 7 7 P 2

ここで、最後の行は、8文字になっていませんよね。これは手順1のときに余りが出たせいです。

手順4.に従い、=で埋めます。

  • A E B A G B A F
  • 7 7 7 P 2 = = =

最後にこれをC#で実装したコードを載せます。↑のは確認済みです。思わぬバグあれば教えてください。。。


EncodeBase32(byte[] data)
{
    const string Base32Dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    var length = data.Length;

    var byteParts = (int)Math.Ceiling((double)length / 5);
    var remainderBytes = length % 5;
    var bitParts = (int)Math.Ceiling((double)remainderBytes * 8 / 5);

    byte[] buffer = new byte[5 * byteParts];
    data.CopyTo(buffer, 0);

    StringBuilder stringBuilder = new StringBuilder();

    for(int i = 0; i < byteParts; i++)
    {
        UInt64 bytePart = 0;
        for (int j = 0; j < 5; j++)
        {
            bytePart |= (UInt64)buffer[5 * i + j] << (4 - j) * 8;
        }

        for (int j = 0; j < 8; j++)
        {
            if (i + 1 == byteParts && remainderBytes != 0 && j >= bitParts)
            {
                stringBuilder.Append('=');
            }
            else
            {
                var index = bytePart >> (7 - j) * 5 & 0b11111;
                stringBuilder.Append(Base32Dictionary[(int)index]);
            }
        }
    }

    return stringBuilder.ToString();
}