2 ポイント 投稿者 GN⁺ 2023-10-24 | 1件のコメント | WhatsAppで共有
  • Base64エンコーディングは、バイナリデータをASCIIテキストに変換し、保存・転送の過程でデータが誤って解釈される可能性を減らす方式
  • 暗号化ではなく表現形式の変更なので、エンコードされたデータは元のテキストやファイルデータに簡単に戻せる
  • 64個の文字は6ビットで表現できるため、Base64の1文字は6ビットのデータを持ち、3バイト(24ビット)は4つのBase64文字に変換される
  • HTMLのData URLs、メールでのバイナリ転送、テキスト中心のネットワークやURLのように生のバイナリが問題を起こしうる環境で有用
  • Ruby、C#、PHP、JavaScript、ターミナルのbase64コマンドなど、さまざまな言語やツールがエンコード・デコード機能を提供している

Base64が変換するもの

  • Base64エンコーディングは、バイナリデータをテキスト、より具体的にはASCIIテキストへ変換する
  • 結果として使われるのは次の64文字のみ
    • A-Z
    • a-z
    • 0-9
    • +
    • /
  • この文字集合は、<>\nのような文字が古いコンピュータやプログラムで誤って解釈される状況を避けるための安全な文字集合として使われる
  • "Ruby on Rails"をBase64でエンコードするとUnVieSBvbiBSYWlscw==になる
  • Base64は暗号化ではない
    • エンコードされたデータは元のテキストへ簡単に戻せる
    • データを隠すのではなく、データの表現だけを変える

Base64を使う場面

  • Data URLsは、画像のようなファイルデータをHTMLの中に直接埋め込めるようにするもので、このときBase64エンコードされたテキストを使う
  • 例の形式はdata:[<mime type>][;charset=<charset>][;base64],<encoded data>
  • メールでは、サーバーが改行を変更することがある環境でもバイナリデータを安全に扱うために、Base64が使われてきた
  • HTMLソースに画像データを直接埋め込む場合は、<>のような文字がタグとして解釈されないよう、エンコードが必要になる
  • テキストまたはUS-ASCIIデータを扱うよう設計されたネットワークを通じて、バイナリデータを保存または転送するときにも使える
  • URLに入れにくい文字を含むデータを渡すときにもBase64が使われることがある
  • Base系のエンコーディングは、オブジェクトをテキストエディタで扱えるようにするため、さまざまなアプリケーションで利用されている

エンコーディングアルゴリズム

  • Base64エンコーディングは次の手順で進む
    • テキストをバイナリ表現に変換する
    • ビットを6ビットずつに分ける
    • 各6ビットのグループを0から63までの10進数に変換する
    • その数値をBase64アルファベットの対応する文字に置き換える
  • 最後のグループのビットが足りない場合は、=または==パディングとして付けることがある
  • 64個の文字を表現するには6ビットが必要
    • 2^6 = 64
    • Base64の1つの値は6ビットのデータを表す
  • バイトは8ビットで、8と6の最小公倍数は24
    • 24ビットは3バイト
    • 24ビットは4つの6ビットBase64値で表現される

「Akshay」のエンコード例

  • "Akshay"は、各文字をASCII数値に変換してからバイナリへ変換すると次のようになる
    • 01000001 01101011 01110011 01101000 01100001 01111001
  • これを6ビットのグループに分けると次のようになる
    • 010000 010110 101101 110011 011010 000110 000101 111001
  • 各グループを10進数に変換すると次の値になる
    • 16 22 45 51 26 6 5 57
  • Base64アルファベットに変換すると次の文字になる
    • Q W t z a G F 5
  • したがって"Akshay"のBase64表現はQWtzaGF5
  • 同じ方法で、画像、PDF、テキスト、動画のようなファイルもバイナリに変換してからBase64でエンコードすれば、ASCIIテキストとして保存または転送できる

言語やツールでの利用

  • RubyはBase64モジュールでエンコードとデコードを処理する
    • Base64.encode64("Ruby on Rails")
    • Base64.decode64(encoded)
  • C#は文字列をバイト配列に変換したあとConvert.ToBase64Stringでエンコードし、System.Convert.FromBase64Stringでデコードする
  • PHPはトップレベル関数のbase64_encodebase64_decodeを提供している
  • JavaScriptはbtoa()でエンコードし、atob()でデコードする
  • ターミナルでもbase64コマンドでエンコードとデコードを実行できる
    • echo "akshay" | base64YWtzaGF5Cg==を出力する
    • echo "YWtzaGF5Cg==" | base64 -dakshayを出力する

1件のコメント

 
GN⁺ 2023-10-24
Hacker Newsのコメント
  • ここでテキストを暗号化しているわけではないと強調してくれてありがとう。多くのジュニア開発者は、暗号化は秘密の値がなければ元に戻せず、ハッシュ化は元に戻せず、エンコーディングは常に簡単に元に戻せる、という違いをかなり後になってから学んで痛い目を見ることが多い
    出力がランダムに見えても、エントロピーは入力と同じだという点も知っておく価値がある。つまり、パスワードを強くしようとして Base64 でエンコードしてはいけない

    • 揚げ足取りに近くて本筋とはあまり関係ないが、パスワードを Base64 でエンコードすると強くなる場合はある。パスワード強度はエントロピーだけの問題ではなく、高いエントロピーが最も効果的な方法であるというだけ
      パスワードが完全にランダム生成されたものなら、Base64 エンコードには何の効果もない。だが、辞書語や覚えやすい規則のような低エントロピーの体系で作ったパスワードなら、攻撃者は賢いパスワードクラッカーに Base64 エンコード規則 まで考慮させる必要があるので、試行ごとに計算が 1 つ増える程度の強度は追加される
      もちろん、そんなパスワード体系は使うべきではない。correct horse battery staple 系のパスワードで十分だと思う
    • 情報系の学位を持っていれば、こうした違いはみんな知っているはずだと思う。コーディングに興味がある人でも、半日あれば学べる概念だ
    • 関連していつも強調すべき点は、ハッシュが必ずしも暗号学的に安全とは限らない ということ
      ハッシュにはセキュリティ以外にも多くの目的があり、そのためハッシュライブラリも多様だ。セキュリティや暗号目的でハッシュを使うなら、その用途向けに設計されたハッシュを使うべき。CRC ハッシュは高速ではあるが、ユーザーパスワードに使うには向いていない
  • Base64 の面白い点の 1 つは、ある文字列から始めてエンコードを繰り返すと、結果の先頭部分がだんだん 不動点 に収束すること。Bash でも確認できる
    10年以上前に偶然見つけて暗号めいたツイートをしたのだが [1]、誰かがその話題でブログ記事を書き、ここにも投稿したものの、あまり議論はなかった [2]。別の人が Reddit /r/compsci に投稿したところ、そちらではブログ記事を正す生産的な議論があった [3]。今ではブログは消えているが、Internet Archive にコピーが残っている [4]
    [1] https://twitter.com/p4bl0/status/298900842076045312
    [2] https://news.ycombinator.com/item?id=5181256
    [3] https://www.reddit.com/r/compsci/comments/18234a/the_base64_...
    [4] https://web.archive.org/web/20130315082932/http://fmota.eu/b...

  • Bash でエンコードするときは -n オプションを使うべき: $ echo -n "abcde" |base64
    -n がないと echo が文字列の末尾に 改行文字 を 1 つ追加し、その文字までエンコードされる

  • base64URL というものもあり、URL に安全な別の ASCII 文字を使ってエンコードする。開発者の中には BASE64URL を単に base64 と呼ぶ人もいて、知らない人には問題になることがある
    https://datatracker.ietf.org/doc/html/rfc4648#section-5

    • base64url の問題は、~. が文字ではないため、エンコードされた値をダブルクリックしても全体を選択できないこと。コピー&ペーストしたい多くの場面で不要な摩擦になる
      Base62 エンコーディング(0-9A-Za-z)は base64url とほぼ同程度に効率的で、URL 安全性も保ちながら、コピー&ペーストがより簡単。人が読むときの曖昧さを減らしたいなら Base58 まで下げられるが、普通は BaseXX エンコーディングをするほどなら長さも長く、コピー&ペーストが一般的なので大きな問題ではない
      https://en.wikipedia.org/wiki/Base62
    • Base64url は通常パディングも省略する
      パディング付きの Base64 文字列は常に長さが 4 の倍数なので、4 の倍数でない文字列を受け取れば、元のパディングがどれだけ必要だったか分かり、最後の 3 バイトをどうデコードするかも判断できる
      なので、そもそも Base64 に == パディングがなぜ必要なのか少し混乱する
  • 基数変換の話になるたびに、図々しく任意基数変換器を宣伝している: https://convert.zamicol.com
    「useful alphabets」の下にある base64 は、基数で反復除算する「自然な」基数で、RFC の「バケット」変換方式は extras の下にある

  • 何かをエンコードして人が直接タイプしなければならないなら、https://en.wikipedia.org/wiki/Base32 を勧める
    悪いフォントのせいで l なのか 1 なのか、o なのか O なのか 0 なのか分からないほどイライラすることはない

  • 少し厳密に言うなら、Base64 はバイナリデータを ASCII 文字全体ではなく、ASCII の部分集合 にエンコードすると言うほうがより正確
    ASCII には 128 個のコードポイントがあり、そのうち表示可能文字が 95 個、制御文字が 33 個あるが、Base64 が使うのはそのうち 64 個、パディングを含めても 65 個だけ

  • 記事では = / == パディング の目的が詳しく扱われておらず、6ビットのグループにちょうど分割できないデータをどう処理するかも例で示されていなかった。
    だいたい理解はできた気がするが、確実に知りたい。= はいつ使い、== はいつ使うのか、常に追加するのか、それとも付けない場合もあるのか、"5byte" のような文字列の余ったビットは正確にどう処理するのか、デコード時に考慮すべき点があるのか、短く完全に答えてもらえるとありがたい。

    • 2つの質問は互いに関係している。
      Base64 の1文字は 6ビットを表すので、データの 3バイトブロック1つは Base64 エンコード文字 4個のブロックに対応する。したがって Base64 データは 4文字単位で処理すると都合がよい。
      = は、エンコード後の文字列長を 4 の倍数に合わせるために必要に応じて 0個、1個、2個付けるパディングである。たとえば "543210""543210==""6543210""6543210=""76543210" はパディング不要である。= が 3個必要になることはない。データ 1バイトでも最小で Base64 文字 2個が必要だからである。
      余ったビットは 0 で埋めればよく、デコーダは完全な 1バイトを作るのに十分なビットがないことを見て捨てられる。現代の多くのケースでは、パディングは必須というより慣習に近い。Wikipedia の記事がかなり詳しい: https://en.wikipedia.org/wiki/Base64
    • パディングは、エンコードされたデータを連結したりストリーミングしたりするとき、つまりエンコードストリームの途中にパディング文字が入る場合にだけ必要である。
      ストリーム・ファイル・文字列の末尾にあるパディング文字は、すでに処理した長さから推測できるので、厳密には必須ではない。
      ただしパディングの扱いはかなり微妙で、その違いから興味深い実装のバリエーションが生まれている: https://eprint.iacr.org/2022/361.pdf
    • 記事によれば Base64 の各文字はデータ 6ビットを表す。バイトは 8ビットで、8 と 6 の最小公倍数は 24 なので、24ビット、つまり 3バイトを 6ビットの Base64 文字 4個で表現できる。
      要するに 24ビット単位 でエンコードしているわけである。データが終わったら、余った 24ビット部分は A ではなく = で埋める。A はデータとして 000000 を意味するからである。自分も理解するために全文を2回読んだ。
  • 自分の Base64 エンコーダシェーダ はここにある: https://github.com/Rezmason/excel_97_egg/blob/main/glsl/base...
    GLSL で約13行まで縮めた: https://github.com/Rezmason/excel_97_egg/blob/main/glsl/base...
    サイドプロジェクトの Cursed Mode で使っていて、WebGL フレームバッファを Base64 でエンコードされた 640x480 のインデックスカラー BMP として 1秒あたり約15回レンダリングしている: https://rezmason.github.io/excel_97_egg/?cursed=1

  • 深く掘り下げ始めると、さらに興味深い細部があり、その細部のバリエーションも驚くほど多い。
    入力データ長がちょうど 3バイトの倍数でない場合、最後の 1バイトまたは 2バイトをエンコードするのに Base64 文字 2個または 3個を使う。Base64 文字1個は 6ビットなので、8ビットや16ビットを表すために 12ビットや18ビットを使うことになり、結果として何もエンコードしない余分な 4ビットまたは 2ビットが生じる。
    RFC ではエンコーダがそのビットを 0 に設定しなければならないと要求しているが、デコーダについては、そのビットが 0 でない入力を拒否してもよいとされているだけである。実際には、デフォルトで拒否する実装はほとんどなく、知る限り Ruby、Rust、Go くらいしか、そのような入力で失敗するよう設定できない。Python には validate オプションがあるが、そのビットまでは検証しない。
    もう1つの大きな違いは、空白と Base64 以外の文字の扱いである。Python を含め、意外なほど多くの実装が入力中の任意の文字を黙って無視する。アルファベットを誤って選ぶと問題になりうる。たとえば Python では base64.standard_b64decode(base64.urlsafe_b64encode(b'\xFF\xFE\xFD\xFC')) はエラーを出さず、黙って誤った出力を返す。
    Ruby の Base64 エンコーダが 60文字ごとに改行を入れるのも面白い。PEM 以外にはそんな短い行を要求する標準エンコーディングはなく、しかも PEM は厳密に 64文字行を要求するので、かなり変わった選択である。
    プログラミング言語と一部の JavaScript ライブラリの差異をまとめた記事を書いており [1]、よりよい Base64 を JS に追加しようとする作業もしている [2]。
    [1] https://gist.github.com/bakkot/16cae276209da91b652c2cb3f612a...
    [2] https://github.com/tc39/proposal-arraybuffer-base64