Mengapa saya mengajukan pertanyaan ini:
Saya tahu ada banyak pertanyaan tentang enkripsi AES, bahkan untuk Android. Dan ada banyak potongan kode jika Anda mencari di Web. Tetapi di setiap halaman, di setiap pertanyaan Stack Overflow, saya menemukan implementasi lain dengan perbedaan besar.
Jadi saya membuat pertanyaan ini untuk menemukan "praktik terbaik". Saya harap kami dapat mengumpulkan daftar persyaratan terpenting dan menyiapkan penerapan yang benar-benar aman!
Saya membaca tentang vektor inisialisasi dan garam. Tidak semua implementasi yang saya temukan memiliki fitur ini. Jadi, apakah Anda membutuhkannya? Apakah itu meningkatkan keamanan banyak? Bagaimana Anda menerapkannya? Haruskah algoritme meningkatkan pengecualian jika data yang dienkripsi tidak dapat didekripsi? Atau apakah itu tidak aman dan seharusnya mengembalikan string yang tidak dapat dibaca? Bisakah algoritme menggunakan Bcrypt sebagai ganti SHA?
Bagaimana dengan dua implementasi yang saya temukan ini? Apakah mereka baik-baik saja? Sempurna atau beberapa hal penting hilang? Apa dari ini yang aman?
Algoritme harus mengambil string dan "kata sandi" untuk enkripsi dan kemudian mengenkripsi string dengan kata sandi itu. Outputnya harus berupa string (hex atau base64?) Lagi. Dekripsi harus dimungkinkan juga, tentu saja.
Apa implementasi AES yang sempurna untuk Android?
Implementasi # 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;
}
}
Implementasi # 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));
}
}
Sumber: http://www.tutorials-android.com/learn/How_to_encrypt_and_decrypt_strings.rhtml
sumber
implements ICrypto
dan mengubahnyathrows CryptoException
kethrows Exception
dan seterusnya. Jadi Anda tidak membutuhkan kelas-kelas itu lagi.getSecretKey
,getHash
,generateSalt
dalam pelaksanaan pertama yang terpakai. Mungkin saya salah, tetapi bagaimana kelas ini dapat digunakan untuk mengenkripsi string dalam praktiknya?Jawaban:
Tidak ada implementasi yang Anda berikan dalam pertanyaan Anda sepenuhnya benar, dan implementasi yang Anda berikan tidak boleh digunakan apa adanya. Berikut ini, saya akan membahas aspek enkripsi berbasis kata sandi di Android.
Kunci dan Hash
Saya akan mulai membahas sistem berbasis kata sandi dengan garam. Garam adalah angka yang dibuat secara acak. Itu tidak "disimpulkan". Penerapan 1 mencakup
generateSalt()
metode yang menghasilkan bilangan acak kuat secara kriptografis. Karena garam penting untuk keamanan, garam harus dirahasiakan setelah dibuat, meskipun hanya perlu dibuat sekali. Jika ini adalah situs Web, relatif mudah untuk merahasiakan salt, tetapi untuk aplikasi yang diinstal (untuk desktop dan perangkat seluler), ini akan jauh lebih sulit.Metode ini
getHash()
mengembalikan hash dari kata sandi dan garam yang diberikan, digabungkan menjadi satu string. Algoritme yang digunakan adalah SHA-512, yang mengembalikan hash 512-bit. Metode ini mengembalikan hash yang berguna untuk memeriksa integritas string, jadi mungkin juga digunakan dengan memanggilgetHash()
hanya dengan kata sandi atau hanya garam, karena itu hanya menggabungkan kedua parameter. Karena metode ini tidak akan digunakan dalam sistem enkripsi berbasis kata sandi, saya tidak akan membahasnya lebih lanjut.Metodenya
getSecretKey()
, mendapatkan kunci darichar
array kata sandi dan garam yang dikodekan hex, seperti yang dikembalikan darigenerateSalt()
. Algoritma yang digunakan adalah PBKDF1 (menurut saya) dari PKCS5 dengan SHA-256 sebagai fungsi hash, dan mengembalikan kunci 256-bit.getSecretKey()
menghasilkan kunci dengan membuat hash berulang kali dari kata sandi, garam, dan penghitung (hingga jumlah iterasi yang diberikanPBE_ITERATION_COUNT
, di sini 100) untuk meningkatkan waktu yang diperlukan untuk memasang serangan brute force. Panjang garam setidaknya harus selama kunci yang dihasilkan, dalam hal ini, setidaknya 256 bit. Hitungan iterasi harus disetel selama mungkin tanpa menyebabkan penundaan yang tidak wajar. Untuk informasi lebih lanjut tentang garam dan jumlah iterasi dalam penurunan kunci, lihat bagian 4 di RFC2898 .Implementasi di PBE Java, bagaimanapun, cacat jika kata sandi berisi karakter Unicode, yaitu yang membutuhkan lebih dari 8 bit untuk diwakili. Seperti yang dinyatakan dalam
PBEKeySpec
, "mekanisme PBE yang didefinisikan dalam PKCS # 5 hanya melihat 8 bit orde rendah dari setiap karakter". Untuk mengatasi masalah ini, Anda dapat mencoba membuat string hex (yang hanya akan berisi karakter 8-bit) dari semua karakter 16-bit dalam kata sandi sebelum meneruskannyaPBEKeySpec
. Misalnya, "ABC" menjadi "004100420043". Perhatikan juga bahwa PBEKeySpec "meminta kata sandi sebagai array karakter, sehingga dapat ditimpa [denganclearPassword()
] setelah selesai". (Sehubungan dengan "melindungi string dalam memori", lihat pertanyaan ini .) Namun, saya tidak melihat ada masalah,Enkripsi
Setelah kunci dibuat, kita dapat menggunakannya untuk mengenkripsi dan mendekripsi teks.
Dalam implementasi 1, algoritma cipher yang digunakan adalah
AES/CBC/PKCS5Padding
AES dalam mode cipher Cipher Block Chaining (CBC), dengan padding yang ditentukan dalam PKCS # 5. (Mode penyandian AES lainnya termasuk mode penghitung (CTR), mode buku kode elektronik (ECB), dan mode penghitung Galois (GCM). Pertanyaan lain tentang Stack Overflow berisi jawaban yang membahas secara rinci berbagai mode penyandian AES dan yang direkomendasikan untuk digunakan. Ketahuilah juga bahwa ada beberapa serangan pada enkripsi mode CBC, beberapa di antaranya disebutkan di RFC 7457.)Perhatikan bahwa Anda harus menggunakan mode enkripsi yang juga memeriksa data terenkripsi untuk integritas (misalnya, enkripsi terotentikasi dengan data terkait , AEAD, dijelaskan dalam RFC 5116). Namun,
AES/CBC/PKCS5Padding
tidak memberikan pemeriksaan integritas, jadi itu sendiri tidak disarankan . Untuk tujuan AEAD, disarankan menggunakan rahasia yang setidaknya dua kali lebih panjang dari kunci enkripsi normal, untuk menghindari serangan kunci terkait: bagian pertama berfungsi sebagai kunci enkripsi, dan bagian kedua berfungsi sebagai kunci untuk pemeriksaan integritas. (Yaitu, dalam hal ini, menghasilkan satu rahasia dari kata sandi dan garam, dan membagi rahasia itu menjadi dua.)Implementasi Java
Berbagai fungsi dalam implementasi 1 menggunakan provider tertentu, yaitu "BC", untuk algoritmanya. Secara umum, tidak disarankan untuk meminta penyedia tertentu, karena tidak semua penyedia tersedia di semua implementasi Java, baik karena kurangnya dukungan, untuk menghindari duplikasi kode, atau karena alasan lain. Saran ini menjadi penting terutama sejak rilis pratinjau Android P pada awal 2018, karena beberapa fungsi dari penyedia "BC" sudah tidak digunakan lagi di sana - lihat artikel "Perubahan Kriptografi di Android P" di Blog Pengembang Android. Lihat juga Pengantar Penyedia Oracle .
Jadi,
PROVIDER
seharusnya tidak ada dan string-BC
harus dihilangkanPBE_ALGORITHM
. Implementasi 2 benar dalam hal ini.Metode tidak sesuai untuk menangkap semua pengecualian, melainkan hanya menangani pengecualian yang bisa. Penerapan yang diberikan dalam pertanyaan Anda bisa memunculkan berbagai pengecualian yang dicentang. Sebuah metode dapat memilih untuk hanya membungkus pengecualian yang dicentang dengan CryptoException, atau menentukan pengecualian yang dicentang dalam
throws
klausa. Untuk kenyamanan, menggabungkan pengecualian asli dengan CryptoException mungkin sesuai di sini, karena ada banyak kemungkinan pengecualian yang dicentang yang dapat dilontarkan kelas.SecureRandom
di AndroidSeperti yang dijelaskan dalam artikel "Some SecureRandom Thoughts", di Android Developers Blog, penerapan
java.security.SecureRandom
di rilis Android sebelum tahun 2013 memiliki kekurangan yang mengurangi kekuatan bilangan acak yang dihasilkannya. Cacat ini dapat diatasi seperti yang dijelaskan di artikel itu.sumber
getInstance
memiliki kelebihan beban yang hanya membutuhkan nama algoritme. Contoh: Cipher.getInstance () Beberapa penyedia, termasuk Bouncy Castle, mungkin terdaftar dalam implementasi Java dan jenis overload ini mencari daftar penyedia untuk salah satu penyedia yang mengimplementasikan algoritma yang diberikan. Anda harus mencobanya dan melihatnya.# 2 tidak boleh digunakan karena hanya menggunakan "AES" (yang berarti enkripsi mode ECB pada teks, sangat tidak-tidak) untuk penyandian. Saya hanya akan berbicara tentang # 1.
Penerapan pertama tampaknya mematuhi praktik terbaik untuk enkripsi. Konstanta umumnya OK, meskipun ukuran garam dan jumlah iterasi untuk melakukan PBE memiliki kekurangan. Selanjutnya, tampaknya untuk AES-256 karena generasi kunci PBE menggunakan 256 sebagai nilai kode keras (sayang sekali setelah semua konstanta tersebut). Ini menggunakan CBC dan PKCS5Padding yang setidaknya seperti yang Anda harapkan.
Yang benar-benar hilang adalah perlindungan otentikasi / integritas, sehingga penyerang dapat mengubah teks sandi. Ini berarti bahwa serangan oracle padding dimungkinkan dalam model klien / server. Ini juga berarti bahwa penyerang dapat mencoba dan mengubah data yang dienkripsi. Ini kemungkinan akan menghasilkan beberapa kesalahan di suatu tempat karena padding atau konten tidak diterima oleh aplikasi, tetapi itu bukan situasi yang Anda inginkan.
Penanganan pengecualian dan validasi input bisa ditingkatkan, menangkap Exception selalu salah di buku saya. Selanjutnya, kelas mengimplementasikan ICrypt, yang saya tidak tahu. Saya tahu bahwa hanya memiliki metode tanpa efek samping di kelas agak aneh. Biasanya, Anda akan membuat itu statis. Tidak ada buffering untuk instance Cipher dll., Jadi setiap objek yang diperlukan akan dibuat ad-nauseum. Namun, Anda dapat dengan aman menghapus ICrypto dari definisi tampaknya, dalam hal ini Anda juga dapat mengubah kode ke metode statis (atau menulis ulang menjadi lebih berorientasi objek, pilihan Anda).
Masalahnya adalah setiap pembungkus selalu membuat asumsi tentang kasus penggunaan. Untuk mengatakan bahwa pembungkus benar atau salah adalah omong kosong. Inilah sebabnya mengapa saya selalu berusaha menghindari menghasilkan kelas pembungkus. Tapi setidaknya hal itu tampaknya tidak salah secara eksplisit.
sumber
Anda telah mengajukan pertanyaan yang cukup menarik. Seperti semua algoritma, kunci sandi adalah "saus rahasia", karena begitu diketahui publik, yang lainnya juga. Jadi Anda mencari cara untuk dokumen ini oleh Google
keamanan
Selain itu Google In-App Billing juga memberikan pemikiran tentang keamanan yang berwawasan juga
billing_best_practices
sumber
Gunakan BouncyCastle Lightweight API. Ini menyediakan 256 AES Dengan PBE dan Garam.
Berikut kode contoh, yang dapat mengenkripsi / mendekripsi file.
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(); } }
sumber
buf
(saya sangat berharap ini bukanstatic
bidang). Ini juga terlihat seperti keduanyaencrypt()
dandecrypt()
akan gagal memproses blok terakhir dengan benar jika inputnya adalah kelipatan 1024 byte.Saya menemukan penerapan yang bagus di sini: http://nelenkov.blogspot.fr/2012/04/using-password-based-encryption-on.html dan https://github.com/nelenkov/android-pbe. Itu juga berguna dalam pencarian saya untuk Implementasi AES yang cukup baik untuk Android
sumber