Java で RSA 暗号を使う

まず OpenSSL コマンドで RSA 秘密鍵を生成する。

$ openssl genrsa -out private_key.pem 1024

Generating RSA private key, 1024 bit long modulus
........................++++++
...++++++
e is 65537 (0x10001)

末尾にビット数を指定している。OpenSSL のコマンド側だとたとえば 256 とかでも通るんだけど,これを Java のほうにもってくと

java.security.InvalidKeyException: RSA keys must be at least 512 bits long

のように怒られたりする。ただし Sun JRE 付属のプロバイダ (JCE) だとそうなだけで Bouncy Castle をプロバイダに使うと怒られなかった。

このあと openssl rsa コマンドで DER 形式にすればいいだけかなと思ったらのちのち怒られたので PKCS #8 形式かつ DER 形式に変換する。

openssl pkcs8 -in private_key.pem -topk8 -nocrypt -outform DER -out private_key.pk8

公開鍵はふつうに DER 形式で出力すればいいだけ。

openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der
writing RSA key


以上のようにして OpenSSL で生成した鍵を使って Java で暗号化・復号化してみる。

なお,本来公開鍵暗号方式では,平文を公開鍵 (受信者側が公開した鍵) で暗号化し,暗号文を秘密鍵 (受信者側が秘匿している鍵) で復号化する。だが,今回の案件では秘密鍵 (送信者側が秘匿している鍵) で暗号化し,公開鍵 (送信者側が公開した鍵) で復号をおこなった。つまり RSA による暗号化というより半ば署名的に使ったことになる。通信内容の秘匿には使えないが,

  • 第3者に暗号文を生成させたくない
  • カジュアルに解読されなければいい

などの理由によりこのような仕様とした。

プログラムは以下のとおり。

package com.example.rsajava;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.Cipher;

import org.apache.commons.codec.binary.Base64;

public class Main {
	private static final String CIPHER_ALGORITHM = "RSA";
	private static final String CIPHER_MODE = CIPHER_ALGORITHM + "/ECB/PKCS1PADDING";

	private static final String PRIVATE_KEY_FILE = "private_key.pk8";
	private static final String PUBLIC_KEY_FILE  = "public_key.der";

	public static void main(String[] args) {
		Main app = new Main();

		InputStreamReader input = new InputStreamReader(System.in);
		BufferedReader reader = new BufferedReader(input, 1);
		for (;;) {
			System.out.print("INPUT:     ");
			System.out.flush();

			String line;
			try {
				line = reader.readLine();
				if (line == null) break;
			} catch (IOException e) {
				e.printStackTrace();
				break;
			}

			byte[] encrypted = app.encryptWithPrivateKey(line.getBytes());
			System.out.print("ENCRYPTED: ");
			System.out.println(Base64.encodeBase64String(encrypted));

			byte[] decrypted = app.decryptWithPublicKey(encrypted);
			System.out.print("DECRYPTED: ");
			System.out.println(new String(decrypted));

			System.out.println();
		}
	}

	private Cipher cipher;
	private Key secret_key;
	private Key public_key;

	public Main() {
		try {
			cipher = Cipher.getInstance(CIPHER_MODE);


			KeyFactory keyfactory = KeyFactory.getInstance(CIPHER_ALGORITHM);
			KeySpec keyspec;

			keyspec = new PKCS8EncodedKeySpec(loadBinaryFile(PRIVATE_KEY_FILE));
			secret_key = keyfactory.generatePrivate(keyspec);

			keyspec = new X509EncodedKeySpec(loadBinaryFile(PUBLIC_KEY_FILE));
			public_key = keyfactory.generatePublic(keyspec);
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}

	public byte[] encryptWithPrivateKey(byte[] source) {
		try {
			cipher.init(Cipher.ENCRYPT_MODE, secret_key);
			return cipher.doFinal(source);
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}

	public byte[] decryptWithPublicKey(byte[] source) {
		try {
			cipher.init(Cipher.DECRYPT_MODE, public_key);
			return cipher.doFinal(source);
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}

	private static byte[] loadBinaryFile(String filename) {
		try {
			FileInputStream in = new FileInputStream(filename);
			byte[] data = new byte[in.available()];
			in.read(data);
			in.close();
			return data;
		} catch (IOException e) {
			e.printStackTrace();
		}

		return new byte[0];
	}
}

かなり RSA 暗号のコード - BiBoLoG からパクっている。Base64 形式の出力のため Apache Commons Codec を利用している。あと例外処理は適当すぎる。


以下実行例。

INPUT:     Hello, World!
ENCRYPTED: gswm4jxzVI7QDM+EFqYq+uzniO81FsGfZVYsGtsecD22TPw+AqB33QKE7WzP0+fKiIPcCjyz/eJL
Z6bm+mplpY5I7QZkIOD+rtW443YaXULU/DXTf0kb/pPMOSbyHMLF5hWZJbzJSq7iGOFuyWqxZ9DB
JvlpobhKF9DLZGJ3JJ0=

DECRYPTED: Hello, World!


INPUT:     Hello, World!
ENCRYPTED: gswm4jxzVI7QDM+EFqYq+uzniO81FsGfZVYsGtsecD22TPw+AqB33QKE7WzP0+fKiIPcCjyz/eJL
Z6bm+mplpY5I7QZkIOD+rtW443YaXULU/DXTf0kb/pPMOSbyHMLF5hWZJbzJSq7iGOFuyWqxZ9DB
JvlpobhKF9DLZGJ3JJ0=

DECRYPTED: Hello, World!


INPUT:     hello, World!
ENCRYPTED: NR7vRLEkRHLpUwFzmgS2lf8FwkpUiX0HGnv6rCgx9wOc2SFupXVog3+7AJmGZlq7RA24LrRKCjsS
awIwtiyPaPDVtT+MYgmb5DWcV9fivewygvw5Ct6qRmvjUMl26+uglYkQ0yF6g250rp8NOJa1k5l/
0NBx4e7ls5Q2C7drHjw=

DECRYPTED: hello, World!

(少なくとも Java での) RSA 暗号化には初期ベクタがないので,同じ文から同じ暗号文が生成されている (1 つめの例と 2 つめの例)。しかし 3 つめの例から少なくとも先頭の文字が違っていれば暗号文の全体 (というかブロック長) が変化するので,自分で初期化ベクタを用意してやればカジュアルに傍受している側をまどわせることができると思われる。

ほか注意点

  • ブロック長 (= 鍵の長さ) で自動的に分割して暗号化してくれるわけではないので自分で分割する必要がある
    • といっても一般に通常鍵の長さをこえるほどの情報はやりとりしないであろう。共通鍵暗号の鍵を最初にやりとりするだけであれば,RSA 鍵長内におさまるわけだし
  • (すくなくとも PKCS#1) パディングをおこなうと,パディングのための領域が必要になる。1024 bit の鍵の場合に暗号化できるブロック内平文は 1024 bit に満たない