非对称加密差异 - Android 与 Java

Asymmetric encryption discrepancy - Android vs Java

我最近开始编写我在 Java 中编写的在线游戏的 Android 版本。但是,我 运行 与加密不一致。 java 应用程序工作正常 - 它从文件中读取 public 密钥,加密一些文本并将其传递到服务器,在服务器上使用私钥正确解密。在 android 上,一切似乎都正常(并且是 运行 通过相同的代码),但是服务器有一个 BadPaddingException 试图解密消息。我在下面包含了所有相关代码和事件的分步序列:

连接到服务器后发生的第一件事是对称密钥的协议。这是在客户端生成的,因此:

SecretKey symmetricKey = null;
try
{
    KeyGenerator keyGen = KeyGenerator.getInstance("AES");
    symmetricKey = keyGen.generateKey();
}
catch (Throwable t)
{
    Debug.stackTrace(t, "Failed to generate symmetric key.");
}

return symmetricKey;

然后转换为Base64字符串:

byte[] keyBytes = secretKey.getEncoded();
return base64Interface.encode(keyBytes);

并使用 public 密钥加密:

public static String encrypt(String messageString, Key key)
{
    String encryptedString = null;
    try
    {
        byte[] messageBytes = messageString.getBytes();
        String algorithm = key.getAlgorithm()
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] cipherData = cipher.doFinal(messageBytes);
        encryptedString = base64Interface.encode(cipherData);

        //Strip out any newline characters
        encryptedString = encryptedString.replaceAll("\n", "");
        encryptedString = encryptedString.replaceAll("\r", "");
    }
    catch (Throwable t)
    {
        Debug.append("Caught " + t + " trying to encrypt message: " + messageString);
    }

    return encryptedString;
}

以这种形式传递给使用私钥解密消息并恢复SecretKey对象的服务器:

public static String decrypt(String encryptedMessage, Key key)
{
    String messageString = null;
    try
    {
        byte[] cipherData = base64Interface.decode(encryptedMessage);
        String algorithm = key.getAlgorithm();
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] messageBytes = cipher.doFinal(cipherData);
        messageString = new String(messageBytes);
    }
    catch (Throwable t)
    {
        Debug.append("Caught " + t + " trying to decrypt message: " + encryptedMessage, failedDecryptionLogging);
    }

    return messageString;
}

但是,每当我对从 Android 应用程序传递过来的消息执行此操作时,doFinal 行都会产生以下异常:

11/07 12:55:55.975   javax.crypto.BadPaddingException: Decryption error
    at sun.security.rsa.RSAPadding.unpadV15(Unknown Source)
    at sun.security.rsa.RSAPadding.unpad(Unknown Source)
    at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:354)
    at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:380)
    at javax.crypto.Cipher.doFinal(Cipher.java:2121)
    at util.EncryptionUtil.decrypt(EncryptionUtil.java:85)
    at server.MessageHandlerRunnable.handleUnencryptedMessage(MessageHandlerRunnable.java:226)
    at server.MessageHandlerRunnable.getResponse(MessageHandlerRunnable.java:188)
    at server.MessageHandlerRunnable.run(MessageHandlerRunnable.java:85)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)

我的第一个想法是问题必须出在 Base64 encoding/decoding 中,因为这是 Android 和桌面版本之间唯一不同的代码。但是,我做了一些测试并验证了这些是一致的,并且我的服务器代码可以使用任何一种编码方法恢复原始文本。

我的下一个想法是 Android 版本一定是使用了错误的 Public 密钥。这是在启动时使用以下代码从两个平台通用的文件生成的:

public static void generatePublicKey()
{
    InputStream in = null;
    ObjectInputStream oin = null;

    try
    {
        in = KeyGeneratorUtil.class.getResourceAsStream("/assets/public.key");
        oin = new ObjectInputStream(new BufferedInputStream(in));

        BigInteger m = (BigInteger) oin.readObject();
        BigInteger e = (BigInteger) oin.readObject();
        RSAPublicKeySpec keySpec = new RSAPublicKeySpec(m, e);
        KeyFactory fact = KeyFactory.getInstance("RSA");
        MessageUtil.publicKey = fact.generatePublic(keySpec);
    } 
    catch (Throwable e) 
    {
        Debug.stackTrace(e, "Unable to read public key - won't be able to communicate with Server.");
    } 
    finally
    {
        if (in != null)
        {
            try {in.close();} catch (Throwable t) {}
        }

        if (oin != null)
        {
            try {oin.close();} catch (Throwable t) {}
        }
    }
}

当我查看两个平台上的密钥(使用 toString())时,我看到以下内容(我截断了模数):

桌面:

Sun RSA public 密钥,1024 位模数:11920225567195913955197820411061866681846853580... public 指数:65537

Android:

OpenSSLRSAPublic密钥{模数=a9bfe8d8a199fc6a...,public指数=10001}

乍一看它们似乎完全不同,但我现在确信它们等同于一个用十进制表示的(桌面)和另一个用十六进制表示的 (Android)。十六进制的 10001 相当于十进制的 65537,将十六进制模数放入在线转换器会生成一个至少以十进制模数的正确数字开头的数字。那么,为什么我会看到 BadPaddingException?

最后一件事值得注意,这似乎与大约一年前在这个问题中提出的问题相同:

RSA on Android is different from PC

但是,没有提出任何解决方案,我认为值得提出一个新问题,我可以在其中提供我可用的所有信息。

如果您想使用 Public-私钥对执行 RSA encryption/decryption,这里有一个适用于 Android 以及任何 server/desktop Java 节目

它有三种方法:

  1. generateRSAKeyPair - 这会生成 public 私钥对。您必须在应用程序上存储 public 密钥,在您的服务器上存储私钥。
  2. encryptRSA - 此方法使用 public 密钥加密给定数据。它 returns 表示加密值的字节数组。如果你需要,你可以对这个值进行base 64编码,这样你就可以在JSON
  3. 中使用它
  4. decryptRSA - 这使用私钥解密加密值。确保将原始加密值传递给此方法,而不是 base 64 编码值。

.

import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import javax.crypto.Cipher;

public class Test {

    private static final String RSA_ECB_PKCS1_PADDING = "RSA/ECB/PKCS1Padding";

    public static void main(String[] args) {
        String data = "Hello World";

        KeyPair kp = generateRSAKeyPair();

        PublicKey publicKey = kp.getPublic();
        PrivateKey privateKey = kp.getPrivate();

        byte[] encryptedValue = encryptRSA(publicKey, data.getBytes());
        byte[] decrytpedValue = decryptRSA(privateKey, encryptedValue);

        String decryptedData = new String(decrytpedValue);

        System.out.println(decryptedData);
    }

    public static KeyPair generateRSAKeyPair() {
        KeyPairGenerator keyGen;
        try {
            keyGen = KeyPairGenerator.getInstance("RSA");
            SecureRandom rnd = new SecureRandom();
            keyGen.initialize(2048, rnd);
            KeyPair keyPair = keyGen.genKeyPair();
            return keyPair;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static byte[] encryptRSA(Key key, byte[] data) {
        byte[] cipherText = null;
        try {
            final Cipher cipher = Cipher.getInstance(RSA_ECB_PKCS1_PADDING);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            cipherText = cipher.doFinal(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cipherText;
    }

    public static byte[] decryptRSA(Key key, byte[] data) {
        byte[] decryptedText = null;
        try {
            final Cipher cipher = Cipher.getInstance(RSA_ECB_PKCS1_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, key);
            decryptedText = cipher.doFinal(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return decryptedText;
    }

}

BadPaddingExceptions 通常是由以下原因之一引起的:

  1. 传递给解密函数的密文不等于从加密函数接收到的密文(仅仅一个位的差异就会完全破坏这个过程)。

  2. 使用不同的密钥(或不匹配的私钥)解密数据。

  3. 程序尝试使用方案 B.

  4. 取消填充方案 A 填充的消息

既然您已经验证了 1. 和 2. 是正确的,那么 3. 很可能是您问题的根源。

您在两端使用相同编程语言的相同代码,为什么会出现不兼容?罪魁祸首是以下两行代码:

String algorithm = key.getAlgorithm();
Cipher cipher = Cipher.getInstance(algorithm);

由于一个key可以和任何padding scheme结合使用,所以它只保存了它所对应的算法,其效果是key.getAlgorithm() returns "RSA"。调用 Cipher.getInstance("RSA") 通常不是一个好主意,因为 Java 会自动选择 platform dependent 默认值 cipher-mode填充.

这可以通过传递完整的密码字符串 ("<algorithm>/<mode>/<padding>") 来避免,例如 "RSA/ECB/PKCS1Padding"。在使用 JCE 库时,对所有 Cipher 个实例执行此操作始终是个好主意。