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


89

为什么我问这个问题:

我知道,即使对于Android,也存在很多关于AES加密的问题。如果您在网上搜索,则有很多代码片段。但是,在每个页面上,在每个堆栈溢出问题中,我都发现另一个实现方式存在重大差异。

因此,我提出了这个问题以寻找“最佳实践”。我希望我们可以收集最重要的要求的清单,并建立一个真正安全的实现!

我读了有关初始化向量和盐的信息。我发现并非所有实现都具有这些功能。那你需要吗?它会大大提高安全性吗?您如何实施?如果加密的数据无法解密,该算法是否应该引发异常?还是那是不安全的,它应该只返回不可读的字符串?该算法可以使用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


我正在尝试实现解决方案1,但它需要一些类。您有完整的源代码吗?
albanx 2012年

1
不,我没有,对不起。但是我只需删除implements ICrypto并更改throws CryptoExceptionthrows Exception等等就可以使它工作。因此,您将不再需要这些类。
caw 2012年

但是还缺少HexEncoder类吗?在哪里可以找到它?
albanx 2012年

我认为HexEncoder是BouncyCastle库的一部分。您可以下载它。或者,您也可以使用Google搜索“ byte [] to hex”,反之亦然。
caw 2012年

谢谢Marco。但我注意到,有3种方法getSecretKeygetHashgenerateSalt在第一次执行有未使用的。也许我是错的,但实际上如何使用此类来加密字符串?
albanx 2012年

Answers:


37

您在问题中给出的任何一种实施方式都不是完全正确的,并且您给出的任何一种实施方式均不应原样使用。接下来,我将讨论Android中基于密码的加密的各个方面。

键和哈希

我将开始讨论含盐的基于密码的系统。盐是随机生成的数字。它不是“推论”的。实施方案1包括一种generateSalt()生成密码学上强的随机数的方法。由于盐对于安全性很重要,因此,即使只生成一次,也应将其保密。如果这是一个网站,则将盐的秘密保密相对容易,但是对于已安装的应用程序(台式机和移动设备)而言,这将更加困难。

该方法getHash()返回给定密码和盐的哈希值,并连接到单个字符串中。使用的算法是SHA-512,它返回512位哈希。此方法返回的哈希值对于检查字符串的完整性很有用,因此也可以将getHash()其仅用于连接两个参数,因此也可以仅使用密码或盐进行调用。由于此方法不会在基于密码的加密系统中使用,因此我将不再对其进行讨论。

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

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

加密

生成密钥后,我们可以使用它来加密和解密文本。

在实现1中,使用的加密算法为AES/CBC/PKCS5Padding,即密码块链接(CBC)加密模式下的AES,在PKCS#5中定义了填充。(其他AES密码模式包括计数器模式(CTR),电子密码本模式(ECB)和Galois计数器模式(GCM)。关于堆栈溢出的另一个问题包含详细讨论各种AES密码模式和推荐使用的答案的答案。也要注意,有几种针对CBC模式加密的攻击,其中一些在RFC 7457中提到。)

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

Java实现

实现1中的各种功能对其算法使用特定的提供程序,即“ BC”。但是,通常不建议请求特定的提供程序,因为并非所有支持程序都在所有Java实现中都可用,无论是出于缺乏支持,避免代码重复还是出于其他原因。自2018年初发布Android P预览以来,此建议尤其重要,因为“ BC”提供程序的某些功能已在此处弃用-请参阅Android开发者博客中的文章“ Android P的密码学更改”。另请参见Oracle提供程序简介

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

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

SecureRandom 在Android中

如Android Developers Blog中的“ Some SecureRandom Thoughts”一文中所述,java.security.SecureRandom2013年之前的Android版本中的实施存在一个缺陷,该缺陷会降低其提供的随机数的强度。如该文章所述,可以缓解此缺陷。


在我看来,双重机密生成有点浪费,您可以轻松地将生成的机密一分为二,或者-如果没有足够的可用位,请在其中添加一个计数器(第一个密钥为1,第二个密钥为2)。秘密并执行单个哈希。无需两次执行所有迭代。
Maarten Bodewes 2011年

感谢您提供有关HMAC和盐的信息。这次我不会使用HMAC,但是稍后可能会非常有用。总的来说,这无疑是一件好事。
caw

非常感谢您所做的所有编辑,以及本(现在)关于Java中AES加密的精彩介绍!
caw 2012年

1
这应该。getInstance具有仅采用算法名称的重载。示例:Cipher.getInstance()可能在Java实现中注册了多个提供程序,包括Bouncy Castle,这种重载会在提供程序列表中搜索实现给定算法的提供程序之一。您应该尝试一下看看。
Peter O.

1
是的,它将按Security.getProviders()给出的顺序搜索提供程序-尽管现在还将检查init()调用期间该提供程序是否接受密钥,从而允许硬件辅助加密。此处有更多详细信息:docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/…
Maarten Bodewes,2012年

18

#2永远不要使用,因为它仅对密码使用“ AES”(这意味着对文本使用ECB模式加密,这是很大的禁止)。我只谈论#1。

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

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

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

问题在于,任何包装程序都总是对用例进行假设。因此说包装纸是对是错。这就是为什么我总是尝试避免生成包装器类的原因。但是至少看起来似乎并没有错。


非常感谢您提供详细的答案!我知道这很遗憾,但是我还不知道代码审查部分:D谢谢您的提示,我将进行检查。但是在我看来,这个问题也确实适用于此,因为我不只是希望回顾这些代码段。相反,我想问您在Android中实现AES加密时哪些方面很重要。再次正确,此代码段适用于AES-256。因此,您会说这通常是AES-256的安全实现吗?用例是我只想安全地将文本信息存储在数据库中。
caw

1
看起来不错,但是没有完整性检查和身份验证的想法会让我感到困扰。如果您有足够的空间,我会认真考虑在密文上添加HMAC。就是说,由于您可能试图简单地增加机密性,所以我认为这是一个很大的优点,但不是直接要求。
Maarten Bodewes 2011年

但是,如果只希望他人不应该访问加密的信息,那么我就不需要HMAC,对吗?如果他们更改了密文并强加解密的“错误”结果,那么这没有真正的问题,对吗?
CAW

如果那不是您的风险情况,那很好。如果他们能够以某种方式在更改密文之后触发系统的重复解密(填充预言攻击),那么他们就可以在不知道密钥的情况下解密数据。如果仅在没有密钥的系统上保留数据,则他们将无法执行此操作。但这就是为什么添加HMAC始终是最佳实践的原因。就我个人而言,我认为具有AES-128和HMAC的系统比没有-的AES-256安全,但是如上所述,可能不是必需的。
Maarten Bodewes,2011年

1
如果要完整性,为什么不以伽罗瓦/计数器模式(AES-GCM)使用AES?
Kimvais 2011年

1

您问了一个非常有趣的问题。与所有算法一样,密码密钥是“秘密调味料”,因为一旦公开就知道了,其他所有内容也是如此。因此,您研究了Google处理此文档的方法

安全

除了Google In-App Billing之外,还提供了一些有关安全性方面的想法,

billing_best_practices


感谢这些链接!“当密码密钥出了,其他所有东西都出了”,您到底是什么意思?
caw 2012年

我的意思是加密密钥需要安全,如果任何人都可以掌握,那么您的加密数据就和纯文本一样好。如果您认为我的回答在一定程度上有所帮助,请
加注

0

使用BouncyCastle轻量级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();
    }
}

谢谢!这可能是一个很好且安全的解决方案,但我不想使用第三方软件。我敢肯定,必须以自己的安全方式实施AES。
caw

2
取决于您是否要包括针对侧通道攻击的防护。通常,您应该假设自己实施密码算法是非常不安全的。由于AES CBC在Oracle的Java运行时库中可用,因此如果算法不可用,最好使用它们并使用Bouncy Castle库。
Maarten Bodewes 2012年

它缺少buf(我真的希望它不是一个static字段)的定义。两者看起来都一样encrypt()decrypt()如果输入是1024字节的倍数,则将无法正确处理最终块。
tc。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.