在Android中使用AES加密的最佳做法是什么?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在Android中使用AES加密的最佳做法是什么?相关的知识,希望对你有一定的参考价值。

为什么我问这个问题:

我知道有很多关于AES加密的问题,即使对于android也是如此。如果你在网上搜索,有很多代码片段。但是在每一个页面上,在每个Stack Overflow问题中,我都发现了另一个具有重大差异的实现。

所以我创建了这个问题以找到“最佳实践”。我希望我们可以收集最重要的要求列表,并建立一个非常安全的实施!

我读到了初始化载体和盐。并非我发现的所有实现都具有这些功能。所以你需要它吗?它是否会增加安全性?你是如何实现它的?如果加密数据无法解密,算法是否应该引发异常?或者这是不安全的,它应该只返回一个不可读的字符串?算法可以使用Bcrypt而不是SHA吗?

我发现这两个实现怎么样?他们还好吗?缺少完美或一些重要的事情?这些是安全的吗?

算法应该使用字符串和“密码”进行加密,然后使用该密码加密字符串。输出应该是一个字符串(十六进制或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中基于密码的加密方面。

键和哈希

我将开始讨论基于密码的系统与盐。盐是随机生成的数字。它不是“推断”的。实现1包括生成加密强随机数的generateSalt()方法。因为盐对安全很重要,所以一旦生成它就应该保密,尽管它只需要生成一次。如果这是一个网站,保持盐的秘密相对容易,但对于已安装的应用程序(对于桌面和移动设备),这将更加困难。

方法getHash()返回给定密码和salt的哈希值,连接成单个字符串。使用的算法是SHA-512,它返回512位散列。这个方法返回一个对检查字符串完整性很有用的哈希值,所以它也可以通过只用密码或只是一个盐调用getHash()来使用,因为它只是连接两个参数。由于此方法不会在基于密码的加密系统中使用,因此我不会进一步讨论。

方法getSecretKey(),从密码的char数组和从generateSalt()返回的十六进制编码的盐派生出一个密钥。使用的算法是来自PKCS5的PBKDF1(我认为),其中SHA-256作为哈希函数,并返回256位密钥。 getSecretKey()通过重复生成密码,盐和计数器的哈希值来生成密钥(直到PBE_ITERATION_COUNT中给出的迭代计数,这里为100),以便增加进行暴力攻击所需的时间。 salt的长度应该至少与生成的密钥一样长,在这种情况下,至少为256位。迭代计数应尽可能长,而不会造成不合理的延迟。有关密钥派生中的盐和迭代计数的更多信息,请参阅RFC2898中的第4节。

但是,如果密码包含Unicode字符,即需要表示超过8位的字符,则Java PBE中的实现存在缺陷。正如PBEKeySpec所述,“PKCS#5中定义的PBE机制仅查看每个字符的低位8位”。若要解决此问题,您可以尝试生成密码中所有16位字符的十六进制字符串(将只包含8位字符),然后再将其传递给PBEKeySpec。例如,“ABC”变为“004100420043”。另请注意,PBEKeySpec“请求将密码作为char数组,因此可以在完成时使用clearPassword()覆盖”。 (关于“保护内存中的字符串”,请参阅this question。)但是,我没有看到任何问题,将salt表示为十六进制编码的字符串。

加密

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

如果加密文本可供外人使用,则建议将加密数据(以及可选的附加参数)应用消息验证代码或MAC来保护其完整性(这种技术称为带有相关数据的验证加密,AEAD,在RFC 5116中描述。这里流行的是基于散列的MAC或HMAC,它们基于SHA-256或其他安全散列函数。但是,如果使用MAC,则建议使用至少是普通加密密钥长度的两倍的秘密,以避免相关的密钥攻击:前半部分用作加密密钥,后半部分用作密钥。苹果电脑。 (也就是说,在这种情况下,从密码和盐生成一个秘密,并将该秘密分成两个。)

Java实现

实现1中的各种功能使用特定提供者,即“BC”,用于其算法。但是,一般情况下,不建议请求特定的提供程序,因为并非所有提供程序都可用于所有Java实现,无论是缺乏支持,是为了避免代码重复,还是出于其他原因。自从2018年初推出Android P预览版以来,这个建议变得尤为重要,因为“BC”提供商的某些功能已被弃用 - 请参阅Android开发者博客中的文章“Android P中的加密更改”。另见Introduction to Oracle Providers

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

方法捕获所有异常是不合适的,而是仅处理它可以的异常。您的问题中给出的实现可以抛出各种已检查的异常。方法可以选择仅使用CryptoException包装那些已检查的异常,或者在throws子句中指定那些已检查的异常。为方便起见,在此处使用CryptoException包装原始异常可能是合适的,因为类可能会抛出许多已检查的异常。

Android中的SecureRandom

正如Android开发者博客中的文章“Some SecureRandom Thoughts”中所详述的,2013年之前在Android版本中实施java.security.SecureRandom有一个缺陷,会降低它提供的随机数的强度。通过将不可预测的随机数据块(例如/dev/urandom的输出)传递给该类的setSeed方法,可以减轻这个缺陷。

另一答案

永远不应该使用#2,因为它只对密码使用“AES”(这意味着对文本进行ECB模式加密,这是一个很大的禁忌)。我只想谈谈#1。

第一个实现似乎遵循加密的最佳实践。常数通常是正常的,尽管盐的大小和执行PBE的迭代次数都是短边。此外,似乎是AES-256,因为PBE密钥生成使用256作为硬编码值(在所有这些常量之后是一种耻辱)。它使用CBC和PKCS5Padding,这至少是你所期望的。

完全缺少任何身份验证/完整性保护,因此攻击者可以更改密文。这意味着在客户端/服务器模型中可以填充oracle攻击。这也意味着攻击者可以尝试更改加密数据。这可能会导致某些错误,因为应用程序不接受填充或内容,但这不是您想要的情况。

可以增强异常处理和输入验证,在我的书中捕获异常总是错误的。此外,该课程实现了ICrypt,我不知道。我知道在课堂上只有没有副作用的方法有点奇怪。通常,你会使那些静态。 Cipher实例等没有缓冲,因此每个必需的对象都会被创建出来。但是,您可以安全地从似乎的定义中删除ICrypto,在这种情况下,您还可以将代码重构为静态方法(或者将其重写为更面向对象,您的选择)。

问题是任何包装器总是对用例做出假设。因此,说包装是对还是错是非常重要的。这就是我总是试图避免生成包装类的原因。但至少它似乎并没有明显错误。

另一答案

你问过一个非常有趣的问题。与所有算法一样,密码密钥是“秘密酱”,因为一旦公众知道,其他一切都是如此。因此,您可以通过Google查看此文档的方法

security

除了Google In-App Billing之外,还提供了有关安全性的想法,这也是很有见地的

billing_best_practices

另一答案

使用BouncyCastle轻量级API。它提供256个带PBE和盐的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

以上是关于在Android中使用AES加密的最佳做法是什么?的主要内容,如果未能解决你的问题,请参考以下文章

Android数据加密之Aes加密

AES加密 在PC上和Android上不一样 怎么解决

Android 加密/解密问题 (AES)

在 Android 和 iPhone 中使用 AES-128 加密(不同的结果)

aes算法填充方式

如何在 Android 中使用自己的密钥进行 AES-256 加密?