ご無沙汰しております。
最近多忙で更新できていませんでした。
なにをしていたかと言うと、
- LCNEMの法人登記
- LCNEMの銀行口座開設
- LCNEMウォレットの開発
とかですね。
LCNEMウォレットはAndroid版の開発を進めていたんですが、iOSとAndroidそれぞれの保守をするとコストがかかるという理由で、Web版のみ公開することにしました。
今はAngular使ってWeb版開発してます。
AngularどころかTypeScriptすら今まで使ったこと無かったんですが、ここまでくるのに2日かかりませんでした。
さて脱線しましたが、今日はBase32の話をします。
Base32とは32種類の英数字のみを用いて、バイナリデータやマルチバイト文字を表すためのエンコード方式です。
Base32は、例えばNEMブロックチェーンのアドレスにも使われています。
NEMのアドレスは、以下のような手順で求めます。最後にBase32エンコードされています。
- 公開鍵を用意する
- SHA-3(256bit)で、公開鍵のハッシュ値を求める
- Ripemd(160bit)で、2.のハッシュ値を求める
- 3.の前にネットワークタイプを追加
- SHA-3(256bit)で、4.のハッシュ値を求める
- 5.の前半4バイトを4.の後ろに追加する
- 6.をBase32でエンコードする
Base32には、アルファベットA~Zと数字2~7を使います。
似たようなエンコード方式としてBase64がありますが、以下のような違いがあります。
- Base32には大文字小文字の区別がいらない
- Base32にはOやIとややこしい0,1がない
- Base32のほうがBase64よりも使える文字が少ないため、エンコード後のデータは大きくなる
です。ここで、1.と2.は、NEMのアドレスのように、紛らわしくてはいけないものに向いています。なのでBase32が使われているんでしょうね。
Base32の仕組み
次に仕組みを説明します。
- データを5バイト=40ビットごとに区切りをつける。余りが出ても良い。
- データを5ビットごとに区切りをつける。余りが出た場合、残りも5ビットになるように二進数の0を後ろに足す。
- 5ビットの値を10進数に変換して、対応表をもとに変換する。
- 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();
}