前回は、SharedPreferencesに直接パスワードを保存することの危険性について紹介しました。
そのため、Android端末内にパスワードを保存する際には、暗号化することが必須になります。
一般のWebサービスなどでパスワードをDBに保存する際には、「ハッシュ化」を行います。
古くはMD5、最近ではSHA-2を主に使いますが、これらは非可逆変換のため、万が一ハッシュ値が漏洩してもパスワードを特定されることはありません。
しかし、今回の目的では、正規のアプリケーションはパスワードを復元出来る必要があるため(そうしないと、ユーザに変わってWebサービスにパスワードを投稿できない)、ハッシュ化ではなくて暗号化を行います。
暗号化アルゴリズムには、強度が高くメジャーなAESを使うことにしましょう。
なお、暗号化技術を独自実装した場合には、各国の輸出規制法を気にする必要がありますが、今回の用途では基本的に申請などは必要ないようです。
まず、暗号化のラッパーライブラリを用意します。
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
public class Encryptor {
private static final String LOG_TAG = "Encryptor";
/** 鍵長(byte単位) */
private static final int KEY_LENGTH_BYTES = 16;
/** 鍵長(bit単位) */
private static final int KEY_LENGTH_BITS = KEY_LENGTH_BYTES * 8;
/**
* ランダムキーを生成する
*/
public static Key generateKey() {
try {
KeyGenerator generator = KeyGenerator.getInstance("AES");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
generator.init(KEY_LENGTH_BITS, random);
return generator.generateKey();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* バイト配列を鍵に変換
*/
public static Key getKey(byte[] bytes) {
byte[] b = new byte[KEY_LENGTH_BYTES];
for (int i = 0; i < KEY_LENGTH_BYTES; i++) {
b[i] = i < bytes.length ? bytes[i] : 0; // シンプル:あいたところは0で埋める
}
return new SecretKeySpec(b, "AES");
}
public static byte[] encrypt(byte[] src, byte[] key) throws KeyException {
return encrypt(src, getKey(key));
}
public static byte[] decrypt(byte[] src, byte[] key) throws KeyException {
return decrypt(src, getKey(key));
}
public static byte[] encrypt(byte[] src, Key skey) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skey);
return cipher.doFinal(src);
}
public static byte[] decrypt(byte[] src, Key skey) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, skey);
return cipher.doFinal(src);
}
}
これで、以下のような手順で暗号化・復号が出来ます。
String plain = "This is password"; //これが平文
Key key = Encryptor.generateKey(); //ランダムでキーを生成
byte[] encrypted = Encryptor.encrypt(plain.getBytes(), key); //暗号化された
byte[] plain2 = Encryptor.decrypt(encrypted, key); //復号化した
String plain3 = new String(plain2); //文字列に戻した
assertEquals(plain, plain3);
以下がポイントです。
- 鍵は128ビットのbyte配列で指定
- データもbyte配列にしておく必要がある
AESはブロック暗号なので、16bitごとに暗号化されます。データが16の倍数では無い場合、パディングが必要です。
また、レインボー攻撃(パスワードを推測するのではなく、平文のパターンを色々暗号化して、同じ結果が見つかったらそれから平文を求める)への対策のため、本来はIVを設定するなどしてより強化すべきです。
今回は、一番シンプルな実装にしました。
このような暗号化の機能を使って、「ユーザから入力されたデータは、暗号化してSharedPreferenceに保存」「利用するときは、復号してから利用」すれば、SharedPreferenceを覗かれても、暗号化されているため容易に閲覧できません。
もちろん、暗号化と復号には同じ鍵が必要です。この鍵は、何らかの方法でアプリが固有に秘密に持っている必要があります。
簡単なのは、ソースコード内で定数として持っておくことです。
しかし、AndroidのAPKファイルは非常に簡単にリバースエンジニアリングできるので、もう少し工夫したいところです。
次回は、暗号化に使う鍵の保存方法について考えてみます。
※本記事、連続なのに公開がとんでもなく遅れて申し訳ありません。何件もコメントを頂いてしまい、お恥ずかしい限りです。