Tech Racho エンジニアの「?」を「!」に。
  • 開発

Androidで安全にパスワードを保存する(3)

前回は、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ファイルは非常に簡単にリバースエンジニアリングできるので、もう少し工夫したいところです。
次回は、暗号化に使う鍵の保存方法について考えてみます。

※本記事、連続なのに公開がとんでもなく遅れて申し訳ありません。何件もコメントを頂いてしまい、お恥ずかしい限りです。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。