在 Android 中使用 AES 加密的最佳实践是什么? [英] What are best practices for using AES encryption in Android?

查看:42
本文介绍了在 Android 中使用 AES 加密的最佳实践是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我为什么问这个问题:

我知道有很多关于 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?

该算法应该采用一个字符串和一个密码"进行加密,然后使用该密码对字符串进行加密.输出应该再次是一个字符串(十六进制或 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;
        }

}

来源: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 中基于密码的加密的各个方面.

Neither implementation you give in your question is entirely correct, and neither implementation you give should be used as is. In what follows, I will discuss aspects of password-based encryption in Android.

键和哈希

我将开始讨论带有盐的基于密码的系统.盐是随机生成的数字.它不是推断"出来的.实现 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.

方法 getHash() 返回给定密码和盐的散列,并连接成单个字符串.使用的算法是 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(),从char 密码数组和一个十六进制编码的salt 派生出一个密钥,从generateSalt().使用的算法是来自 PKCS5 的 PBKDF1(我认为),以 SHA-256 作为哈希函数,并返回一个 256 位的密钥.getSecretKey() 通过重复生成密码、salt 和计数器的哈希值(最多达到 PBE_ITERATION_COUNT 中给出的迭代计数,此处为 100)来生成密钥以增加发动蛮力攻击所需的时间.盐的长度应该至少与生成的密钥一样长,在这种情况下,至少为 256 位.迭代次数应设置得尽可能长,不会造成不合理的延迟.有关密钥派生中的盐和迭代计数的更多信息,请参阅 RFC2898.

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 中的实现存在缺陷.如PBEKeySpec,PKCS #5 中定义的 PBE 机制仅查看每个字符的低 8 位".要解决此问题,您可以在将密码传递给 PBEKeySpec 之前尝试生成密码中所有 16 位字符的十六进制字符串(仅包含 8 位字符).例如,ABC"变为004100420043".另请注意,PBEKeySpec将密码请求为字符数组,因此可以在完成后[使用 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". Note also that PBEKeySpec "requests the password as a char array, so it can be overwritten [with clearPassword()] when done". (With respect to "protecting strings in memory", see this question.) I don't see any problems, though, with representing a salt as a hex-encoded string.

加密

一旦生成了密钥,我们就可以使用它来加密和解密文本.

Once a key is generated, we can use it to encrypt and decrypt text.

在实现1中,使用的密码算法是AES/CBC/PKCS5Padding,即密码块链接(CBC)密码模式中的AES,填充定义在PKCS#5中.(其他AES密码模式包括计数器模式(CTR)、电子码本模式(ECB)和伽罗瓦计数器模式(GCM).Another question on Stack Overflow 包含详细讨论各种 AES 密码模式和推荐使用的模式的答案.也要注意,有几种针对 CBC 模式加密的攻击,其中一些在 RFC 7457 中提到.)

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). Another question on Stack Overflow contains answers that discuss in detail the various AES cipher modes and the recommended ones to use. Be aware, too, that there are several attacks on CBC mode encryption, some of which are mentioned in RFC 7457.)

请注意,您应该使用还检查加密数据完整性的加密模式(例如,使用关联数据进行身份验证的加密,AEAD,在 RFC 5116 中进行了描述).但是,AES/CBC/PKCS5Padding 不提供完整性检查,因此不推荐单独使用.出于AEAD目的,建议使用至少是普通加密密钥两倍长的秘密,以避免相关的密钥攻击:前半部分作为加密密钥,后半部分作为完整性检查的密钥.(也就是说,在这种情况下,从密码和盐生成一个秘密,然后将该秘密一分为二.)

Note that you should use an encryption mode that also checks the encrypted data for integrity (e.g., authenticated encryption with associated data, AEAD, described in RFC 5116). However, AES/CBC/PKCS5Padding doesn't provide integrity checking, so it alone is not recommended. For AEAD purposes, 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 integrity check. (That is, in this case, generate a single secret from a password and salt, and split that secret in two.)

Java 实现

实现 1 中的各种函数为其算法使用特定的提供程序,即BC".但是,一般而言,不建议请求特定的提供者,因为并非所有提供者都适用于所有 Java 实现,无论是缺乏支持、避免代码重复还是出于其他原因.自 2018 年初发布 Android P 预览版以来,此建议变得尤为重要,因为BC"提供商的某些功能已在那里被弃用 —请参阅 Android 开发人员博客中的文章Android P 中的密码学变化".另请参阅Oracle 提供程序简介.

The various functions in implementation 1 use a specific provider, namely "BC", for its algorithms. In general, though, it is not recommended to request specific providers, since not all providers are available on all Java implementations, whether for lack of support, to avoid code duplication, or for other reasons. This advice has especially become important since the release of Android P preview in early 2018, because some functionality from the "BC" provider has been deprecated there — see the article "Cryptography Changes in Android P" in the Android Developers Blog. See also the Introduction to Oracle Providers.

因此,PROVIDER 不应该存在并且字符串 -BC 应该从 PBE_ALGORITHM 中删除.实现 2 在这方面是正确的.

Thus, PROVIDER should not exist and the string -BC should be removed from PBE_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 exceptions it 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.

SecureRandom 在 Android 中

SecureRandom in Android

如 Android 开发者博客中一些 SecureRandom 想法"一文所述,2013 年之前的 Android 版本中 java.security.SecureRandom 的实现存在降低随机数强度的缺陷它提供.可以按照那篇文章中的描述来缓解此缺陷.

As detailed in the article "Some SecureRandom Thoughts", in the Android Developers Blog, the implementation of java.security.SecureRandom in Android releases before 2013 has a flaw that reduces the strength of random numbers it delivers. This flaw can be mitigated as described in that article.

这篇关于在 Android 中使用 AES 加密的最佳实践是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆