Android
客户端 (4.2.1) 应用程序通过 HttpPost
请求将公钥发送到 PHP
(5.6) API。此 API 使用符合 AES
的 RIJNDAEL_128
对数据进行加密,然后使用具有 OpenSSL 公共(public)加密和 RSA_PKCS1_OAEP_PADDING
的客户端公钥对 AES 加密的 key 进行加密。它将通过 base64
编码的数据 XML
发送回客户端 android 应用程序,该应用程序将对数据进行加密。我已经设置了一个基本的 PHP 测试脚本来测试整个过程,这按预期工作。
目前我正致力于在客户端 Android 应用程序中实现解密,但已经解密 AES key 失败。除了这个当前的问题,我还有其他问题(见文末)。
这是正在发生的事情的文本图形概要:
client -> public key -> API -> data -> AESencrypt(data), RSAencrypt(AES-key) -> base64encode[AES(data)], base64encode[RSA(AES-key)] -> <xml>base64[AES(data)], base64[RSA(AES-key)]</xml> -> client -> base64[AES(data)], base64[RSA(AES-key)] -> base64decode[AES(data)], base64decode[RSA(AES-key)] -> AESdecrypt(data), RSAdecrypt(AES-key) -> data
我正在使用
MCRYPT_RIJNDAEL_128
加密数据,我读到它与 AES 兼容(请参阅 PHP doc for mycrypt )。这是代码:
<?php
$randomBytes = openssl_random_pseudo_bytes(32, $safe);
$randomKey = bin2hex($randomBytes);
$randomKeyPacked = pack('H*', $randomKey);
// test with fixed key:
// $randomKeyPacked = "12345678901234567890123456789012";
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$dataCrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $randomKeyPacked, $data, MCRYPT_MODE_CBC, $iv);
由此产生的 AES key 使用 openssl_public_encrypt 和填充设置
OPENSSL_PKCS1_OAEP_PADDING
进行编码。阅读源代码( source of PHP OpenSSL implementation )这相当于 RSA_PKCS1_OAEP_PADDING
描述为EME-OAEP as defined in PKCS #1 v2.0 with SHA-1, MGF1 and an empty encoding parameter.
在 OpenSSL 文档中找到 here 。之后我 base64_encode 数据能够通过 XML 字符串传输到客户端。代码如下所示:
openssl_public_encrypt($randomKeyPacked, $cryptionKeyCrypted, $clientPublicKey, OPENSSL_PKCS1_OAEP_PADDING);
$content = array(
'cryptionKeyCryptedBase64' => base64_encode($cryptionKeyCrypted),
'cryptionIVBase64' => base64_encode($iv),
'dataCryptedBase64' => base64_encode($dataCrypted)
);
// $content gets parsed to a valid xml element here
客户端 Android 应用程序通过
HttpPost
请求通过 BasicResponseHandler
获取返回数据。这个返回的 XML 字符串是有效的,并通过 Simple 解析为相应的 java 对象。在保存传输数据的实际内容的类中,我目前尝试解密数据。我使用转换 RSA/ECB/OAEPWithSHA-1AndMGF1Padding
解密 AES key ,由于 this site(只有我能找到)是一个有效的字符串,并且似乎与我在 PHP 中使用的填充等效。我包括了生成私钥的方式,因为它与生成发送到 PHP API 的公钥的方式相同。这是那个类:public class Content {
@Element
private String cryptionKeyCryptedBase64;
@Element
private String cryptionIVBase64;
@Element
private String dataCryptedBase64;
@SuppressLint("TrulyRandom")
public String getData() {
String dataDecrypted = null;
try {
PRNGFixes.apply(); // fix TrulyRandom
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
keygen.initialize(2048);
KeyPair keypair = keygen.generateKeyPair();
PrivateKey privateKey = keypair.getPrivate();
byte[] cryptionKeyCrypted = Base64.decode(cryptionKeyCryptedBase64, Base64.DEFAULT);
//byte[] cryptionIV = Base64.decode(cryptionIVBase64, Base64.DEFAULT);
Cipher cipherRSA = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipherRSA.init(Cipher.DECRYPT_MODE, privateKey);
byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);
byte[] dataCrytped = Base64.decode(dataCryptedBase64, Base64.DEFAULT);
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipherAES = Cipher.getInstance("AES");
cipherAES.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decryptedAESBytes = cipherAES.doFinal(dataCrytped);
dataDecrypted = new String(decryptedAESBytes, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return dataDecrypted;
}
}
这样做我目前在线失败
byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);
Bad padding exceptions
用于几乎所有 PHP openssl_public_encrypt
填充参数 - 我尝试过的 Android 密码转换字符串组合。通过省略默认为 OPENSSL_PKCS1_PADDING
的 openssl_public_encrypt 中的 padding 参数和仅 Cipher.getInstance("RSA")
的密码转换字符串来使用标准 PHP 填充参数,我没有得到错误的填充异常。但是加密 key 似乎无效,因为 AES 解密失败java.security.InvalidKeyException: Key length not 128/192/256 bits.
我尝试使用固定 key 验证它(请参阅上面 PHP 代码中的代码注释),但在解密并将其转换为字符串后,我没有取回相同的 key 。如果我正确读取 Eclipse ADT 调试器,它似乎只是乱码数据,尽管它是 256 位长。
什么可能是正确的 Cipher 转换字符串以用作 PHP 的
OPENSSL_PKCS1_OAEP_PADDING
等价物。读取 this documentation 我需要 "algorithm/mode/padding"
形式的转换字符串,我猜算法 = RSA 但我无法找到如何将 OpenSSL(以上)文档中关于填充的说明转换为有效的密码转换字符串。 IE。例如,mode
是什么?不幸的是,这个 Android RSA decryption (fails) / server-side encryption (openssl_public_encrypt) 接受的答案并没有解决我的问题。
无论如何这可以解决我的问题还是我的问题起源于其他地方?
我将如何进一步调试?将 base64 解码、解密的 key 转换为人类可读形式以便我可以将其与用于加密的 key 进行比较的正确方法是什么?
我试过:
String keyString = new String(keyBytes, "UTF-8");
但这不会返回任何人类可读的文本,因此我假设 key 错误或我的转换方法错误。
同样在 PHP 中解密 AES 加密数据,解密函数 mcrypt_decrypt 中需要 IV。正如您在我发送的代码中看到的那样,但在 Android 中似乎不需要这样做?为什么这样?
PS:我希望我提供了所有需要的信息,我可以在评论中进一步补充。
PPS:为了完整起见,这里是发出 HttpPost 请求的 Android 客户端代码:
@SuppressLint("TrulyRandom")
protected String doInBackground(URI... urls) {
try {
System.setProperty("jsse.enableSNIExtension", "false");
HttpClient httpClient = createHttpClient();
HttpPost httpPost = new HttpPost(urls[0]);
PRNGFixes.apply(); // fix TrulyRandom
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
keygen.initialize(2048);
KeyPair keypair = keygen.generateKeyPair();
PublicKey publickey = keypair.getPublic();
byte[] publicKeyBytes = publickey.getEncoded();
String pubkeystr = "-----BEGIN PUBLIC KEY-----\n"+Base64.encodeToString(publicKeyBytes,
Base64.DEFAULT)+"-----END PUBLIC KEY-----";
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
nameValuePairs.add(new BasicNameValuePair("publickey", pubkeystr));
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
// Execute HTTP Post Request
HttpResponse response = httpClient.execute(httpPost);
return new BasicResponseHandler().handleResponse(response);
} catch (Exception e) {
Toast toast = Toast.makeText(asyncResult.getContext(),
"unknown exception occured: " + e.getMessage(),
Toast.LENGTH_SHORT);
toast.show();
return "error";
}
}
最佳答案
您正在 doInBackground
中生成一个 RSA key 对,并告诉主机使用该 key 对的公共(public)部分来加密 DEK(数据加密 key )。然后,您将在 getData
中生成一个完全不同的 RSA key 对,并尝试使用该 key 对的私有(private)部分来解密加密的 DEK。公钥加密的工作方式是使用 key 对的公共(public)部分进行加密,并使用 相同 key 对 的私有(private)部分进行解密;公共(public)和私有(private)的一半是 数学上相关的 。您需要至少保存和使用您发送的公共(public)部分的 key 对的私有(private)部分(可选地,具有两部分的 key 对)。
正确获得 DEK 后,为了解密 CBC 模式数据, 是的,您确实需要使用与用于加密相同的 IV 来解密 。您的接收者需要将其放入 IvParameterSpec
并在 Cipher.init(direction,key[,params])
调用中传递它。或者,如果您可以更改 PHP,因为您为每条消息使用新的 DEK,使用固定的 IV 是安全的;最简单的方法是使用 '\0'x16
加密并允许 Java 解密默认为全零。
此外,您需要使用参数 Base64.NO_WRAP
设置 Base64.decode 因为 PHP 只会输出由 \0
分隔的 base64。为此,您还需要使用 "AES/CBC/ZeroBytePadding"
转换密码来解密 AES 数据,因为 PHP 函数 mycrypt_encrypt
将用零填充数据。
下面是 getData
函数的样子:
public String getData() {
String dataDecrypted = null;
try {
byte[] cryptionKeyCrypted = Base64.decode(cryptionKeyCryptedBase64, Base64.NO_WRAP);
byte[] cryptionIV = Base64.decode(cryptionIVBase64, Base64.NO_WRAP);
Cipher cipherRSA = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
// get private key from the pair used to grab the public key to send to the api
cipherRSA.init(Cipher.DECRYPT_MODE, rsaKeyPair.getPrivateKey());
byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);
byte[] dataCrytped = Base64.decode(dataCryptedBase64, Base64.NO_WRAP);
IvParameterSpec ivSpec = new IvParameterSpec(cryptionIV);
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipherAES = Cipher.getInstance("AES/CBC/ZeroBytePadding");
cipherAES.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec);
byte[] decryptedAESBytes = cipherAES.doFinal(dataCrytped);
dataDecrypted = new String(decryptedAESBytes, "UTF-8");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return dataDecrypted;
}
关于java - 在android上解密php加密数据,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26068460/