Base64

Base64 は、エンコード方式の1つで、64種類の印字可能な英数字記号を用いてデータをエンコードします。広く使われるエンコード方式であり、例えば電子メールのマルチバイト文字のエンコードなどに用いられます。そのほか、画像ファイルのエンコード、Basic認証などに用いられています。

例えば、UTF8の文字列 1位 を Base64でエンコードすると、MeS9jQ== となります。半角文字の "1" も、全角文字の "位" も別の半角英数に置き換えられていることがわかります。

特徴

Base64 は、データを64種類の文字(A–Z, a–z, 0–9, +, /)とパディング用の文字(=)にエンコードします。後述の通り、6bitずつに分解して対応する64種の文字に置き換えていくので、データサイズにかかわらずエンコード後のデータサイズは約33%(6bit->8bitなので)増加します。

約33%となっているのはパディング用の文字(=)が追加される場合があるためです。ただし基本3割増しのデータ量になることは覚えておきましょう。

また、すべてのデータをエンコードするので元データがテキストだったとしても、読める形にはなりません。これは、Quoted Printable が、印字可能な文字はそのまま出力することと比べ異なる点です。

Quoted Printable は、読める形を保とうとするエンコード方式の為、エンコード対象のデータによってはデータサイズが増減しますが、Base64は一律約33%データサイズが増加します。

アルゴリズムの解説

エンコード

  1. エンコード対象のデータを6bitずつに分解します。データが6bitに足りない分は0で埋めます。
    • 例: 01100001 -> 011000 + 010000
  2. 分解して得られた各6bitのデータを、4文字ずつ対応表を使って文字に置き換えます。4文字に満たない分は "=" で埋めます。
    • 例: 011000 + 010000 -> YQ==
    • エンコード後のBase64の文字数は必ず4倍数になります。

デコード

デコードはエンコードの手順を逆から実行していきます。

  1. 対応表からbitデータに置き換えます。"=" は無視します。
    • 例: YQ== -> 011000 + 010000
  2. 8bitに満たない末尾の0は切り捨てます。
    • 例: 011000 + 010000 -> 01100001

対応表

Base64で使う対応表は次の通りです。6bitに分解したデータを対応表に突き合わせます。デコード時もこの対応表を使うことになります。

10進 2進 文字 10進 2進 文字 10進 2進 文字 10進 2進 文字
0 000000 A 16 010000 Q 32 100000 g 48 110000 w
1 000001 B 17 010001 R 33 100001 h 49 110001 x
2 000010 C 18 010010 S 34 100010 i 50 110010 y
3 000011 D 19 010011 T 35 100011 j 51 110011 z
4 000100 E 20 010100 U 36 100100 k 52 110100 0
5 000101 F 21 010101 V 37 100101 l 53 110101 1
6 000110 G 22 010110 W 38 100110 m 54 110110 2
7 000111 H 23 010111 X 39 100111 n 55 110111 3
8 001000 I 24 011000 Y 40 101000 o 56 111000 4
9 001001 J 25 011001 Z 41 101001 p 57 111001 5
10 001010 K 26 011010 a 42 101010 q 58 111010 6
11 001011 L 27 011011 b 43 101011 r 59 111011 7
12 001100 M 28 011100 c 44 101100 s 60 111100 8
13 001101 N 29 011101 d 45 101101 t 61 111101 9
14 001110 O 30 011110 e 46 101110 u 62 111110 +
15 001111 P 31 011111 f 47 101111 v 63 111111 /

サンプルコード

文字列を渡すと、Base64でエンコードした文字列を返してくれるエンコード処理と同じくデコード処理の実装のサンプルコードです。文字列をエンコード・デコードする場合にはその文字列の文字コードを指定する必要がありますが、子のサンプルではUTF8固定で処理しています。

対応表は64種の文字による配列としています。インデックスとその値が対応表の組み合わせに当たります。例えば"111101(62)" は、ConversionTable[62] で "9" が得られます。逆引きの場合は "9" のインデックスを探します。

処理の詳細はコメントにある通りです。基本は上記アルゴリズムをそのままに実装しています。そこまで難しくはないかと思います。

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

class Base64
{
    public char[] ConversionTable = new char[]
    {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
        'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
        'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
        'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
        'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
        'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
        '8', '9', '+', '/',
    };

    public string Encode(string text)
    {
        var result = "";

        // 文字列(UTF8固定)をバイト型配列に変換
        var bytes = Encoding.UTF8.GetBytes(text);

        // 2進数文字列に展開
        var bits = bytes
            .SelectMany(octet => Convert.ToString(octet, 2).PadLeft(8, '0'))
            .ToArray();

        // 6bit ずつ処理する
        var bit6 = "";
        for (int i = 0; i < bits.Length; i++)
        {
            // 2進数文字列で保持する
            bit6 += bits[i];

            // 6bit になったら変換表から対応文字を取得し出力
            if (bit6.Length == 6)
            {
                // 2進数文字列として変換
                var tableIndex = Convert.ToInt32(bit6, 2);
                result += ConversionTable[tableIndex];

                bit6 = "";
            }

            // 最後に6bitに足りていない場合は0で埋める
            if (i == bits.Length - 1 && bit6.Length != 0)
            {
                var padCount = 6 - bit6.Length;
                for (int j = 0; j < padCount; j++) bit6 += "0";

                // 2進数文字列として変換
                var tableIndex = Convert.ToInt32(bit6, 2);
                result += ConversionTable[tableIndex];
            }
        }

        // 出力文字数が4の倍数になるように "=" を付け足す
        while (result.Length % 4 != 0) result += "=";

        return result;
    }

    public string Decode(string text)
    {
        var bytes = new List<byte>();
        var bitPosition = 0; // 現在処理中のbit位置
        var buff = "";       // 2進数文字列保持用

        // = を削除
        text = text.TrimEnd('=');

        foreach (var c in text)
        {
            // 変換表から値を取得
            var value = Array.IndexOf(this.ConversionTable, c);

            // 6桁の2進数文字列に変換
            var n2 = Convert.ToString(value, 2).PadLeft(6, '0');

            // 1ビットずつループ
            foreach (var n in n2)
            {
                // 8ビット中何ビット目の出力かカウント
                bitPosition++;

                buff += n;

                // 8ビット分処理済の場合は出力し初期化
                if (bitPosition == 8)
                {
                    // 2進数文字列をバイト型に変換して保持
                    bytes.Add(Convert.ToByte(buff, 2));
                    bitPosition = 0;
                    buff = "";
                }
            }
        }

        // 取得したバイトデータを文字列(UTF8固定)に変換
        var result = Encoding.UTF8.GetString(bytes.ToArray());

        return result;
    }
}