Edited at
JavaDay 18

暗号処理をやってみよう!

More than 1 year has passed since last update.

この記事は、Java Advent Calendar 2016 の18日目の記事です。

前日は、fullmetal248さんの「JARファイルの難デコンパイル化について頑張ってみた話」 でした。

明日は、yumix_hさんの「誕生日枠!」 です。


暗号処理


暗号化とは

暗号化は、もとのデータ(文字列など)を、一見してなんだかわからないものに変換する処理です。解読キーを使うと、もとの文字列に復元することができ、これを復号化といいます。

Java言語は、暗号処理のライブラリをjavax.cryptoパッケージに持っているので、いろいろな暗号化方式を使って、暗号処理が可能です。


暗号処理

以下では、暗号処理の中でも使われることの多いAES暗号について解説します。また、AES暗号をプログラムの中で使うためのライブラリクラスを作成したので、ソースコードとその使い方も解説します。

では最初に、AES暗号に関する基本的な知識から。


AES暗号とは

1990年代後半、それまでアメリカ政府が公用に使っていたDES暗号が、そこらのパソコン(たしか30台ほどを使って)で破られてしまうことが明らかになりました。そこで世界コンペをやって新しい暗号方式を公募することになりました。日本からも楕円暗号などのエントリーがあったのですが、最終的にAES暗号が採用されました。


CBCモード

AES暗号は、全体を小さなブロックに切って、少しずつ暗号処理をします。その際、直前に暗号化したブロックと、次に暗号化するブロックのXORを取り、それを暗号化するという手順を行うのがCBCモードです。直前のブロックの暗号が、次のブロックの暗号化に影響するので、特定の文字列がどのような暗号に符号化されるか、平文と暗号を見比べても、推測できないという、優れた特徴があります。


IV(Initial Vector)

CBCモードでは、先頭のブロックにはXORで重ねるデータブロックがありません。そこで、先頭ブロック用には、人為的に値を作りますが、これがIVです。IVは、Javaの標準クラスを使う場合、128ビットの文字列です。


ランダムな文字列でIVを作成

暗号処理時に、毎回異なるIVを与えると、平文は同じでも毎回違う暗号ができます。そこでIVはランダムな文字列を使います。コモンズのライブラリ(commons-lang3-3.4-bin.zip)にある、RandomStringUtilsを使うと、ランダムな文字列を、長さや使う文字の種類などを指定して簡単に生成できます。


暗号解読キー

暗号化、復号化の両方で使用するキー文字列です。これも128ビット、つまり、16文字ちょうどでなければいけません。内容は自由に作成できますが、解読キーが長すぎれば切り詰め、短すぎれば文字列を補うなどの処理を、システム側でする必要があります。


暗号処理を行うCryptoクラスのソースコード

 次が暗号化、復号化を行うライブラリクラスです。暗号処理を行うには、Cryptクラスのインスタンスを作成して、encryptoメソッド(暗号化)、または、decrypto(復号化)メソッドを使うだけです。ただ、使う際に、キーやIVの保存が必要になるので、具体的な使い方のサンプルもこの後に掲載します。

最初に、コモンズのクラスが必要なので、そのためのpom.xmlファイルです。pomの意味や使い方は、13日の投稿に書いておきましたので、そちらを見てください。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>crypto</groupId>
<artifactId>crypto</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

次が暗号処理のCryptoクラスです。

プロジェクトは、Eclipseで、普通のJavaプロジェクトを作成した後、プロジェクトを右クリックして[構成]⇒[mavenプロジェクトに変換]を選択して作成しています。

package com.tkxwebs.crypto;

import java.io.Serializable;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* AES暗号/CBCモードによる暗号処理クラス
* @author 川場隆 kawaba@tk-webs.com
*/

public class Crypto implements Serializable {
private Cipher encrypter; // 暗号化用の暗号器
private Cipher decrypter; // 復号化用の暗号器

/* コンストラクタ(引数は暗号解読キーとIV)
* 引数は、どちらもStringからbyte[]に変換したものを指定する
*/

public Crypto(byte[] secretKey, byte[] ivs) throws Exception{
IvParameterSpec iv = new IvParameterSpec(ivs); // 暗号化時のスタートブロック用の初期値を作成

SecretKeySpec key = new SecretKeySpec(secretKey, "AES"); // 暗号方式+解読キーのセットを作成
encrypter = Cipher.getInstance("AES/CBC/PKCS5Padding"); // 暗号方式と生成方式などを指定して暗号器を作成
encrypter.init(Cipher.ENCRYPT_MODE, key, iv); // 暗号器を暗号化モードにセットする

decrypter = Cipher.getInstance("AES/CBC/PKCS5Padding"); // もうひとつ、暗号器を作成しておく
decrypter.init(Cipher.DECRYPT_MODE, key, iv); // 暗号器を復号モードにセットする
}

/* 暗号化処理を実行するメソッド */
public String encrypto(String text) throws Exception {
byte[] crypto = encrypter.doFinal(text.getBytes()); // 暗号化する
byte[] str64 = Base64.getEncoder().encode(crypto); // 表示できるように文字の配列に変換する
return new String(str64); // さらに文字列にしておく
}
/* 復号化処理を実行するメソッド */
public String decrypto(String str64) throws Exception {
byte[] str = Base64.getDecoder().decode(str64); // 暗号文字列を元のバイナリに戻す
byte[] text = decrypter.doFinal(str); // 復号化する
return new String(text); // 文字列に変換して返す
}
}

なお、次のFileUtilクラスは、暗号キーやIVを保存しておくために使うユーティリティです。

package com.tkxwebs.crypto;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class FileUtil {
public static void writeBytes(byte[] b, String path) {
try(ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(path))) {
o.write(b);
} catch (Exception e) {
e.printStackTrace();
}
}

public static byte[] readBytes(String path) {
byte[] b = new byte[16];
try(ObjectInputStream in = new ObjectInputStream(new FileInputStream(path))) {
in.read(b, 0, 16);
} catch (Exception e) {
e.printStackTrace();
}
return b;
}
}


Cryptoクラスの使い方

 では、実際の使い方を見てみましょう。

 暗号を使うのはどういう時でしょう。パスワードなどを暗号化しますね。そして、それを復号化するのは、例えばログイン処理などで照合する時です。明らかに、暗号化と復号化は、実行のタイミングが違うわけです。

ですから、暗号解読キーとIVは、システム内でファイルやデータベースに(さらに暗号化して)記録して保管しておかねばなりません。ここでは、そのさらなる暗号化の処理は省略しますが、いづれにせよ、暗号化、復号化のタイミングで、暗号解読キーと復号キーを書きだしたり、読みだしたりしなければなりません。

この使用例では、読み出しと書きこみには、byte配列をそのままオブジェクトとしてファイルに保管するユーティリティ(FileUtilクラス)を使っています。


使用例のコード

package sample;

import org.apache.commons.lang3.RandomStringUtils;
import com.tkxwebs.crypto.Crypto;
import com.tkxwebs.crypto.FileUtil;

public class CryptoExample {
public static void main(String args[]) {
/*******************************************************************************
* 暗号化と復号化は、別々に行われるのが普通であるので、暗号解読キーと、IVは、ファイル
* に保存しておき、実行時に読み込んで利用する
*******************************************************************************/

/*------------------------------------------------------------------------------
データの準備
--------------------------------------------------------------------------------*/

byte[] iv = RandomStringUtils.randomAlphanumeric(16).getBytes();// IV(暗号化時のスタートブロック用の初期値 128ビット固定長)
byte[] key = "kawaba_2015_key_".getBytes(); // 暗号解読キー(128ビット固定長)

/*------------------------------------------------------------------------------
IVと暗号解読キーはファイルに保存しておく
--------------------------------------------------------------------------------*/

FileUtil.writeBytes(iv, "iv");
FileUtil.writeBytes(key, "secret");

/*------------------------------------------------------------------------------
データの表示
--------------------------------------------------------------------------------*/

System.out.println("IV(スタートブロック用の初期値)="+new String(iv)+ "("+iv.length + "byte)");
System.out.println("暗号解読キー=" + new String(key) + "(16byte)");

/*------------------------------------------------------------------------------
暗号化の処理
--------------------------------------------------------------------------------*/

String source = "なんたらかんたら"; // 暗号化する文字列
String result = ""; // 暗号化した結果の文字列
try { // Cryptoオブジェクトを生成する
Crypto c = new Crypto(FileUtil.readBytes("secret"), FileUtil.readBytes("iv")); // 解読キーとIVをファイルから読み込む
result = c.encrypto(source); // 暗号化した文字列を得る
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("暗号=" + result); // 暗号文字列を表示する

/*------------------------------------------------------------------------------
復号化の処理
--------------------------------------------------------------------------------*/

try { // Cryptoオブジェクトを生成する
Crypto c2 =new Crypto(FileUtil.readBytes("secret"), FileUtil.readBytes("iv")); // 解読キーとIVをファイルから読み込む
result = c2.decrypto(result); // 復号化した文字列を得る
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("復号=" + result); // 元の文字列を表示する
}
}

このコードを実行すると、次のように表示されます。

何度か実行してみて、同じ文字列でも、毎回、違う暗号に変換されることを確認してください。

IV(スタートブロック用の初期値)=u7aTAfOMMKJvD02S(16byte)

暗号解読キー=kawaba_2015_key_(16byte)
暗号=piiKP/GX3HcykHbXAHjptPDtEORdx6oASa2luFtAbZA=
復号=なんたらかんたら