36

首先,我已经看到 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");
}

对我不起作用,因为在 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)

我的代码非常简单,并且一直工作到 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);
}

我的猜测是默认提供程序并不是 Android 4.2 中唯一改变的东西,否则我的代码将适用于建议的解决方案。

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

跟种子有关系吗?它是否有最小/最大长度、字符限制等?

任何想法/解决方案?

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

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

  2. BouncyCastle 在 Android 2.2 (API 8)->Android2.3 (API 9) 中从 1.34 更改为 1.45,所以我之前讲的解密问题和这里描述的一样:BouncyCastle AES error when upgrade to 1.45

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

4

7 回答 7

46

首先是免责声明:

永远不要SecureRandom用于派生密钥!这是破碎的,没有意义!

问题中的以下代码块尝试从密码中确定性地派生密钥,称为“种子”,因为密码用于“种子”随机数生成器。

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

但是,该"SHA1PRNG"算法没有很好地定义,结果"SHA1PRNG"可能会返回不同的甚至完全随机的密钥。


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

    SecretKey key = new SecretKeySpec(keyBytes, "AES");

如果您使用密码来派生密钥,请遵循 Nelenkov 的优秀教程,并注意一个好的经验法则是盐大小应该与密钥输出的大小相同。

iterationCount工作系数)当然会发生变化,并且应该随着 CPU 功率的提高而变化——通常建议不要低于 2018 年的 40 到 100K。请注意 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");

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

于 2012-11-18T08:26:19.230 回答
12
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);
}

资源

于 2016-08-17T17:34:22.080 回答
11

The problem is that with the new provider, the following snippet of code

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();

generates a different, genuinely random rawKey every time it's executed. So, you're trying to decrypt with a key different from the one used to encrypt data and you get the exception. You won't be able to recover your key or data when it has been generated this way, and only the seed has been saved.

于 2012-12-07T14:30:45.667 回答
4

为我解决的问题(正如@Giorgio建议的那样)只是替换了这个

SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");

有了这个

SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG", "Crypto");
于 2014-02-06T18:45:33.293 回答
0

因为所有这些都没有帮助我生成一个在所有 android 设备上都是确定性的加密密码(>=2.1),所以我搜索了另一个 AES 实现。我找到了一个适用于所有设备的设备。我不是安全专家,因此如果技术不够安全,请不要对我的回答投反对票。我只是为那些遇到过我以前遇到过的同样问题的人发布代码。

import java.security.GeneralSecurityException;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import android.util.Log;

public class EncodeDecodeAES {


    private static final String TAG_DEBUG = "TAG";
    private IvParameterSpec ivspec;
    private SecretKeySpec keyspec;
    private Cipher cipher;

    private String iv = "fedcba9876543210";//Dummy iv (CHANGE IT!)
    private String SecretKey = "0123456789abcdef";//Dummy secretKey (CHANGE IT!)

    public EncodeDecodeAES() {
        ivspec = new IvParameterSpec(iv.getBytes());

        keyspec = new SecretKeySpec(SecretKey.getBytes(), "AES");

        try {
            cipher = Cipher.getInstance("AES/CBC/NoPadding");
        } catch (GeneralSecurityException e) {
            Log.d(TAG_DEBUG, e.getMessage());
        }
    }

    public byte[] encrypt(String text) throws Exception {
        if (text == null || text.length() == 0)
            throw new Exception("Empty string");

        byte[] encrypted = null;

        try {
            cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);

            encrypted = cipher.doFinal(padString(text).getBytes());
        } catch (Exception e) {
            Log.d(TAG_DEBUG, e.getMessage());
            throw new Exception("[encrypt] " + e.getMessage());
        }

        return encrypted;
    }

    public byte[] decrypt(String code) throws Exception {
        if (code == null || code.length() == 0)
            throw new Exception("Empty string");

        byte[] decrypted = null;

        try {
            cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);

            decrypted = cipher.doFinal(hexToBytes(code));
        } catch (Exception e) {
            Log.d(TAG_DEBUG, e.getMessage());
            throw new Exception("[decrypt] " + e.getMessage());
        }
        return decrypted;
    }

    public static String bytesToHex(byte[] data) {
        if (data == null) {
            return null;
        }

        int len = data.length;
        String str = "";
        for (int i = 0; i < len; i++) {
            if ((data[i] & 0xFF) < 16)
                str = str + "0" + java.lang.Integer.toHexString(data[i] & 0xFF);
            else
                str = str + java.lang.Integer.toHexString(data[i] & 0xFF);
        }
        return str;
    }

    public static byte[] hexToBytes(String str) {
        if (str == null) {
            return null;
        } else if (str.length() < 2) {
            return null;
        } else {
            int len = str.length() / 2;
            byte[] buffer = new byte[len];
            for (int i = 0; i < len; i++) {
                buffer[i] = (byte) Integer.parseInt(str.substring(i * 2, i * 2 + 2), 16);
            }
            return buffer;
        }
    }

    private static String padString(String source) {
        char paddingChar = ' ';
        int size = 16;
        int x = source.length() % size;
        int padLength = size - x;

        for (int i = 0; i < padLength; i++) {
            source += paddingChar;
        }

        return source;
    }
}

你可以像这样使用它:

EncodeDecodeAES aes = new EncodeDecodeAES ();
/* Encrypt */
String encrypted = EncodeDecodeAES.bytesToHex(aes.encrypt("Text to Encrypt"));
/* Decrypt */
String decrypted = new String(aes.decrypt(encrypted));

来源:这里

于 2013-08-21T13:04:17.540 回答
0

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

于 2012-11-17T18:39:59.553 回答
-1

它确实与种子有关,它也应该使用 8 的倍数(如 8、16、24 或 32),尝试用 A 和 B 或 1 和 0 完成种子(必须是这样的 ABAB.. ..,因为 AAA.. 或 BBB.. 也不起作用。)最多达到 8 的倍数。如果您仅读取和加密字节,还有另一件事(没有像我那样将其转换为 Char64),那么您需要适当的 PKCS5 或 PKCS7 填充,但是在您的情况下(由于只有 128 位,并且它是用较旧的Android 版本)PKCS5 就足够了,尽管你也应该把它放在你的 SecreteKeySpec 中,比如“AES/CBC/PKCS5Padding”“AES/ECB/PKCS5Padding”而不仅仅是“AES”,因为 Android 4.2 它使用 PKCS7Padding 作为默认值,如果它只是字节,你真的需要与之前默认的相同算法。尝试使用低于 4.2 的 Android 设备检查“ keygen.init(128, secrand); ”上的对象树,如果我没记错的话,它有标签cipher,然后使用它。试试看。

于 2012-11-18T12:23:10.677 回答