Android 4.2破坏了我的加密/解密代码,提供的解决方案无效

pan*_*dre 34 android cryptography bouncycastle

首先,我已经看到 Android 4.2 在Android 4.2 和提供的解决方案破坏了我的AES加密/解密代码加密错误:

SecureRandom sr = null;
if (android.os.Build.VERSION.SDK_INT >= JELLY_BEAN_4_2) {
    sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");
} else {
    sr = SecureRandom.getInstance("SHA1PRNG");
}
Run Code Online (Sandbox Code Playgroud)

对我不起作用,因为,当解码Android 4.2中Android <4.2加密的数据时,我得到:

javax.crypto.BadPaddingException: pad block corrupted
at com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineDoFinal(BaseBlockCipher.java:709)
Run Code Online (Sandbox Code Playgroud)

我的代码很简单,直到Android 4.2才开始工作:

public static byte[] encrypt(byte[] data, String seed) throws Exception {

    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");
    secrand.setSeed(seed.getBytes());
    keygen.init(128, secrand);

    SecretKey seckey = keygen.generateKey();
    byte[] rawKey = seckey.getEncoded();

    SecretKeySpec skeySpec = new SecretKeySpec(rawKey, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
    return cipher.doFinal(data);
}

public static byte[] decrypt(byte[] data, String seed) throws Exception {

    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");
    secrand.setSeed(seed.getBytes());
    keygen.init(128, secrand);

    SecretKey seckey = keygen.generateKey();
    byte[] rawKey = seckey.getEncoded();

    SecretKeySpec skeySpec = new SecretKeySpec(rawKey, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, skeySpec);
    return cipher.doFinal(data);
}
Run Code Online (Sandbox Code Playgroud)

我的猜测是默认提供程序并不是Android 4.2中唯一改变的,否则我的代码将与提议的解决方案一起使用.

我的代码基于我很久以前在StackOverflow上找到的一些帖子; 我看到它与上述帖子不同,因为它只是对字节数组进行加密和解密,而其他解决方案则加密和解密字符串(我认为是HEX字符串).

它与种子有关吗?它是否具有最小/最大长度,字符限制等?

任何想法/解决方案?

编辑:经过大量的测试,我发现有两个问题:

  1. 在Android 4.2(API 17)中更改了提供程序 - >这个很容易修复,只需应用我在帖子顶部提到的解决方案

  2. 在Android 2.2(API 8) - > Android2.3(API 9)中,BouncyCastle从1.34变为1.45,因此我之前所说的解密问题与此处描述的相同:升级到1.45时出现BouncyCastle AES错误

所以现在的问题是:有没有办法恢复BouncyCastle 1.34 +中BouncyCastle 1.34中加密的数据?

kro*_*oot 45

首先是免责声明:

不要永远使用SecureRandom派生的关键!这是破碎的,没有意义!

来自问题的以下代码块试图从密码确定性地导出密钥,称为"种子",因为密码用于"种子"随机数生成器.

KeyGenerator keygen = KeyGenerator.getInstance("AES");
SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");
secrand.setSeed(seed.getBytes());
keygen.init(128, secrand);
SecretKey seckey = keygen.generateKey();
Run Code Online (Sandbox Code Playgroud)

然而,该"SHA1PRNG"算法没有很好地定义,并且结果的实现"SHA1PRNG"可能返回不同的甚至完全随机的密钥.


如果您正在从磁盘读取AES密钥,只需存储实际密钥,不要经历这种奇怪的舞蹈.您可以SecretKey通过执行以下操作从字节中获取AES使用情况:

    SecretKey key = new SecretKeySpec(keyBytes, "AES");
Run Code Online (Sandbox Code Playgroud)

如果您使用密码来获取密钥,请遵循 Nelenkov的优秀教程,但需要注意的是,经验法则是盐的大小应与密钥输出的大小相同.

iterationCount(工作因子)当然受到的改变,应立即更换为CPU供电的进展-通常建议不要去低于40到100K为2018年要注意的是PBKDF2只增加了一个固定的时间延迟猜测的密码; 它不是真正弱密码的替代品.

它看起来像这样:

    /* User types in their password: */
    String password = "password";

    /* Store these things on disk used to derive key later: */
    int iterationCount = 1000;
    int saltLength = 32; // bytes; should be the same size as the output (256 / 8 = 32)
    int keyLength = 256; // 256-bits for AES-256, 128-bits for AES-128, etc
    byte[] salt; // Should be of saltLength

    /* When first creating the key, obtain a salt with this: */
    SecureRandom random = new SecureRandom();
    byte[] salt = new byte[saltLength];
    random.nextBytes(salt);

    /* Use this to derive the key from the password: */
    KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt,
                iterationCount, keyLength);
    SecretKeyFactory keyFactory = SecretKeyFactory
                .getInstance("PBKDF2WithHmacSHA1");
    byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
    SecretKey key = new SecretKeySpec(keyBytes, "AES");
Run Code Online (Sandbox Code Playgroud)

而已.你不应该使用的任何其他东西.

  • 支持恢复旧数据的一种方法是从先前版本的Android获取SHA1RPNG(来自libcore)的代码并将其放入应用程序中. (2认同)
  • @LarsH 回答太晚了,但我已经相应地更新了 kroot 的回答。 (2认同)

Gio*_*gio 11

问题是使用新的提供程序,以下代码片段

KeyGenerator keygen = KeyGenerator.getInstance("AES");
SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");
secrand.setSeed(seed.getBytes());
keygen.init(128, secrand);
SecretKey seckey = keygen.generateKey();
byte[] rawKey = seckey.getEncoded();
Run Code Online (Sandbox Code Playgroud)

rawKey每次执行时都会生成一个不同的,真正随机的.因此,您尝试使用与用于加密数据的密钥不同的密钥进行解密,并获得异常.以这种方式生成密钥或数据时,您将无法恢复密钥或数据,并且只保存了种子.

  • 快速解决方法是更改​​SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG"); 使用SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG","Crypto"); (2认同)
  • 正如其他用户所指出的,在 Android 4.2 之后,这将每次生成不同的密钥。对于面向 Android N+ 的应用程序,如果您提供“Crypto”提供程序作为第二个参数,它将抛出异常,如果没有安全提供程序,则可能只为“SHA1PRNG”返回 null。 (2认同)

Pav*_*bik 5

private static final int ITERATION_COUNT = 1000;
private static final int KEY_LENGTH = 256;
private static final String PBKDF2_DERIVATION_ALGORITHM = "PBKDF2WithHmacSHA1";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final int PKCS5_SALT_LENGTH = 32;
private static final String DELIMITER = "]";
private static final SecureRandom random = new SecureRandom();

public static String encrypt(String plaintext, String password) {
    byte[] salt  = generateSalt();
    SecretKey key = deriveKey(password, salt);

    try {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        byte[] iv = generateIv(cipher.getBlockSize());
        IvParameterSpec ivParams = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, ivParams);
        byte[] cipherText = cipher.doFinal(plaintext.getBytes("UTF-8"));

        if(salt != null) {
            return String.format("%s%s%s%s%s",
                    toBase64(salt),
                    DELIMITER,
                    toBase64(iv),
                    DELIMITER,
                    toBase64(cipherText));
        }

        return String.format("%s%s%s",
                toBase64(iv),
                DELIMITER,
                toBase64(cipherText));
    } catch (GeneralSecurityException e) {
        throw new RuntimeException(e);
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
    }
}

public static String decrypt(String ciphertext, String password) {
    String[] fields = ciphertext.split(DELIMITER);
    if(fields.length != 3) {
        throw new IllegalArgumentException("Invalid encypted text format");
    }
    byte[] salt        = fromBase64(fields[0]);
    byte[] iv          = fromBase64(fields[1]);
    byte[] cipherBytes = fromBase64(fields[2]);
    SecretKey key = deriveKey(password, salt);

    try {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        IvParameterSpec ivParams = new IvParameterSpec(iv);
        cipher.init(Cipher.DECRYPT_MODE, key, ivParams);
        byte[] plaintext = cipher.doFinal(cipherBytes);
        return new String(plaintext, "UTF-8");
    } catch (GeneralSecurityException e) {
        throw new RuntimeException(e);
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
    }
}

private static byte[] generateSalt() {
    byte[] b = new byte[PKCS5_SALT_LENGTH];
    random.nextBytes(b);
    return b;
}

private static byte[] generateIv(int length) {
    byte[] b = new byte[length];
    random.nextBytes(b);
    return b;
}

private static SecretKey deriveKey(String password, byte[] salt) {
    try {
        KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH);
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBKDF2_DERIVATION_ALGORITHM);
        byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
        return new SecretKeySpec(keyBytes, "AES");
    } catch (GeneralSecurityException e) {
        throw new RuntimeException(e);
    }
}

private static String toBase64(byte[] bytes) {
    return Base64.encodeToString(bytes, Base64.NO_WRAP);
}

private static byte[] fromBase64(String base64) {
    return Base64.decode(base64, Base64.NO_WRAP);
}
Run Code Online (Sandbox Code Playgroud)

资源


Mar*_*ski 0

我无法回答您提出的问题,但我只是尝试解决这个问题> - 如果您在跨设备/操作系统版本上遇到 bouncycastle 的一些问题,您应该完全放弃内置版本,而是将 bouncycastle 添加为 jar到你的项目,改变你import指向那个jar,重建并假设它一切正常,从现在开始你将不受android内置版本更改的影响。