できない.dev

文字列をBase64にするには

文字列を UTF-8 のバイト列にしてから Base64 に変換し、元の文字列へ戻す方法を各言語で示す。
+ と / を - と _ に置き換える URL セーフ版と、末尾のパディングの有無までを扱う。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

import base64
 
text = "こんにちは!"
encoded = base64.b64encode(text.encode("utf-8")).decode("ascii")
print(encoded)                                   # 44GT44KT44Gr44Gh44GvIQ==
print(base64.b64decode(encoded).decode("utf-8")) # こんにちは!
 
data = bytes([0xFB, 0xFF])
print(base64.b64encode(data).decode("ascii"))          # +/8=
print(base64.urlsafe_b64encode(data).decode("ascii"))  # -_8=(+ と / を - と _ に置き換える)

b64encode はバイト列を受け取る関数なので、文字列は先に encode("utf-8") でバイト列にする。
結果もバイト列で返るため、文字列として使うなら decode("ascii") で戻す。
+ と / を - と _ に置き換えた URL セーフ版には urlsafe_b64encode を使う。

JavaScript

const text = "こんにちは!";
const encoded = Buffer.from(text, "utf8").toString("base64");
console.log(encoded);                                      // 44GT44KT44Gr44Gh44GvIQ==
console.log(Buffer.from(encoded, "base64").toString("utf8")); // こんにちは!
 
const data = Buffer.from([0xfb, 0xff]);
console.log(data.toString("base64"));    // +/8=
console.log(data.toString("base64url")); // -_8(パディングの = は付かない)
 
try {
  btoa(text); // btoa は Latin-1 の範囲外の文字を受け付けない
} catch (e) {
  console.log(e.name); // InvalidCharacterError
}

Node.js では Buffer を介して変換し、文字列は Buffer.from(text, "utf8") でバイト列にしてから toString("base64") を呼ぶ。
URL セーフ版は "base64url" を指定でき、こちらは末尾の = が付かない。
ブラウザ由来の btoa は Latin-1 の範囲の文字しか受け付けず、日本語を渡すと InvalidCharacterError になる。

TypeScript

// ブラウザでも Node.js でも動く書き方(Buffer を使わない)
function toBase64(text: string): string {
  const bytes = new TextEncoder().encode(text);
  let binary = "";
  for (const b of bytes) binary += String.fromCharCode(b);
  return btoa(binary);
}
 
function fromBase64(encoded: string): string {
  const binary = atob(encoded);
  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}
 
const encoded: string = toBase64("こんにちは!");
console.log(encoded);               // 44GT44KT44Gr44Gh44GvIQ==
console.log(fromBase64(encoded));   // こんにちは!

TextEncoder でバイト列にしてから btoa に渡す書き方なら、Buffer が無いブラウザでも Node.js でも同じコードで動く。
btoa は 1 文字が 1 バイトの文字列を要求するため、バイトを String.fromCharCode で 1 文字ずつ文字列に直してから渡す。
戻す側は atob の結果をバイト列に直し、TextDecoder で UTF-8 として読む。

Go

package main
 
import (
	"encoding/base64"
	"fmt"
)
 
func main() {
	text := "こんにちは!"
	encoded := base64.StdEncoding.EncodeToString([]byte(text))
	fmt.Println(encoded) // 44GT44KT44Gr44Gh44GvIQ==
 
	decoded, err := base64.StdEncoding.DecodeString(encoded)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(decoded)) // こんにちは!
 
	data := []byte{0xfb, 0xff}
	fmt.Println(base64.StdEncoding.EncodeToString(data))    // +/8=
	fmt.Println(base64.URLEncoding.EncodeToString(data))    // -_8=
	fmt.Println(base64.RawURLEncoding.EncodeToString(data)) // -_8(パディングなし)
}

encoding/base64 の StdEncoding が標準の変換で、URL セーフ版は URLEncoding、さらにパディングを付けない版は RawURLEncoding である。
デコードの DecodeString はエラーを返すので、外部から受け取った文字列を戻す場面では err を必ず確認する。

Rust

// Cargo.toml: base64 = "0.22"
use base64::{engine::general_purpose, Engine as _};
 
fn main() {
    let text = "こんにちは!";
    let encoded = general_purpose::STANDARD.encode(text.as_bytes());
    println!("{}", encoded); // 44GT44KT44Gr44Gh44GvIQ==
 
    let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
    println!("{}", String::from_utf8(decoded).unwrap()); // こんにちは!
 
    let data = [0xfbu8, 0xff];
    println!("{}", general_purpose::STANDARD.encode(data)); // +/8=
    println!("{}", general_purpose::URL_SAFE.encode(data)); // -_8=
    println!("{}", general_purpose::URL_SAFE_NO_PAD.encode(data)); // -_8
}

標準ライブラリに Base64 の関数は無いため、base64 クレートを Cargo.toml に追加して使う。
0.22 系では用途ごとの engine(STANDARD・URL_SAFE・URL_SAFE_NO_PAD など)を選び、encode と decode を呼ぶ。
Engine トレイトを use しないと、encode メソッドが見つからないというコンパイルエラー(E0599)になる。

Java

import java.nio.charset.StandardCharsets;
import java.util.Base64;
 
public class Main {
    public static void main(String[] args) {
        String text = "こんにちは!";
        String encoded = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8));
        System.out.println(encoded); // 44GT44KT44Gr44Gh44GvIQ==
 
        byte[] decoded = Base64.getDecoder().decode(encoded);
        System.out.println(new String(decoded, StandardCharsets.UTF_8)); // こんにちは!
 
        byte[] data = {(byte) 0xfb, (byte) 0xff};
        System.out.println(Base64.getEncoder().encodeToString(data)); // +/8=
        System.out.println(Base64.getUrlEncoder().encodeToString(data)); // -_8=
        System.out.println(Base64.getUrlEncoder().withoutPadding().encodeToString(data)); // -_8
    }
}

java.util.Base64 が標準で入っており、getEncoder が標準、getUrlEncoder が URL セーフ版で、withoutPadding を付けるとパディングが消える。
文字列をバイト列にするときは StandardCharsets.UTF_8 を明示し、実行環境の既定の文字コードに結果が左右されないようにする。

C#

using System;
using System.Text;
 
var text = "こんにちは!";
var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(text));
Console.WriteLine(encoded); // 44GT44KT44Gr44Gh44GvIQ==
 
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
Console.WriteLine(decoded); // こんにちは!
 
var data = new byte[] { 0xfb, 0xff };
var standard = Convert.ToBase64String(data);
Console.WriteLine(standard); // +/8=
 
// URL セーフ版の専用メソッドは標準ライブラリに無いので、記号を置き換える
var urlSafe = standard.Replace('+', '-').Replace('/', '_').TrimEnd('=');
Console.WriteLine(urlSafe); // -_8

Convert.ToBase64String はバイト配列を受け取るので、文字列は先に Encoding.UTF8.GetBytes で変換する。
URL セーフ版の専用の型(System.Buffers.Text.Base64Url)は .NET 8 では使えずコンパイルエラーになるため、+ と / を置き換え、末尾の = を TrimEnd で取り除いて作る。

つまずき

Base64 は文字列ではなくバイト列を変換する仕組みなので、文字列を渡す前にバイト列へ直す必要がある。
Python で文字列のまま b64encode に渡すと「a bytes-like object is required, not 'str'」という TypeError になる。
JavaScript の btoa に「こんにちは!」を渡すと InvalidCharacterError になる。
どちらも、先に UTF-8 のバイト列にしてから渡すと解決する。

文字コードで結果が変わる

同じ「こんにちは!」でも、どの文字コードのバイト列から作るかで Base64 の値は変わる。
UTF-8 から作ると 44GT44KT44Gr44Gh44GvIQ== になり、7 言語のコードは同じ値を返した。
Shift_JIS(cp932)のバイト列から作ると grGC8YLJgr+CzSE= になる。
受け取る側がバイト列を UTF-8 として読む前提なら、送る側も UTF-8 で作る。

URL セーフ版とパディング

標準の Base64 は + と / を使うので、URL やファイル名にそのまま載せると意味を持ってしまう。
URL セーフ版は + を -、/ を _ に置き換える。
バイト列 FB FF は、標準が「+/8=」、URL セーフが「-_8=」、パディングなしが「-_8」になった。

デコードで言語の差が出る。
Python の b64decode は、標準にない - と _ を既定で黙って読み飛ばすため、URL セーフ版の文字列を渡すと「Invalid base64-encoded string」のような分かりにくいエラーになる。
URL セーフ版を戻すときは urlsafe_b64decode を使う。
パディングを省いた文字列は Python では「Incorrect padding」で失敗するが、Node.js の Buffer.from は標準・URL セーフのどちらの文字も、パディングの有無も受け付けた。

暗号化ではない

Base64 は誰でも元に戻せる表記の変換であり、秘密を守る手段にはならない。
3 バイトを 4 文字で表すため、元より約 3 分の 1 長くなる。
「こんにちは!」は UTF-8 で 16 バイトだが、Base64 にすると 24 文字になった。

このコードで動いた?