在Android中使用AES加密的最佳实践是什么?

91

为什么我要问这个问题:

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


我正在尝试实现解决方案1,但需要一些类。你有完整的源代码吗? - albanx
1
不好意思,我没有。但是我通过简单地删除“implements ICrypto”并将“throws CryptoException”更改为“throws Exception”等方式使其正常工作。因此,您将不再需要那些类了。 - caw
但是HexEncoder类也缺失了?我在哪里可以找到它? - albanx
此实现仅可用于存储本地数据,而不用于交换数据,因为存在随机盐值。 - albanx
对于HexEncode,您可以使用此链接,对于hex to bye[],您可以使用此链接 - VSB
显示剩余4条评论
5个回答

38
你在问题中给出的两种实现都不完全正确,也不能直接使用。 接下来,我将讨论Android中基于密码的加密方面。 密钥和哈希 我将从含盐的基于密码的系统开始讨论。盐是一个随机生成的数字,不是"推导"得出的。实现1包括一个generateSalt()方法,用于生成具有密码学强度的随机数。由于盐对于安全性很重要,一旦生成就应该保密,尽管它只需要生成一次。如果这是一个网站,保持盐的机密性相对容易,但对于桌面和移动设备的安装应用程序,这将更加困难。 getHash()方法返回给定密码和盐的哈希值,将它们串联成一个字符串。所使用的算法是SHA-512,返回一个512位的哈希值。由于该方法返回的哈希值对于检查字符串的完整性很有用,因此可以通过仅传递密码或仅传递盐来调用getHash()方法,因为它只是连接两个参数。由于该方法不会在基于密码的加密系统中使用,因此不会进一步讨论它。
方法getSecretKey()从密码的char数组和十六进制编码的盐(由generateSalt()返回)派生出一个密钥。使用的算法是PBKDF1(我认为)来自PKCS5,哈希函数为SHA-256,并返回256位密钥。getSecretKey()通过反复生成密码、盐和计数器的哈希值(最多迭代次数在PBE_ITERATION_COUNT中给出,这里为100),以增加启动暴力攻击所需的时间来生成密钥。盐的长度应至少与正在生成的密钥一样长,即至少256位。迭代次数应尽可能设置得长,而不会导致不合理的延迟。有关密钥派生中盐和迭代次数的更多信息,请参见RFC2898中的第4节。
Java的PBE实现存在缺陷,如果密码包含Unicode字符,即需要超过8位表示的字符。如PBEKeySpec所述,“PKCS#5中定义的PBE机制仅查看每个字符的低8位”。为了解决这个问题,您可以尝试在将其传递给PBEKeySpec之前生成密码中所有16位字符的十六进制字符串(其中仅包含8位字符)。例如,“ABC”变成“004100420043”。还要注意,PBEKeySpec“将密码请求为char数组,因此可以在完成后用clearPassword()覆盖它”。 (关于“保护内存中的字符串”,请参见this question。)但我认为,将盐表示为十六进制编码的字符串没有任何问题。 加密 生成密钥后,我们可以使用它来加密和解密文本。
在实现1中,使用的密码算法是AES/CBC/PKCS5Padding,即在密码块链接(CBC)密码模式下使用AES加密,并使用PKCS#5定义的填充。 (其他AES密码模式包括计数器模式(CTR),电子密码本模式(ECB)和Galois计数器模式(GCM)。Stack Overflow上的另一个问题中包含详细讨论各种AES密码模式及建议使用的密码模式。也要注意,CBC模式加密存在多种攻击方式,其中一些在RFC 7457中提到。)
请注意,您应该使用一种加密模式,该模式还会检查加密数据的完整性(例如,RFC 5116中描述的关联数据认证加密,AEAD)。然而,AES/CBC/PKCS5Padding不提供完整性检查,因此单独使用不建议。为了实现AEAD目的,建议使用至少两倍于普通加密密钥长度的秘密,以避免相关密钥攻击:前半部分用作加密密钥,后半部分用作完整性检查密钥。(也就是说,在这种情况下,从密码和盐生成一个单一的秘密,并将该秘密分成两部分。) Java实现 实现1中的各种功能使用特定提供程序“BC”来进行算法。然而,通常不建议请求特定提供程序,因为并非所有Java实现都可用所有提供程序,无论是由于缺乏支持、避免代码重复还是其他原因。自2018年初Android P预览版发布以来,这个建议变得尤其重要,因为“BC”提供程序的一些功能已在那里被弃用——请参阅Android Developers Blog上的文章“Android P中的加密更改”。另请参见Oracle Providers介绍
因此,`PROVIDER`不应存在,并且字符串`-BC`应从`PBE_ALGORITHM`中删除。实现2在这方面是正确的。

一个方法捕获所有异常是不合适的,应该只处理它可以处理的异常。您问题中提供的实现可能会引发各种已检查的异常。一个方法可以选择仅使用CryptoException包装那些已检查的异常,或在throws子句中指定这些已检查的异常。出于方便考虑,在这里使用CryptoException包装原始异常可能是恰当的,因为类可以引发许多已检查的异常。

Android中的SecureRandom

如Android开发者博客中的文章"Some SecureRandom Thoughts"所述,在2013年之前的Android版本中,java.security.SecureRandom的实现存在缺陷,会降低其提供的随机数的强度。如该文章所述,可以通过缓和此缺陷来解决问题。


感谢关于HMAC和盐的信息。这次我不会使用HMAC,但以后它可能非常有用。总的来说,这是一件好事,毫无疑问。 - caw
非常感谢您所有的编辑以及这篇(现在)精彩的Java AES加密介绍! - caw
1
它应该可以。getInstance有一个重载,只需要算法的名称。例如:Cipher.getInstance() Java实现中可能注册了多个提供程序,包括Bouncy Castle,这种重载会搜索提供程序列表以查找实现给定算法的提供程序之一。你应该尝试并查看。 - Peter O.
1
是的,它将按照Security.getProviders()给定的顺序搜索提供程序 - 尽管现在它还将在init()调用期间检查该密钥是否被该提供程序接受,从而实现硬件辅助加密。更多细节请参见:http://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html。 - Maarten Bodewes
@owlstead:感谢您对双重秘密生成的评论。很抱歉没有早些发表评论,但我的当前答案包括了您的想法(在“加密”一节的最后一段)。请查看编辑历史记录。 - Peter O.
显示剩余3条评论

18

#2绝不能使用,因为它仅使用“AES”(这意味着对文本使用ECB模式加密,大忌),我只会谈论#1。

第一个实现似乎符合加密的最佳实践。常量通常是可以接受的,尽管PBE执行的盐大小和迭代次数都比较短。此外,它似乎是用于AES-256,因为PBE密钥生成使用256作为硬编码值(毕竟那些常量)。它使用CBC和PKCS5Padding,至少你会期望这样。

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

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

问题在于任何包装器始终对用例进行假设。因此说包装器正确或错误是无意义的。这就是为什么我总是尝试避免生成包装器类。但至少它似乎没有显式地出错。


1
看起来不错,但是没有完整性检查和身份验证的想法会让我感到困扰。如果您有足够的空间,我会认真考虑在密文上添加HMAC。话虽如此,由于您可能只是想简单地增加保密性,因此我认为这是一个很大的优势,但并非直接要求。 - Maarten Bodewes
但是,如果意图只是让其他人无法访问加密信息,我不需要HMAC,对吗?如果他们更改密文并强制进行“错误”的解密结果,那么就没有真正的问题了,对吗? - caw
好的,现在我已经完全明白了 :) 再次非常感谢您的帮助!但是我还有两个问题:我需要传递相同的密码和盐来进行解密和加密,对吗?否则我将无法再次解密密文。但是我如何推导出盐呢?我不能为所有加密和解密任务都采用相同的字符串吗?另外:我在公共网络中找不到任何“ICrypto”。所以这很可能是那段代码的作者自己编写的,对吗?如果没有那个表达式,代码会工作吗? - caw
1
如果您想要完整性,为什么不使用Galois/Counter-mode(AES-GCM)中的AES呢? - Kimvais
谢谢,Kimvais,AES-GCM是个好主意!但在我的情况下,不幸的是它不可用。由于我不想存储盐,所以我将创建一个哈希并将其用作盐。那么硬编码盐是否不安全?我不知道如果反编译Java/Android应用程序可以获得多少信息。 - caw
显示剩余6条评论

1

你提出了一个非常有趣的问题。就像所有算法一样,密码密钥是“秘密酱料”,因为一旦公众知道了这个密钥,其他所有东西也就不再是秘密了。所以你可以查看谷歌的文档来寻找解决方法。

security

除了谷歌应用内购买之外,也提供了有关安全性的思考,这也很有见地。

billing_best_practices


谢谢提供这些链接!你所说的“当密码钥匙泄露时,一切都会泄露”的意思是什么? - caw
我的意思是加密密钥需要安全,如果任何人能够获取它,那么你的加密数据就和明文一样不安全。如果您认为我的回答在某种程度上有帮助,请点赞 :-) - the100rabh

0

0

使用BouncyCastle轻量级API。它提供了带有PBE和Salt的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
取决于您是否想包括对抗侧信道攻击的保护。通常,您应该假设自己实现加密算法是相当不安全的。由于Oracle的Java运行时库中提供了AES CBC,因此最好使用它们,并在算法不可用时使用Bouncy Castle库。 - Maarten Bodewes
缺少对“buf”的定义(真心希望它不是“static”字段)。如果输入的字节数是1024的倍数,那么“encrypt()”和“decrypt()”都无法正确地处理最后一个块。 - tc.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接