この質問をする理由:
AndroidでもAES暗号化に関して多くの質問があったことは知っています。また、Webを検索する場合、多くのコードスニペットがあります。しかし、すべてのページで、すべてのStack Overflowの質問で、大きな違いがある別の実装を見つけます。
そこで、この質問を作成して「ベストプラクティス」を見つけました。最も重要な要件のリストを収集し、本当に安全な実装をセットアップできることを願っています!
初期化ベクトルとソルトについて読みました。私が見つけたすべての実装にこれらの機能があるわけではありません。必要ですか?セキュリティが大幅に向上しますか?どのように実装しますか?暗号化されたデータを復号化できない場合、アルゴリズムは例外を発生させる必要がありますか?またはそれは安全ではなく、読み取り不能な文字列を返すだけですか?アルゴリズムはSHAの代わりにBcryptを使用できますか?
私が見つけたこれら2つの実装はどうですか?大丈夫ですか?完璧な、またはいくつかの重要なものが欠けていますか?これらのうち安全なものは何ですか?
アルゴリズムは、暗号化のために文字列と「パスワード」を受け取り、そのパスワードで文字列を暗号化する必要があります。出力は再び文字列(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;
}
}
実装#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でのベース暗号化。
キーとハッシュ
パスワードベースのシステムとソルトの議論を始めます。ソルトは、ランダムに生成された数値です。 「推定」されていません。実装1には、暗号的に強力な乱数を生成するgenerateSalt()
メソッドが含まれています。ソルトはセキュリティ上重要であるため、一度生成するだけでよいのですが、生成後は秘密にしておく必要があります。これがWebサイトである場合、saltを秘密にすることは比較的簡単ですが、インストールされたアプリケーション(デスクトップおよびモバイルデバイス用)の場合、これははるかに困難になります。
メソッドgetHash()
は、指定されたパスワードとソルトのハッシュを、単一の文字列に連結して返します。使用されるアルゴリズムはSHA-512で、512ビットのハッシュを返します。このメソッドは、文字列の整合性をチェックするのに役立つハッシュを返します。したがって、両方のパラメーターを単純に連結するため、パスワードまたはソルトのみでgetHash()
を呼び出すことで使用できます。この方法はパスワードベースの暗号化システムでは使用されないため、これ以上説明しません。
メソッドgetSecretKey()
は、generateSalt()
から返されるように、パスワードのchar
配列と16進エンコードされたソルトからキーを取得します。使用されるアルゴリズムは、ハッシュ関数としてSHA-256を使用したPKCS5のPBKDF1(私が思う)であり、256ビットキーを返します。 getSecretKey()
は、パスワード、ソルト、およびカウンター(_PBE_ITERATION_COUNT
_で指定された反復カウントまで、ここでは100)のハッシュを繰り返し生成することによりキーを生成し、ブルートフォース攻撃。ソルトの長さは、少なくとも生成されるキーと同じ長さ、この場合は少なくとも256ビットでなければなりません。反復カウントは、不当な遅延を引き起こすことなく、できるだけ長く設定する必要があります。キー派生のソルトと反復カウントの詳細については、 RFC2898 のセクション4を参照してください。
ただし、パスワードにUnicode文字、つまり8ビット以上を表現する必要がある文字がパスワードに含まれている場合、JavaのPBEの実装に欠陥があります。 PBEKeySpec
で述べたように、「PKCS#5で定義されたPBEメカニズムは、各文字の下位8ビットのみを参照します」。この問題を回避するには、PBEKeySpec
に渡す前に、パスワード内のすべての16ビット文字の16進文字列(8ビット文字のみを含む)を生成してみてください。たとえば、「ABC」は「004100420043」になります。また、PBEKeySpecは「パスワードをchar配列として要求するため、[clearPassword()
]で完了時に上書きできる」ことにも注意してください。 (「メモリ内の文字列の保護」に関しては、 この質問 を参照してください。)しかし、ソルトを16進エンコード文字列として表現することに問題はありません。
暗号化
キーが生成されると、それを使用してテキストを暗号化および復号化できます。実装1では、使用される暗号アルゴリズムは_AES/CBC/PKCS5Padding
_、つまり、PKCS#5でパディングが定義されているCipher Block Chaining(CBC)暗号モードのAESです。 (その他のAES暗号モードには、カウンターモード(CTR)、電子コードブックモード(ECB)、ガロアカウンターモード(GCM)が含まれます。 スタックオーバーフローに関する別の質問 には、さまざまなAES暗号モードについて詳しく説明する回答が含まれますCBCモード暗号化にはいくつかの攻撃があり、そのいくつかはRFC 7457で言及されていることにも注意してください。
暗号化されたテキストを部外者が利用できるようにする場合、その完全性を保護するために、暗号化されたデータ(およびオプションで追加のパラメーター)にメッセージ認証コード(MAC)を適用することをお勧めします(authenticated RFC 5116で説明されている関連データ、AEADによる暗号化。ここで人気があるのは、SHA-256またはその他の安全なハッシュ関数に基づいたハッシュベースのMAC(HMAC)です。ただし、MACを使用する場合、関連するキー攻撃を回避するために、通常の暗号化キーの少なくとも2倍の長さのシークレットを使用することをお勧めします。前半は暗号化キーとして機能し、後半はマック。 (この場合、パスワードとソルトから単一のシークレットを生成し、そのシークレットを2つに分割します。)
Java実装
実装1のさまざまな機能は、アルゴリズムに特定のプロバイダー、つまり「BC」を使用します。ただし、一般的に、特定のプロバイダーを要求することはお勧めしません。すべてのプロバイダーが、サポートの欠如、コードの重複の回避、またはその他の理由で、すべてのJava実装で利用できるわけではないからです。このアドバイスは、2018年初頭のAndroid Pプレビューのリリース以降、特に重要になりました。「BC」プロバイダーの一部の機能が廃止されているためです。記事「Androidの暗号化の変更Android Developers Blogの_ P "。 Oracleプロバイダーの紹介 も参照してください。
したがって、PROVIDER
は存在してはならず、文字列_-BC
_は_PBE_ALGORITHM
_から削除する必要があります。実装2はこの点で正しいです。
メソッドがすべての例外をキャッチすることは不適切ですが、可能な例外のみを処理することはできません。あなたの質問で与えられた実装は、さまざまなチェックされた例外を投げることができます。メソッドは、それらのチェック済み例外のみをCryptoExceptionでラップするか、throws
句でそれらのチェック済み例外を指定するかを選択できます。便宜上、元の例外をCryptoExceptionでラップすることが適切な場合があります。これは、クラスがスローできる多くのチェック済み例外が存在する可能性があるためです。
SecureRandom
in Android
Android Developers Blogの記事「Some SecureRandom Thoughts」で詳しく説明されているように、2013年より前のAndroidリリースでの_Java.security.SecureRandom
_の実装には、配信する乱数。この欠陥は、予測不能でランダムなデータブロック(_/dev/urandom
_の出力など)をそのクラスのsetSeed
メソッドに渡すことで軽減できます。
#2は、暗号に "AES"(つまり、テキストに対するECBモード暗号化を意味する)のみを使用するため、決して使用しないでください。 #1についてだけ説明します。
最初の実装は、暗号化のベストプラクティスに準拠しているようです。定数は一般に問題ありませんが、PBEを実行するためのソルトサイズと反復回数はどちらも短い側にあります。さらに、PBEキーの生成では256がハードコーディングされた値として使用されるため、AES-256のようです(これらすべての定数の後の恥)。 CBCとPKCS5Paddingを使用しますが、これは少なくとも予想どおりです。
認証/完全性保護は完全に欠落しているため、攻撃者は暗号文を変更できます。つまり、パディングOracle攻撃は、クライアント/サーバーモデルで可能です。また、攻撃者が暗号化されたデータを変更しようとする可能性があることも意味します。これにより、パディングまたはコンテンツがアプリケーションによって受け入れられないため、どこかでエラーが発生する可能性がありますが、それはあなたが望んでいる状況ではありません。
例外処理と入力検証を強化することができ、例外をキャッチすることは私の本では常に間違っています。さらに、このクラスはICryptを実装していますが、これはわかりません。クラス内で副作用のないメソッドのみを持つことは少し奇妙だということを知っています。通常、これらは静的にします。 Cipherインスタンスなどのバッファリングはありません。そのため、必要なすべてのオブジェクトがアドネズムとして作成されます。ただし、ICryptoを定義から安全に削除できます。その場合、コードを静的メソッドにリファクタリングすることもできます(または、よりオブジェクト指向のコードに書き換えることもできます)。
問題は、どのラッパーも常にユースケースについて仮定を行うことです。したがって、ラッパーが正しいか間違っていると言うことは二段です。これが、ラッパークラスの生成を常に回避しようとする理由です。しかし、少なくとも明示的に間違っているようには見えません。
あなたはかなり興味深い質問をしました。すべてのアルゴリズムと同様に、暗号キーは「秘密のソース」です。これは、一般に知られるようになると、他のすべてのものも同じになるためです。そこで、Googleによるこのドキュメントへの方法を検討します。
Google In-App Billingに加えて、セキュリティに関する考察も提供されます。
ここでニースの実装を見つけました: http://nelenkov.blogspot.fr/2012/04/using-password-based-encryption-on.html および https:// github。 com/nelenkov/Android-pbe これは、Android向けの十分なAES実装の探求にも役立ちました
BouncyCastle Lightweight APIを使用します。 PBEとソルトで256 AESを提供します。
ここでは、ファイルを暗号化/復号化できるサンプルコードを示します。
public void encrypt(InputStream fin, OutputStream fout, String password) {
try {
PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest());
char[] passwordChars = password.toCharArray();
final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount);
CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128);
aesCBC.init(true, aesCBCParams);
PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
aesCipher.init(true, aesCBCParams);
// Read in the decrypted bytes and write the cleartext to out
int numRead = 0;
while ((numRead = fin.read(buf)) >= 0) {
if (numRead == 1024) {
byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
final byte[] plain = new byte[offset];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
} else {
byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
int last = aesCipher.doFinal(plainTemp, offset);
final byte[] plain = new byte[offset + last];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
}
}
fout.close();
fin.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void decrypt(InputStream fin, OutputStream fout, String password) {
try {
PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest());
char[] passwordChars = password.toCharArray();
final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount);
CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128);
aesCBC.init(false, aesCBCParams);
PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
aesCipher.init(false, aesCBCParams);
// Read in the decrypted bytes and write the cleartext to out
int numRead = 0;
while ((numRead = fin.read(buf)) >= 0) {
if (numRead == 1024) {
byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
// int last = aesCipher.doFinal(plainTemp, offset);
final byte[] plain = new byte[offset];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
} else {
byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
int last = aesCipher.doFinal(plainTemp, offset);
final byte[] plain = new byte[offset + last];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
}
}
fout.close();
fin.close();
} catch (Exception e) {
e.printStackTrace();
}
}