Base64に関するあれこれ（Java）

JSONに画像データを埋め込んだりと、Base64エンコード／デコードする機会があったので、調べたことを備忘録として記録します。
# Base64とは
こちらの記事がわかりやすく解説していましたので引用します。
https://qiita.com/PlanetMeron/items/2905e2d0aa7fe46a36d4
Base64変換アルゴリズムまで丁寧にわかりやすく記載してあり参考になります。

簡単にいうと
・a-z(26文字)
・A-Z(26文字)
・0-9(10文字)
・+
・/
・= (データ長を揃えるための末尾へのパディング文字として利用)
の計65文字で表現したエンコード方式だそうです。※最後の「=」はパディング不要であれば64文字表現となる。

JSONで特殊文字が含まれないように、バイナリデータ等を64(or65)の文字列へ変換して安全にデータ送受信ができるようになったりします。
64(or65)種類の文字だけで表現することになるので、データ量が約1.3倍（133%）と増加するのも特徴です。

電子メールのMIME形式としてBase64エンコードする際は、MIMEの基準で76文字ごとに改行コード（CRLF）が入ります。
この2バイト分を含めるとデータ量は約137%となります。

Base64の形式を分類すると以下の3パターンです。
※サンプルで載せているデータは「わあいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもああ」のバイト列をBase64変換した値です。

#### 1. 基本的なBase64  
```
gu2CoIKigqSCpoKogqmCq4Ktgq+CsYKzgrWCt4K5gruCvYK/gsKCxILGgsiCyYLKgsuCzILNgtCC04LWgtmC3ILdgt6C34LggqCCoA==
```  
・文字数が4の倍数末尾2文字はパディング文字「=」
・+, / 記号を含む（※これをURLに使用するとパーセントエンコーディング対象となる）

#### 2. MIME基準の76文字ごとに改行コードを含んだBase64
```
gu2CoIKigqSCpoKogqmCq4Ktgq+CsYKzgrWCt4K5gruCvYK/gsKCxILGgsiCyYLKgsuCzILNgtCC
04LWgtmC3ILdgt6C34LggqCCoA==
```  
・文字数が4の倍数末尾2文字はパディング文字「=」
・+, / 記号を含む（※これをURLに使用するとパーセントエンコーディング対象となる）
 
#### 3. URL-SafeなBase64
```
gu2CoIKigqSCpoKogqmCq4Ktgq-CsYKzgrWCt4K5gruCvYK_gsKCxILGgsiCyYLKgsuCzILNgtCC04LWgtmC3ILdgt6C34LggqCCoA
```  
・URLアンセーフな記号が変換されている。「+」→「-」、「/」→「_」、「=」除去

※URL-Safe, UnSafeなBase64…
通常のBase64エンコードでは、アルファベット文字列と数字列に追加して+,/,=と3種類の記号文字で表現されます。
このとき、+,/,=はURLに含めることが出来ないためURLアンセーフとされています。（※通常はこれらの文字やマルチバイト文字は特殊文字としてパーセントエンコードされます。）
そのため、Base64エンコードと合わせて「+」「/」「=」を別の文字に変換する必要性が出てきます。

# 使用用途
- JSONなどで特殊文字が含まれないように画像データをbase64文字列にする。
- Basic認証ではユーザ名とパスワードをコロン（:）区切ってBase64エンコードした文字列を用いている。
- Webページ表示時のリクエスト数を減らすために、base64エンコードした画像をhtmlにそのまま埋め込む。
- 7ビットのデータしか扱うことの出来ない電子メールで広く利用されている。  

# Java7,8～での利用方法

## Java8～
Java8以降ではJDK標準に java.util.Base64 のユーティリティが用意されているのでこれを利用するのがよさそうです。
Java8以降は実機検証していません。
こちらが非常にシンプルでわかりやすいコードサンプルが載っていましたので参考になりました。
https://gist.github.com/komiya-atsushi/d878e6e4bf9ba6dae8fa

# Java7
Java7でBase64を扱うには・・・

- Apache Commons Codec を使う
- javax.mail（JavaMail の MimeUtility を利用する）を使う
- Seasar2のBase64Utilを使う
- 自作する

などがありそうです。
今回は手元にあった上2つ「Apache Commons Codec」と「javax.mail」を使ってBase64エンコード／デコードしたいと思います。
ここでの実装は、バイナリデータ(byte[])をBase64文字列(String)へエンコード／デコードするサンプルを実装します。

### Apache Commons Codec
バージョン：1.11で検証

#### 基本的なBase64エンコード
```java
public static String encodeBase64(byte[] data) {
	return Base64.encodeBase64String(data);
}
```
⇒ ```Base64#encodeBase64String``` を使っています。

#### MIME基準の76文字ごとに改行コードを含んだBase64エンコード
```java
public static String encodeBase64Chunked(byte[] data) {
	byte[] encoded = Base64.encodeBase64Chunked(data);
	return new String(encoded);
}
```
⇒ ```Base64#encodeBase64Chunked``` を使っています。
改行コード付与のインタフェースが用意されていました。

#### URL-SafeなBase64エンコード
```java
public static String encodeBase64URLSafe(byte[] data) {
	return Base64.encodeBase64URLSafeString(data);
}
```
⇒ ```Base64#encodeBase64URLSafeString``` を使っています。
URL-Safe専用のインタフェースが用意されていました。

#### デコード
```java
public static byte[] decodeBase64(String data) {
	return Base64.decodeBase64(data);
}
```
⇒ デコードは一律これでいけそうです。

### javax.mail
バージョン：1.4.6で検証

#### 基本的なBase64エンコード
```java
private static String trimCRLF(ByteArrayOutputStream encodedCRLF) {
	byte inputArray[] = encodedCRLF.toByteArray();
	byte outputArray[] = new byte[encodedCRLF.size()];

	// CR(0x0d)、LF(0x0a)箇所を飛ばして出力用配列にコピー
	int n = 0;
	for (int i = 0; i < encodedCRLF.size() - 1; i++) {
		if (inputArray[i] == 0x0d) {// CR
			if (inputArray[i + 1] == 0x0a) {// LF
				i++;
				continue;
			}
		}
		outputArray[n] = inputArray[i];
		n++;
	}
	return new String(outputArray, 0, n);
}

public static String encodeBase64(byte[] data) {
	try (ByteArrayOutputStream encodedChunked = new ByteArrayOutputStream()) {
		try (OutputStream os = MimeUtility.encode(encodedChunked, "base64")) {
			os.write(data);
			os.flush();
		}
		// 改行文字を除去する
		String encodedStr = trimCRLF(encodedChunked);
		return encodedStr;

	} catch (IOException e) {
		e.printStackTrace();
		return "Bad Encryption";
	} catch (MessagingException e) {
		e.printStackTrace();
		return "Bad Encryption";
	}
}

```
⇒ MimeUtilityではMimeUtility.encodeでエンコードができるようですが、
デフォルトで改行を含んだMIME基準の形式となりました。
ですので、改行なしとするために改行コード除去を行っています。

#### MIME基準の76文字ごとに改行コードを含んだBase64エンコード
```java
public static String encodeBase64Chunked(byte[] data) {
	try (ByteArrayOutputStream encodedChunked = new ByteArrayOutputStream()) {
		try (OutputStream os = MimeUtility.encode(encodedChunked, "base64")) {
			os.write(data);
			os.flush();
		}
		return encodedChunked.toString();

	} catch (IOException e) {
		e.printStackTrace();
		return "Bad Encryption";
	} catch (MessagingException e) {
		e.printStackTrace();
		return "Bad Encryption";
	}
}
```
⇒ MimeUtility.encodeを使っています。

#### URL-SafeなBase64エンコード
```java
private static String urlSafeEncode(ByteArrayOutputStream encodedCRLF) {
	byte inputArray[] = encodedCRLF.toByteArray();
	byte outputArray[] = new byte[encodedCRLF.size()];

	// CR(0x0d)、LF(0x0a)箇所を飛ばして出力用配列にコピー
	int n = 0;
	for (int i = 0; i < encodedCRLF.size() - 1; i++) {
		if (inputArray[i] == 0x0d) {// CR
			if (inputArray[i + 1] == 0x0a) {// LF
				i++;
				continue;
			}
		}

		// URL-Safeに変換
		if (inputArray[i] == 0x2b) {// 「+」
			outputArray[n] = 0x2d;// 「-」
		}
		else if (inputArray[i] == 0x2f) {// 「/」
			outputArray[n] = 0x5f;// 「_」
		}
		else if (inputArray[i] == 0x3d) {// 「=」
			continue;
		}
		else {
			outputArray[n] = inputArray[i];
		}
		n++;
	}
	return new String(outputArray, 0, n);
}

public static String encodeBase64URLSafe(byte[] data) {
	try (ByteArrayOutputStream encodedChunked = new ByteArrayOutputStream()) {
		try (OutputStream os = MimeUtility.encode(encodedChunked, "base64")) {
			os.write(data);
			os.flush();
		}
		// 改行文字を除去してURL-Safeにする
		String encodedURLSafe = urlSafeEncode(encodedChunked);
		return encodedURLSafe;

	} catch (IOException e) {
		e.printStackTrace();
		return "Bad Encryption";
	} catch (MessagingException e) {
		e.printStackTrace();
		return "Bad Encryption";
	}

```
⇒ URL-Safeにするために、改行コード除去に加えて記号「+」「/」「=」を変換しています。
すごく長いです。。

#### デコード
```java
public static byte[] decodeBase64(String data) {
	byte[] urlsafe = data.getBytes();
	int mod = urlsafe.length % 4;
	byte[] nosafe = new byte[urlsafe.length + mod];
	for (int i = 0; i < urlsafe.length; i++) {
		if (urlsafe[i] == 0x2d) {// 「-」
			nosafe[i] = 0x2b;// 「+」
		}
		else if (urlsafe[i] == 0x5f) {// 「_」
			nosafe[i] = 0x2f;// 「/」
		}
		else {
			nosafe[i] = urlsafe[i];
		}
	}
	// 固定長4の倍数に満たないものは = でパディング
	for (int i = urlsafe.length + mod - 1; i >= urlsafe.length; i--) {
		nosafe[i] = 0x3d;// 「=」
	}

	ByteArrayInputStream from = new ByteArrayInputStream(nosafe);
	try (InputStream is = MimeUtility.decode(from, "base64"); ByteArrayOutputStream to = new ByteArrayOutputStream()) {
		byte[] buf = new byte[8192];
		int readCnt;
		while ((readCnt = is.read(buf)) != -1) {
			to.write(buf, 0, readCnt);
		}
		to.flush();
		return to.toByteArray();

	} catch (MessagingException | IOException e) {
		e.printStackTrace();
		return "Bad Decryption".getBytes();
	}
}
```
⇒ 強引ですが。。
デコード処理ではURL-Safeに変換済みの文字列が入ることを想定してURL-UnSafeに逆変換しています。
桁数を4の倍数に合わせて「=」パディングを行った後に、MimeUtility.decodeを使ってデコード実行しています。
普通はこんなコード書かないと思います。

# まとめ
- Base64の中でも、「基本形式」「改行コード含むMIME基準の形式」「URL-Safeに変換した形式」の大きく3種類が存在する。
- Java8以降なら標準のjava.util.Base64クラスを使う。
- Java7ではApache Commons Codecが使いやすい。使えない場合はjavax.mailを使うとよい。

以上。