C++ での Base 64 エンコーディングの実装

Abdul Mateen 2023年10月12日
  1. エンコード方式 Base_64
  2. C++ でのBase_64エンコーディングの実装
C++ での Base 64 エンコーディングの実装

このチュートリアルでは、C++ の base_64 でのエンコーディングについて説明します。

最初に、base_64 エンコーディングと、それが必要な理由と場所について説明します。 後で、C++ での base_64 のエンコード/デコードについて説明します。

エンコード方式 Base_64

Base_64 はエンコーディング スキームへの追加です。 これは、バイナリ データを ASCII 文字列で表すという点で、バイナリからテキストへのエンコーディングに似ています。

違いは、base_64 エンコーディングが基数 64 への変換を使用することです。 Base_64 エンコーディング名は、塩基の数学的定義に由来します。

基数は、数体系の基数を表します。 同様に、base_2 には 0 と 1 の 2つの基本数字しかありません。

8 進数システムであるBase_8には、0 から 7 までの 8つの基本数字があります。

同様に、base_16 には 0 から 15 までの 16 個の基本数字があり、A から F を使用して 10 から 15 を表します。 base_64 には、以下で構成される 64 個の基本数字があります。

  1. 26 大文字のアルファベット
  2. 26個の小さなアルファベット
  3. 0~9の10桁
  4. 2つの記号 +/

Base_64エンコーディングは、ASCII を処理するように設計されたメディアを介してデータを転送するために一般的に使用されます。 base_64は、メディアを介して送信されるデータの整合性を維持しようとします。

主なアプリケーションは、MIME 経由の電子メールであり、複雑なデータを XML で保存します。 Base_64 はプライバシー強化電子メール (PEM) とも呼ばれます。

Base_64 ステップのエンコード

エンコードの場合、データはバイナリ文字列としてあり、文字列内の各文字を操作する必要があります。 base_64 でエンコードするには、次の手順を実行する必要があります。

  • 各文字の ASCII 値を取得します。
  • ASCII 値の 8 ビット バイナリを見つけます。
  • ステップ2で得た8ビットを、桁を並べ替えて6ビットに変換します(一部のビット操作を含む何らかの操作が必要です(後述))。
  • 6 ビット バイナリを対応する 10 進数値に変換します。
  • base_64 (すでに説明した base_64 の基本数字) を使用して、それぞれの base_64 文字を各 10 進数値に割り当てます。

ここでは、8 ビット グループから 6 ビット グループへの変換であるステップ 3 の詳細について説明します。

8 ビット グループを 6 ビット グループに変換する手順

最初に説明したように、Base_64 エンコーディングでは、64 個の主要な文字/数字がありますが、通常はデータをバイト単位で読み書きします。 1 バイトは 8 ビットで、0 から 255 を格納できます。つまり、1 バイトで一意の 256 を表すことができます。

6 ビットは 64 の一意の値を表すことができます。最後の 2 ビットは 0 のままにして、各バイトが Base_64 エンコード方式の 1 桁/文字のみを格納するようにする必要があります。

各文字/ASCII 値は 8 ビットを使用します。 したがって、各バイトの 2 ビットを調整するには、元のデータよりも多くのストレージが必要になります。

Base_64 エンコーディングでは、データを失うことなく 6 ビットに変換する必要があります。

8 と 6 の LCM を取ると、24 になります。3 バイトには 24 ビットがありますが、8 ビットのうち 6 ビットを使用すると (最後の 2 ビットは使用されません)、24 ビットには 4 バイトが必要です。 したがって、データを失うことなく、3つの 8 ビット グループのそれぞれを 4つの 6 ビット グループに変換できます。

最初のステップは、データを 3 バイトのセットにグループ化することです。 最後のグループのバイト数が少ない場合、値が 0 のバイトを追加してグループを完成させます。

次に、3 バイトの各セットは、次の操作を使用して 4 バイトにグループ化されます。 3 バイトのセットを t1, t2 & t3 として、4 バイトを f1, f2, f3 & f4 として考えてみましょう。

f1 = ( t1 & 0xfc ) >> 2

マスク 0xfc (バイナリ 11111100 に相当) を考慮し、ビット単位で適用し、セットの最初のバイトとマスクの間で操作します。 次に、ビット単位の演算結果に対して右シフトを 2 回使用します。

シフト操作は左の 6 ビットを右に転送し、最後の 2 ビットは 0 になります。

マスク 0xfc の最初の 2 ビットは 0 です。 演算によってセットの最初のバイトの最初の 2 ビットが 0 になる場合 (つまり、最初のバイトの最後の 6 ビットが考慮されることを意味します)、最初の 2 ビット (この演算では無視されます) は次のプロセスで考慮されます。

f2 = ( ( t1 & 0x03 ) << 4 ) + ( ( t2 & 0xf0 ) >> 4 )

ここでは、演算の最初のバイトにマスク 0x03 00000011 が適用されます (つまり、最初の 2 ビットのみが考慮され、最後の 6 ビットは前の演算で既に考慮されています)。 シフト操作は、最初のバイトの結果の 2 ビットを左に転送し、式の 5 番目と 6 番目のビットにします。

マスク 0xf0 11110000 は、演算の 2 番目のバイトに適用されます (つまり、最後の 4 ビットのみが考慮されます)。 シフト操作は、結果の 4 ビットを右に転送して、式の最初の 4 ビットにします。

式の最初の部分では 5 番目と 6 番目のビットがオンになり、2 番目の部分では最初の 4 ビットがオンになり、全体として、最初の 6 ビットがオンになり、最後のビットがオフになります。

最後に、それらを組み合わせて、最後の 2 ビットがオフになっているバイトを取得します。 このステップでは、6 ビットの別のバイトを取得しました。最初のバイトが完了し、2 番目のバイトの最初の 4 ビットが考慮されます。

f3 = ( ( t2 & 0x0f ) << 2 ) + ( ( t3 & 0xc0 ) >> 6 )

マスク 0x0f 00001111 は、操作のために 2 番目のバイトに適用されます (つまり、最初の 4 ビットのみが考慮されます)。 シフト操作は、結果の 4 ビットを左に転送して式の 3、4、5、6 ビットにし、最初の 2 ビット用のスペースを作成します。

次に、操作のためにマスク 0xc0 11000000 が 3 番目のバイトに適用されます (つまり、最初の 2 ビットのみが考慮されます)。 シフト操作は、結果の 2 ビットを右に転送して、式の最初と 2 番目のビットにします。

最後に、両方の結果を組み合わせて、6 ビット グループの 3 番目のバイトを取得します。 繰り返しますが、セットでは、セットの 2 番目のバイトと 3 番目のバイトの 2 ビットが完了しました。

f4 = t3 & 0x3f

最後に、3 番目のバイトには操作のみがあり、マスク 0x3f 00111111 は最初の 6 ビットがオンで、最後の 2 ビットがオフです。 3 番目のバイトの操作では、3 番目のバイトの残りの 6 ビットが考慮されます。

base_64 で使用される 64 個の基本数字については既に説明しました。 次のステップでは、4 バイトのセット (ビット演算を使用して取得) の各バイトが base_64 に変換され、文字列に連結されます。

PLAY という単語をエンコードしてみましょう。 最初のステップでは、それぞれ 3 キャラクターのセットを作成します。 最初のセットには、PLA があります。

次の段階では、Y\0\0 があります。 ここで、\0 は、セットを完成させるために追加されたヌル文字です。

これらの各文字の ASCII は 80 76 65 89 0 0 です。 対応するバイナリ値は 01010000 01001000 01000001 01011001 です。

では、ビット操作をしましょう。

  1. f1 = 01010000 & 11111100 = 01010000 >> 2 = 00010100 = 20
  2. 01010000 & 00000011 = 0000000 << 4 = 00000000 式の最初の部分
  3. 01001000 & 11110000 = 01010000 >> 4 = 00000101 式の 2 番目の部分
  4. f2 = 00000000 + 00000101 = 00000101 = 5、最初の部分と 2 番目の部分の結果を加算
  5. 01001000 & 00001111 = 00001000 << 2 = 00100000 式の最初の部分
  6. 01000001 & 11000000 = 01000000 >> 4 = 00000100 式の 2 番目の部分
  7. f3 = 00100000 + 00000100 = 00100100 = 36、最初の部分と 2 番目の部分の結果を加算
  8. f4 = 01000001 & 00000011 = 00000001 = 1

次に、2 番目と 3 番目の値が 0 である次のセットで操作を繰り返します。 したがって、結果は次のようになります。

f1 = 00010110 = 21

f2 = 00010000 = 16

f3 = 0

f4 = 0

次に、これらの値を base_64 に変換する必要があります。 また、最後の 2 バイトにいくつかのセンチネル/特殊文字を配置して、デコード プロセスがそれらを認識し、それに応じてデコードできるようにする必要があります。

最初のセットでは、f1= 20, f2 = 5, f3 = 36 & f4 = 1. 対応する base_64 値は UFkB になります。

次のセットは f1 = 21, f2 = 16, f3 = 0 & f4 = 0. ここでも、対応する base_64 値は VQ^^ になり、キャレット記号が特殊文字として使用されるため、文字列全体は UFkBV^^ になります。

デコード プロセスは単純に逆のプロセスです。 以下の C++ コードから両方のメソッドをすばやく取得できます。

C++ でのBase_64エンコーディングの実装

C++ でエンコードを行うのは簡単なプロセスです。 C++ で (説明した手順を) すぐに実装できます。

段階的に説明し、最後に 2つの例を含む完全なコードを示します。

まず、base_64 変換のために、base_64 の基本的な数字/文字を含む定数文字列を定義します。

const string base64_chars =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

関数のコーディングとデコードについて説明する前に、最初にいくつかの定義があります。 主に、エンコードとデコードに使用されるマスクがいくつかあります。

これらのマスクのうち 6つについては、8 ビット グループから 6 ビット グループへの変換を説明する際に既に説明しました。

これらのマスクの一部は、デコード プロセスで 6 ビット グループから 8 ビット グループに変換するために使用されます。さらに、さらに 2つのマスクが必要になります。 全部で8枚のマスクがあります。

typedef unsigned char UC;
typedef unsigned int UI;
#define EXTRA '^'
#define MASK1 0xfc
#define MASK2 0x03
#define MASK3 0xf0
#define MASK4 0x0f
#define MASK5 0xc0
#define MASK6 0x3f
#define MASK7 0x30
#define MASK8 0x3c

次に、エンコード機能について考えます。 3キャラセットでお作りします。

次に、それらをビット演算を使用して 4 文字のグループに変換します。詳細については、既に説明しました。 最後に、4 文字のグループの各バイトを変換して連結し、エンコードされた文字列を作成します。

コードは次のとおりです。

string encode_base64(UC const* buf, UI bufLen) {
  string encoded = "";
  UI i = 0, j = 0, k = 0;
  UC temp_a_3[3], temp_4[4];
  for (i = 0; i < bufLen; i += 3) {
    for (j = i, k = 0; j < bufLen && j < i + 3; j++) temp_a_3[k++] = *(buf++);
    for (; k < 3; k++) temp_a_3[k] = '\0';
    temp_4[0] = (temp_a_3[0] & MASK1) >> 2;
    temp_4[1] = ((temp_a_3[0] & MASK2) << 4) + ((temp_a_3[1] & MASK3) >> 4);
    temp_4[2] = ((temp_a_3[1] & MASK4) << 2) + ((temp_a_3[2] & MASK5) >> 6);
    temp_4[3] = temp_a_3[2] & MASK6;
    for (j = i, k = 0; j < bufLen + 1 && j < i + 4; j++, k++)
      encoded += base64_chars[temp_4[k]];
    for (; k < 4; k++) encoded += EXTRA;  // sentinal value
  }
  return encoded;
}

この関数は 2つのパラメーターを受け取ります。1つ目は生データ (コーディングのために送信される) で、2つ目はメッセージの長さです。 サイズ 3 と 4 の 2つの配列を宣言しました。ループ内では、サイズ 3 の最初の配列にデータを格納します。

次に、最後のセットのバイト数が少ない場合は、null 文字を追加して最後のセットを完成させます。 次に、8 ビット データを 6 ビット単位の操作に変換する 4つのステートメントがあります。

最後に、最後から 2 番目のループで、4つの 6 ビット文字のグループを base_64 に変換します。

最後のループでは、余分な文字を格納して 4 バイトのセットを完成させます。 次に、decode 関数があります。

vector<UC> decode_base64(string const& encoded) {
  UI i = 0, j = 0, k = 0, in_len = encoded.size();
  UC temp_a_3[3], temp_4[4];
  vector<UC> decoded;
  for (i = 0; i < in_len; i += 4) {
    for (j = i, k = 0; j < i + 4 && encoded[j] != EXTRA; j++)
      temp_4[k++] = base64_chars.find(encoded[j]);
    for (; k < 4; k++) temp_4[k++] = '\0';
    temp_a_3[0] = (temp_4[0] << 2) + ((temp_4[1] & MASK7) >> 4);
    temp_a_3[1] = ((temp_4[1] & MASK4) << 4) + ((temp_4[2] & MASK8) >> 2);
    temp_a_3[2] = ((temp_4[2] & MASK2) << 6) + temp_4[3];
    for (j = i, k = 0; k < 3 && encoded[j + 1] != EXTRA; j++, k++)
      decoded.push_back(temp_a_3[k]);
  }
  return decoded;
}

この関数は、エンコードされたメッセージを受け取り、次の手順を含む逆の操作を行います。

  1. base_64 文字セットから取得した各文字のインデックスを取得し、4 バイトのセットを作成します。
  2. ここでも、エンコード プロセスで保存した特殊文字に対して 0 を追加します。
  3. 次に、逆ビット操作で 4 バイトのセットを 3 バイトのセットに変換します (ここでは、これらの操作の詳細には立ち入りません)。
  4. 最後に、3 バイトのセットを結合して、結合されたデコードされたメッセージを取得します。

最後に、コーディングとエンコーディングの 2つの例を含む完全なコードを示します。

#include <iostream>
#include <string>
#include <vector>

using namespace std;

typedef unsigned char UC;
typedef unsigned int UI;
#define EXTRA '^'
#define MASK1 0xfc
#define MASK2 0x03
#define MASK3 0xf0
#define MASK4 0x0f
#define MASK5 0xc0
#define MASK6 0x3f
#define MASK7 0x30
#define MASK8 0x3c

const string base64_chars =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

string encode_base64(UC const* buf, UI bufLen) {
  string encoded = "";
  UI i = 0, j = 0, k = 0;
  UC temp_a_3[3], temp_4[4];
  for (i = 0; i < bufLen; i += 3) {
    for (j = i, k = 0; j < bufLen && j < i + 3; j++) temp_a_3[k++] = *(buf++);
    for (; k < 3; k++) temp_a_3[k] = '\0';
    temp_4[0] = (temp_a_3[0] & MASK1) >> 2;
    temp_4[1] = ((temp_a_3[0] & MASK2) << 4) + ((temp_a_3[1] & MASK3) >> 4);
    temp_4[2] = ((temp_a_3[1] & MASK4) << 2) + ((temp_a_3[2] & MASK5) >> 6);
    temp_4[3] = temp_a_3[2] & MASK6;
    for (j = i, k = 0; j < bufLen + 1 && j < i + 4; j++, k++)
      encoded += base64_chars[temp_4[k]];
    for (; k < 4; k++) encoded += EXTRA;  // sentinal value
  }
  return encoded;
}
vector<UC> decode_base64(string const& encoded) {
  UI i = 0, j = 0, k = 0, in_len = encoded.size();
  UC temp_a_3[3], temp_4[4];
  vector<UC> decoded;
  for (i = 0; i < in_len; i += 4) {
    for (j = i, k = 0; j < i + 4 && encoded[j] != EXTRA; j++)
      temp_4[k++] = base64_chars.find(encoded[j]);
    for (; k < 4; k++) temp_4[k++] = '\0';
    temp_a_3[0] = (temp_4[0] << 2) + ((temp_4[1] & MASK7) >> 4);
    temp_a_3[1] = ((temp_4[1] & MASK4) << 4) + ((temp_4[2] & MASK8) >> 2);
    temp_a_3[2] = ((temp_4[2] & MASK2) << 6) + temp_4[3];
    for (j = i, k = 0; k < 3 && encoded[j + 1] != EXTRA; j++, k++)
      decoded.push_back(temp_a_3[k]);
  }
  return decoded;
}
int main() {
  vector<UC> myData = {'6', '7', '8', '9'};
  string encoded = encode_base64(&myData[0], myData.size());
  cout << "Encoded String: " << encoded << '\n';
  vector<UC> decoded = decode_base64(encoded);
  cout << "Decoded Data: ";
  for (int i = 0; i < decoded.size(); i++) cout << (char)decoded[i] << ' ';
  cout << '\n';
  myData = {4, 16, 64};
  encoded = encode_base64(&myData[0], myData.size());
  cout << "Encoded String: " << encoded << '\n';
  decoded = decode_base64(encoded);
  cout << "Decoded Data: ";
  for (int i = 0; i < decoded.size(); i++) cout << (int)decoded[i] << ' ';
  cout << '\n';
  return 0;
}

主に、2つのデータセットがあります。 最初のセットには数字があります。 次のセットには数値があります。 したがって、最後のループでは、型を整数にキャストしてデコードされたメッセージを出力します。

出力:

Encoded String: Njc4OQ^^
Decoded Data: 6 7 8 9
Encoded String: BBBA
Decoded Data: 4 16 64

最初のセットは 4 文字 (4 バイト) で、エンコードされたメッセージ Njc4OQ^^ は 6 文字です (最後の 2 文字は余分です)。 2 番目のセットには 3 バイトがあり、エンコードされたメッセージ BBBA には 4 バイトがあります。

base_64 エンコーディングでは、すべての文字に 1 に設定された最大 6 ビットがあり、対応する 64 個の base_64 プライマリ文字があります。 同様に、エンコードされたメッセージには、ASCII よりも 33% 多くのストレージが必要です。

追加のストレージにもかかわらず、利点は特殊文字を管理できることです。 したがって、このエンコード スキームはデータを転送し、完全性を維持します。