1. ホーム
  2. android

[解決済み] AndroidでAES暗号を使用する際のベストプラクティスは何ですか?

2023-03-30 18:42:15

質問

なぜこの質問をするのか。

私は、Android でも AES 暗号化について多くの質問があることを知っています。そして、Web を検索すれば、たくさんのコード スニペットがあります。しかし、すべてのページ、すべての Stack Overflow の質問で、大きな違いのある別の実装を見つけます。

そこで、私はベストプラクティスを見つけるためにこの質問を作成しました。最も重要な要件のリストを収集し、本当に安全な実装をセットアップすることができればと思います!

初期化ベクトルとソルトについて読みました。私が見つけたすべての実装がこれらの機能を備えていたわけではありません。では、それは必要なのでしょうか?セキュリティはかなり向上するのでしょうか?どのように実装するのですか?暗号化されたデータを復号できない場合、アルゴリズムは例外を発生させるべきですか?それとも、それは安全ではなく、単に読めない文字列を返すべきですか?アルゴリズムは、SHA の代わりに Bcrypt を使用できますか?

私が見つけたこれら2つの実装についてはどうですか?それらは大丈夫ですか?完璧ですか、それともいくつかの重要なことが欠けていますか?安全なのはどちらでしょうか?

アルゴリズムは、文字列と暗号化のための"password"を受け取り、そのパスワードで文字列を暗号化する必要があります。出力は再び文字列(hexまたはbase64?)であるべきです。もちろん復号化も可能であるべきです。

Androidのための完璧なAES実装は何ですか?

実装その1です。

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class AdvancedCrypto implements ICrypto {

        public static final String PROVIDER = "BC";
        public static final int SALT_LENGTH = 20;
        public static final int IV_LENGTH = 16;
        public static final int PBE_ITERATION_COUNT = 100;

        private static final String RANDOM_ALGORITHM = "SHA1PRNG";
        private static final String HASH_ALGORITHM = "SHA-512";
        private static final String PBE_ALGORITHM = "PBEWithSHA256And256BitAES-CBC-BC";
        private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
        private static final String SECRET_KEY_ALGORITHM = "AES";

        public String encrypt(SecretKey secret, String cleartext) throws CryptoException {
                try {

                        byte[] iv = generateIv();
                        String ivHex = HexEncoder.toHex(iv);
                        IvParameterSpec ivspec = new IvParameterSpec(iv);

                        Cipher encryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
                        encryptionCipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);
                        byte[] encryptedText = encryptionCipher.doFinal(cleartext.getBytes("UTF-8"));
                        String encryptedHex = HexEncoder.toHex(encryptedText);

                        return ivHex + encryptedHex;

                } catch (Exception e) {
                        throw new CryptoException("Unable to encrypt", e);
                }
        }

        public String decrypt(SecretKey secret, String encrypted) throws CryptoException {
                try {
                        Cipher decryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
                        String ivHex = encrypted.substring(0, IV_LENGTH * 2);
                        String encryptedHex = encrypted.substring(IV_LENGTH * 2);
                        IvParameterSpec ivspec = new IvParameterSpec(HexEncoder.toByte(ivHex));
                        decryptionCipher.init(Cipher.DECRYPT_MODE, secret, ivspec);
                        byte[] decryptedText = decryptionCipher.doFinal(HexEncoder.toByte(encryptedHex));
                        String decrypted = new String(decryptedText, "UTF-8");
                        return decrypted;
                } catch (Exception e) {
                        throw new CryptoException("Unable to decrypt", e);
                }
        }

        public SecretKey getSecretKey(String password, String salt) throws CryptoException {
                try {
                        PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), HexEncoder.toByte(salt), PBE_ITERATION_COUNT, 256);
                        SecretKeyFactory factory = SecretKeyFactory.getInstance(PBE_ALGORITHM, PROVIDER);
                        SecretKey tmp = factory.generateSecret(pbeKeySpec);
                        SecretKey secret = new SecretKeySpec(tmp.getEncoded(), SECRET_KEY_ALGORITHM);
                        return secret;
                } catch (Exception e) {
                        throw new CryptoException("Unable to get secret key", e);
                }
        }

        public String getHash(String password, String salt) throws CryptoException {
                try {
                        String input = password + salt;
                        MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM, PROVIDER);
                        byte[] out = md.digest(input.getBytes("UTF-8"));
                        return HexEncoder.toHex(out);
                } catch (Exception e) {
                        throw new CryptoException("Unable to get hash", e);
                }
        }

        public String generateSalt() throws CryptoException {
                try {
                        SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
                        byte[] salt = new byte[SALT_LENGTH];
                        random.nextBytes(salt);
                        String saltHex = HexEncoder.toHex(salt);
                        return saltHex;
                } catch (Exception e) {
                        throw new CryptoException("Unable to generate salt", e);
                }
        }

        private byte[] generateIv() throws NoSuchAlgorithmException, NoSuchProviderException {
                SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
                byte[] iv = new byte[IV_LENGTH];
                random.nextBytes(iv);
                return iv;
        }

}

出典 http://pocket-for-android.1047292.n5.nabble.com/Encryption-method-and-reading-the-Dropbox-backup-td4344194.html

実装その2です。

import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 * Usage:
 * <pre>
 * String crypto = SimpleCrypto.encrypt(masterpassword, cleartext)
 * ...
 * String cleartext = SimpleCrypto.decrypt(masterpassword, crypto)
 * </pre>
 * @author ferenc.hechler
 */
public class SimpleCrypto {

    public static String encrypt(String seed, String cleartext) throws Exception {
        byte[] rawKey = getRawKey(seed.getBytes());
        byte[] result = encrypt(rawKey, cleartext.getBytes());
        return toHex(result);
    }

    public static String decrypt(String seed, String encrypted) throws Exception {
        byte[] rawKey = getRawKey(seed.getBytes());
        byte[] enc = toByte(encrypted);
        byte[] result = decrypt(rawKey, enc);
        return new String(result);
    }

    private static byte[] getRawKey(byte[] seed) throws Exception {
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        sr.setSeed(seed);
        kgen.init(128, sr); // 192 and 256 bits may not be available
        SecretKey skey = kgen.generateKey();
        byte[] raw = skey.getEncoded();
        return raw;
    }


    private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
        byte[] encrypted = cipher.doFinal(clear);
        return encrypted;
    }

    private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);
        byte[] decrypted = cipher.doFinal(encrypted);
        return decrypted;
    }

    public static String toHex(String txt) {
        return toHex(txt.getBytes());
    }
    public static String fromHex(String hex) {
        return new String(toByte(hex));
    }

    public static byte[] toByte(String hexString) {
        int len = hexString.length()/2;
        byte[] result = new byte[len];
        for (int i = 0; i < len; i++)
            result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
        return result;
    }

    public static String toHex(byte[] buf) {
        if (buf == null)
            return "";
        StringBuffer result = new StringBuffer(2*buf.length);
        for (int i = 0; i < buf.length; i++) {
            appendHex(result, buf[i]);
        }
        return result.toString();
    }
    private final static String HEX = "0123456789ABCDEF";
    private static void appendHex(StringBuffer sb, byte b) {
        sb.append(HEX.charAt((b>>4)&0x0f)).append(HEX.charAt(b&0x0f));
    }

}

出典 http://www.tutorials-android.com/learn/How_to_encrypt_and_decrypt_strings.rhtml

どのように解決するのですか?

質問であげた実装はどちらも全く正しくなく、そのまま使うべきではないでしょう。 以下では、Androidにおけるパスワードベースの暗号化の側面について説明します。

キーとハッシュ

まず、ソルトを使ったパスワード方式について説明します。ソルトはランダムに生成される数字です。それは"deduced"ではありません。実装1では generateSalt() メソッドがあり、暗号学的に強力な乱数を生成します。ソルトはセキュリティ上重要なので、一度だけ生成すればよいのですが、一度生成したものは秘密にすべきです。これが Web サイトであれば、ソルトを秘密にするのは比較的簡単ですが、インストールされたアプリケーション (デスクトップおよびモバイル デバイス用) では、これははるかに難しいでしょう。

メソッドは getHash() は、与えられたパスワードとソルトを一つの文字列に連結したハッシュを返します。使用されるアルゴリズムはSHA-512で、512ビットのハッシュが返されます。このメソッドは文字列の整合性をチェックするのに便利なハッシュを返すので、同様に getHash() をパスワードだけ、あるいはソルトだけで呼び出すと、 両方のパラメータを単純に連結して使うことができます。このメソッドはパスワードベースの暗号化システムで使用されることはないので、これ以上説明することはありません。

メソッド getSecretKey() から、キーを派生させる。 char から返される、パスワードと 16 進数のソルトの配列から鍵を導きます。 generateSalt() . 使用されるアルゴリズムは PKCS5 の PBKDF1 (だと思います) で、ハッシュ関数として SHA-256 を使用し、256 ビットのキーを返します。 getSecretKey() で指定された反復回数まで)パスワード、ソルト、カウンタのハッシュを繰り返し生成することによって鍵を生成します。 PBE_ITERATION_COUNT で指定された反復回数まで、ここでは 100) のハッシュを繰り返し生成することで、 ブルートフォース攻撃に必要な時間を長くしています。ソルトの長さは、少なくとも生成する鍵と同じ長さ、この場合は少なくとも256ビットであるべきです。反復回数は、不合理な遅延を発生させない範囲で可能な限り長く設定する必要があります。鍵の生成におけるソルトと反復回数に関するより詳しい情報は RFC2898 .

しかし、JavaのPBEにおける実装は、パスワードにUnicode文字、つまり、表現するために8ビット以上を必要とする文字が含まれている場合に欠陥があります。で述べたように PBEKeySpec PKCS #5 で定義された PBE メカニズムは、各文字の下位 8 ビットだけを見ます。この問題を回避するには、パスワードのすべての 16 ビット文字から 16 ビット文字列 (8 ビット文字だけを含む) を生成し、それを PBEKeySpec . たとえば、"ABC" は "004100420043" になります。また、PBEKeySpecはパスワードをchar配列として要求するため、[co]で上書きすることができます。 clearPassword() で)上書きすることができます。 この質問 .) しかし、私はソルトを16進文字列として表現することに何の問題もないと思っています。

暗号化

鍵が生成されると、それを使ってテキストを暗号化したり復号化したりすることができるようになります。

実装1では、使用する暗号アルゴリズムが AES/CBC/PKCS5Padding つまり、PKCS#5 で定義されているパディングを使用した Cipher Block Chaining (CBC) 暗号モードの AES です。(他のAES暗号モードには、カウンタモード(CTR)、電子コードブックモード(ECB)、ガロアカウンタモード(GCM)があります。 Stack Overflowの別の質問 には、さまざまな AES 暗号モードと使用する推奨モードについて詳細に説明する回答があります。また、CBC モード暗号化にはいくつかの攻撃があり、そのいくつかは RFC 7457 で言及されていることに注意してください)。

暗号化されたデータの整合性もチェックする暗号化モードを使うべきであることに注意してください (たとえば。 関連付けられたデータで認証された暗号化 AEAD、RFC 5116 で説明) を使用する必要があることに注意してください。しかし AES/CBC/PKCS5Padding は整合性チェックを提供しないので、単独では推奨されません。 . AEAD の目的では、関連する鍵の攻撃を避けるために、通常の暗号化鍵の少なくとも 2 倍の長さの秘密を使用することが推奨されます:最初の半分は暗号化鍵として機能し、後半は完全性チェックの鍵として機能します。(つまり、この場合、パスワードとソルトから 1 つの秘密を生成し、その秘密を 2 つに分割します)。

Javaでの実装

実装1の様々な関数は、そのアルゴリズムに特定のプロバイダ、すなわち "BC" を使用しています。しかし、一般的には、特定のプロバイダーを要求することは推奨されません。サポートがない、コードの重複を避ける、またはその他の理由のために、すべてのプロバイダーがすべての Java 実装で利用できるとは限らないからです。このアドバイスは、2018年初頭に Android P プレビューがリリースされて以来、特に重要になっています。また Oracle プロバイダーの紹介 .

このように PROVIDER は存在してはならず、文字列 -BC から削除されなければなりません。 PBE_ALGORITHM . この点では実装2が正しい。

メソッドがすべての例外をキャッチするのではなく、処理できる例外のみを処理することは不適切です。質問で与えられた実装は、さまざまなチェックされた例外を投げることができます。メソッドはこれらのチェックされた例外のみを CryptoException でラップすることを選択するか、これらのチェックされた例外を throws 節で指定することができます。クラスが投げることができるチェックされた例外は潜在的にたくさんあるので、便宜上、元の例外をCryptoExceptionでラップすることはここでは適切かもしれません。

SecureRandom アンドロイド

Android Developers Blog の記事 "Some SecureRandom Thoughts" で詳しく説明されているように、Android の実装では java.security.SecureRandom の実装には、乱数の強度を低下させる欠陥があります。この欠陥は、その記事で説明されているように緩和することができます。