问题描述
为什么我问这个问题:
我知道AES加密方面有很多问题,即使是Android。如果您搜索网页,还有很多代码段。但是在每一个页面上,在每个Stack Overflow问题中,我发现另一个实现有重大差异。
I know there have been a lot of questions about AES encryption, even for Android. And there are lots of code snippets if you search the Web. But on every single page, in every Stack Overflow question, I find another implementation with major differences.
所以我创建了这个问题,找到一个最佳实践。我希望我们可以收集最重要的要求清单,并设置一个非常安全的实现!
So I created this question to find a "best practice". I hope we can collect a list of the most important requirements and set up an implementation that is really secure!
我读到了初始化向量和盐。我发现并不是所有的实现都有这些功能。那么你需要它吗?它是否增加了安全性?你如何实现它?如果加密的数据不能解密,算法是否会引发异常?或者是不安全的,它应该只返回一个不可读的字符串?算法可以使用Bcrypt而不是SHA吗?
I read about initialization vectors and salts. Not all implementations I found had these features. So do you need it? Does it increase the security a lot? How do you implement it? Should the algorithm raise exceptions if the encrypted data cannot be decrypted? Or is that insecure and it should just return an unreadable string? Can the algorithm use Bcrypt instead of SHA?
我发现这两个实现怎么办?他们还好吗完美还是一些重要的事情丢失?这些是安全的?
What about these two implementations I found? Are they okay? Perfect or some important things missing? What of these is secure?
该算法应该使用字符串和密码进行加密,然后使用该密码加密字符串。输出应该是一个字符串(hex或base64?)。当然,解密也是可行的。
The algorithm should take a string and a "password" for encryption and then encrypt the string with that password. The output should be a string (hex or base64?) again. Decryption should be possible as well, of course.
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));
}
}
资料来源:
推荐答案
键和哈希
我将开始讨论基于密码的系统与盐。盐是一个随机生成的数字。不是推论的。实现1包括生成密码强的随机数的 generateSalt()
方法。因为盐对于安全性很重要,所以一旦生成盐就要保密,尽管只需要生成一次。如果这是一个网站,保持盐密码相对容易,但是对于安装的应用程序(对于台式机和移动设备)来说,这将会更加困难,因为假定这些应用程序不会保密。
I will start discussing the password-based system with salts. The salt is a randomly generated number. It is not "deduced". Implementation 1 includes a generateSalt()
method that generates a cryptographically strong random number. Because the salt is important to security, it should be kept secret once it is generated, though it only needs to be generated once. If this is a Web site, it's relatively easy to keep the salt secret, but for installed applications (for desktop and mobile devices), this will be much more difficult, as such applications are assumed not to keep secrets.
方法 getHash()
返回给定密码和salt的哈希值,并入一个字符串。使用的算法是SHA-512,它返回512位散列。这种方法返回一个哈希值,用于检查字符串的完整性,所以它也可以通过调用 getHash()
来使用,只需要一个密码或一个盐,因为它只是简单的连接两个参数。由于这种方法不会在基于密码的加密系统中使用,我将不再进一步讨论。
The method getHash()
returns a hash of the given password and salt, concatenated into a single string. The algorithm used is SHA-512, which returns a 512-bit hash. This method returns a hash that's useful for checking a string's integrity, so it might as well be used by calling getHash()
with just a password or just a salt, since it simply concatenates both parameters. Since this method won't be used in the password-based encryption system, I won't be discussing it further.
方法 getSecretKey( )
,从 generateSalt返回的密码和一个十六进制编码盐的
。所使用的算法是PKCS5和SHA-256作为散列函数的PBKDF1(我认为),并返回一个256位的密钥。 char
数组中导出一个键) getSecretKey()
通过重复生成密码,盐和计数器的哈希值(直到 PBE_ITERATION_COUNT $中给出的迭代计数)生成一个密钥c $ c>,这里100),以增加安装暴力攻击所需的时间。盐的长度应至少与生成的密钥一样长,在这种情况下至少要达到256位。迭代次数应尽可能长,不造成不合理的延迟。有关关键衍生中的盐和迭代次数的更多信息,请参阅中的第4节
The method getSecretKey()
, derives a key from a char
array of the password and a hex-encoded salt, as returned from generateSalt()
. The algorithm used is PBKDF1 (I think) from PKCS5 with SHA-256 as the hash function, and returns a 256-bit key. getSecretKey()
generates a key by repeatedly generating hashes of the password, salt, and a counter (up to the iteration count given in PBE_ITERATION_COUNT
, here 100) in order to increase the time needed to mount a brute-force attack. The salt's length should be at least as long as the key being generated, in this case, at least 256 bits. The iteration count should be set as long as possible without causing unreasonable delay. For more information on salts and iteration counts in key derivation, see section 4 in RFC2898.
但是,如果密码包含Unicode字符,即需要超过8位的字符,那么Java PBE中的实现是有缺陷的。如,PKCS#5中定义的PBE机制仅查看每个字符的低位8位。要解决此问题,您可以尝试生成密码中所有16位字符的十六进制字符串(仅包含8位字符),然后将其传递到 PBEKeySpec
。例如,ABC变成004100420043。实际上,您也可以使用char数组作为密码的参数,因为出于安全考虑,PBEKeySpec将密码作为char数组请求,因此可以覆盖[使用 clearPassword()
]完成后。
The implementation in Java's PBE, however, is flawed if the password contains Unicode characters, that is, those that require more than 8 bits to be represented. As stated in PBEKeySpec
, "the PBE mechanism defined in PKCS #5 looks at only the low order 8 bits of each character". To work around this problem, you can try generating a hex string (which will contain only 8-bit characters) of all 16-bit characters in the password before passing it to PBEKeySpec
. For example, "ABC" becomes "004100420043". In fact, you might as well use a char array as a parameter for the password, since for security purposes, PBEKeySpec "requests the password as a char array, so it can be overwritten [with clearPassword()
] when done". I don't see any problems, though, with representing a salt as a hex-encoded string.
加密
生成一个密钥后,我们可以使用它来加密和解密文本。在实现1中,所使用的密码算法是AES / CBC / PKCS5Padding ,即密文块链接(CBC)密码模式中的AES,PKCS#5中定义了填充。 (其他AES加密模式包括计数器模式(CTR),电子码本模式(ECB)和伽罗瓦计数器模式(GCM))。
Once a key is generated, we can use it to encrypt and decrypt text. In implementation 1, the cipher algorithm used is AES/CBC/PKCS5Padding
, that is, AES in the Cipher Block Chaining (CBC) cipher mode, with padding defined in PKCS#5. (Other AES cipher modes include counter mode (CTR), electronic codebook mode (ECB), and Galois counter mode (GCM).)
如果加密文本将被创建对外部人员可用,然后将加密的数据(以及可选的其他参数)应用消息认证码或MAC,以保护其完整性。这里受欢迎的是基于SHA-1,SHA-256或其他安全散列函数的基于散列的MAC或HMAC。但是,如果使用MAC,建议使用至少为正常加密密钥的两倍的密码,以避免相关的密钥攻击:上半部分用作加密密钥,第二半用作密钥苹果电脑。 (也就是说,在这种情况下,从密码和盐生成一个秘密,并将该秘密分成两部分。)
If the encrypted text will be made available to outsiders, then applying a message authentication code, or MAC, to the encrypted data (and optionally additional parameters) is recommended to protect its integrity. Popular here are hash-based MACs, or HMACs, that are based on SHA-1, SHA-256, or other secure hash functions. If a MAC is used, however, using a secret that's at least twice as long as a normal encryption key is recommended, to avoid related key attacks: the first half serves as the encryption key, and the second half serves as the key for the MAC. (That is, in this case, generate a single secret from a password and salt, and split that secret in two.)
Java实现
实现1中的各种功能使用特定的提供者,即BC,
作为其算法。一般来说,不建议请求特定的
提供程序,因为并非所有提供程序都可用于所有Java实现,
请参阅。
The various functions in implementation 1 uses a specific provider, namely "BC",for its algorithms. In general, though, it is not recommended to request specificproviders, since not all providers are available on all Java implementations,see the Introduction to Oracle Providers.
PROVIDER
不应该存在,字符串 -BC
应该从
PBE_ALGORITHM
。实现2在这方面是正确的。
Thus, PROVIDER
should not exist and the string -BC
should probably be removed fromPBE_ALGORITHM
. Implementation 2 is correct in this respect.
它不适合捕获所有异常的方法,而只能处理可以使用的
异常。您的问题中提供的实现可能会引发各种检查的异常。一个方法可以选择仅使用CryptoException来包装那些检查的异常,或者在 throws
子句中指定这些检查的异常。为方便起见,使用CryptoException包装原始异常在这里可能是适当的,因为类可能会抛出许多被检查的异常。
It is inappropriate for a method to catch all exceptions, but rather to handle only the exceptionsit can. The implementations given in your question can throw a variety of checked exceptions. A method can choose to wrap only those checked exceptions with CryptoException, or specify those checked exceptions in the throws
clause. For convenience, wrapping the original exception with CryptoException may be appropriate here, since there are potentially many checked exceptions the classes can throw.
这篇关于在Android中使用AES加密的最佳做法是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!