在 C# 中解密使用 RSA 在 iPhone 上加密的内容时遇到问题

发布于 2024-07-26 21:47:31 字数 4779 浏览 4 评论 0原文

到目前为止,我已经花了两天时间研究这个问题,并梳理了我可以使用的所有资源,所以这是最后的手段。

我有一个 X509 证书,其公钥已存储在 iPhone 的钥匙串中(此时仅限模拟器)。 在 ASP.NET 方面,我已在证书存储区中使用私钥获取了证书。 当我在 iPhone 上加密字符串并在服务器上解密时,我收到 CryptographicException“错误数据”。 我尝试了 RSACryptoServiceProvider 页面上的一个长镜头,但它没有帮助。

我比较了两边的 base-64 字符串,它们是相等的。 我比较了解码后的原始字节数组,它们也相等。 如果我使用公钥在服务器上加密,则字节数组与 iPhone 的版本不同,并且很容易使用私钥解密。 原始明文字符串有 115 个字符,因此它在我的 2048 位密钥的 256 字节限制之内。

这是 iPhone 加密方法(几乎逐字来自 CryptoExercise 示例应用 的 wrapSymmetricKey 方法):

+ (NSData *)encrypt:(NSString *)plainText usingKey:(SecKeyRef)key error:(NSError **)err
{
    size_t cipherBufferSize = SecKeyGetBlockSize(key);
    uint8_t *cipherBuffer = NULL;
    cipherBuffer = malloc(cipherBufferSize * sizeof(uint8_t));
    memset((void *)cipherBuffer, 0x0, cipherBufferSize);
    NSData *plainTextBytes = [plainText dataUsingEncoding:NSUTF8StringEncoding];
    OSStatus status = SecKeyEncrypt(key, kSecPaddingNone,
                                (const uint8_t *)[plainTextBytes bytes], 
                                [plainTextBytes length], cipherBuffer, 
                                &cipherBufferSize);
    if (status == noErr)
    {
        NSData *encryptedBytes = [[[NSData alloc]
                    initWithBytes:(const void *)cipherBuffer 
                    length:cipherBufferSize] autorelease];
        if (cipherBuffer)
        {
            free(cipherBuffer);
        }
        NSLog(@"Encrypted text (%d bytes): %@",
                    [encryptedBytes length], [encryptedBytes description]);
        return encryptedBytes;
    }
    else
    {
        *err = [NSError errorWithDomain:@"errorDomain" code:status userInfo:nil];
        NSLog(@"encrypt:usingKey: Error: %d", status);
        return nil;
    }
}

这是服务器端 C# 解密方法:

private string Decrypt(string cipherText)
{
    if (clientCert == null)
    {
        // Get certificate
        var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        store.Open(OpenFlags.ReadOnly);
        foreach (var certificate in store.Certificates)
        {
            if (certificate.GetNameInfo(X509NameType.SimpleName, false) == CERT)
            {
                clientCert = certificate;
                break;
            }
        }
    }

    using (var rsa = (RSACryptoServiceProvider)clientCert.PrivateKey)
    {
        try
        {
            var encryptedBytes = Convert.FromBase64String(cipherText);
            var decryptedBytes = rsa.Decrypt(encryptedBytes, false);
            var plaintext = Encoding.UTF8.GetString(decryptedBytes);
            return plaintext;
        }
        catch (CryptographicException e)
        {
            throw(new ApplicationException("Unable to decrypt payload.", e));
        }
    }
}

我怀疑平台之间存在一些编码问题。 我知道一个是大尾数,另一个是小尾数,但我不太了解哪个是哪个,或者如何克服差异。 Mac OS X、Windows 和 iPhone都是小端字节序,所以这不是问题。

新理论:如果将 OAEP 填充布尔值设置为 false,则默认为 PKCS#1 1.5 填充。 SecKey 仅具有 PKCS1PKCS1MD2PKCS1MD5SecPadding 定义>PKCS1SHA1。 也许 Microsoft 的 PKCS#1 1.5 != Apple 的 PKCS1,因此填充会影响加密的二进制输出。 我尝试使用 kSecPaddingPKCS1 并将 fOAEP 设置为 false 但它仍然不起作用。 显然,kSecPaddingPKCS1< /code> 相当于 PKCS#1 1.5 。 回到理论的绘图板...

其他新尝试的理论:

  1. iPhone 上的证书(.cer 文件)与服务器上的 PKCS#12 捆绑包(.pfx 文件)并不完全相同,因此它永远无法工作。 在不同的证书存储中安装了 .cer 文件,并且服务器加密的字符串往返就好了;
  2. 转换为 base-64 以及 POST 到服务器的行为会导致同一类往返中不存在的奇怪情况,因此我首先尝试了一些 URLEncoding/Decoding,然后从 iPhone 发布原始二进制文件,验证它是否相等,并得到相同的错误数据;
  3. 我的原始字符串是 125 字节,所以我认为它可能会在 UTF-8 中被截断(长镜头),所以我将其裁剪为 44 字节字符串,但没有结果;
  4. 回顾 System.Cryptography 库,以确保我使用了合适的类,并发现了“RSAPKCS1KeyExchangeDeformatter”,对新的前景感到高兴,但当它的行为完全相同时感到沮丧。

成功!

事实证明,我在 iPhone 模拟器上的钥匙串中存在一些问题,可以这么说,这些问题使水变得浑浊。 我删除了 ~/Library/Application Support/iPhone Simulator/User/Library/Keychains/keychain-2-debug.db 处的钥匙串数据库,以使其重新创建并且工作正常。 感谢您的所有帮助。 我认为这可能是一些简单但不明显的事情。 (我学到了两件事:1)从模拟器中卸载应用程序不会清除其钥匙串条目,2)定期重新启动。)

注意:钥匙串文件的通用路径取决于 iOS 版本: 〜/库/应用程序支持/iPhone模拟器/[版本]/Library/Keychains/keychain-2-debug.db 例如, 〜/库/应用程序支持/iPhone模拟器/4.3/Library/Keychains/keychain-2-debug.db

I've spent two days on this so far and combed through every source at my disposal, so this is the last resort.

I have an X509 certificate whose public key I have stored in the iPhone's keychain (simulator only at this point). On the ASP.NET side, I've got the certificate in the cert store with a private key. When I encrypt a string on the iPhone and decrypt it on the server, I get a CryptographicException "Bad data." I tried the Array.Reverse suggested in the RSACryptoServiceProvider page on a longshot, but it did not help.

I have compared the base-64 strings on both sides and they're equal. I've compared the raw byte arrays after decoding and they too are equal. If I encrypt on the server using the public key, the byte array is different from the iPhone's version and readily decrypts using the private key. The raw plaintext string is 115 characters so it's within the 256-byte limitation of my 2048-bit key.

Here's the iPhone encryption method (pretty much verbatim from the CryptoExercise sample app's wrapSymmetricKey method):

+ (NSData *)encrypt:(NSString *)plainText usingKey:(SecKeyRef)key error:(NSError **)err
{
    size_t cipherBufferSize = SecKeyGetBlockSize(key);
    uint8_t *cipherBuffer = NULL;
    cipherBuffer = malloc(cipherBufferSize * sizeof(uint8_t));
    memset((void *)cipherBuffer, 0x0, cipherBufferSize);
    NSData *plainTextBytes = [plainText dataUsingEncoding:NSUTF8StringEncoding];
    OSStatus status = SecKeyEncrypt(key, kSecPaddingNone,
                                (const uint8_t *)[plainTextBytes bytes], 
                                [plainTextBytes length], cipherBuffer, 
                                &cipherBufferSize);
    if (status == noErr)
    {
        NSData *encryptedBytes = [[[NSData alloc]
                    initWithBytes:(const void *)cipherBuffer 
                    length:cipherBufferSize] autorelease];
        if (cipherBuffer)
        {
            free(cipherBuffer);
        }
        NSLog(@"Encrypted text (%d bytes): %@",
                    [encryptedBytes length], [encryptedBytes description]);
        return encryptedBytes;
    }
    else
    {
        *err = [NSError errorWithDomain:@"errorDomain" code:status userInfo:nil];
        NSLog(@"encrypt:usingKey: Error: %d", status);
        return nil;
    }
}

And here's the server-side C# decryption method:

private string Decrypt(string cipherText)
{
    if (clientCert == null)
    {
        // Get certificate
        var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        store.Open(OpenFlags.ReadOnly);
        foreach (var certificate in store.Certificates)
        {
            if (certificate.GetNameInfo(X509NameType.SimpleName, false) == CERT)
            {
                clientCert = certificate;
                break;
            }
        }
    }

    using (var rsa = (RSACryptoServiceProvider)clientCert.PrivateKey)
    {
        try
        {
            var encryptedBytes = Convert.FromBase64String(cipherText);
            var decryptedBytes = rsa.Decrypt(encryptedBytes, false);
            var plaintext = Encoding.UTF8.GetString(decryptedBytes);
            return plaintext;
        }
        catch (CryptographicException e)
        {
            throw(new ApplicationException("Unable to decrypt payload.", e));
        }
    }
}

My suspicion was that there was some encoding problems between the platforms. I know that one is big-endian and the other is little-endian but I don't know enough to say which is which or how to overcome the difference. Mac OS X, Windows, and the iPhone are all little-endian so that's not the problem.

New theory: if you set the OAEP padding Boolean to false, it defaults to PKCS#1 1.5 padding. SecKey only has SecPadding definitions of PKCS1, PKCS1MD2, PKCS1MD5, and PKCS1SHA1. Perhaps Microsoft's PKCS#1 1.5 != Apple's PKCS1 and so the padding is affecting the binary output of the encryption. I tried using kSecPaddingPKCS1 with the fOAEP set to falseand it still didn't work. Apparently, kSecPaddingPKCS1 is equivalent to PKCS#1 1.5. Back to the drawing board on theories…

Other newly-tried theories:

  1. Certificate on iPhone (.cer file) is not exactly the same as the PKCS#12 bundle on the server (.pfx file) and so it could never work. Installed .cer file in different cert store and server-encrypted string roundtripped just fine;
  2. Conversion to base-64 and act of POSTing to server resulted in oddness that wasn't present in same class roundtrip so I first tried some URLEncoding/Decoding and then posted raw binary from iPhone, verified that it was equal, and got same bad data;
  3. My original string was 125 bytes so I thought it might be truncating in UTF-8 (long shot) so I cropped it down to a 44-byte string with no result;
  4. Looked back over the System.Cryptography library to make sure I was using an appropriate class and discovered `RSAPKCS1KeyExchangeDeformatter`, became elated at new prospects, and dejected when it behaved exactly the same.

Success!

It turned out that I had some cruft in my Keychain on the iPhone Simulator that was muddying the waters, so to speak. I deleted the Keychain DB at ~/Library/Application Support/iPhone Simulator/User/Library/Keychains/keychain-2-debug.db to cause it to be re-created and it worked fine. Thank you for all of your help. Figures it would have been something simple but non-obvious. (Two things I learned: 1) uninstalling the app from the simulator does not clear its Keychain entries and 2) start absolutely fresh periodically.)

NOTE: The generic path for the keychain file is dependent on the iOS version:
~/Library/Application Support/iPhone Simulator/[version]/Library/Keychains/keychain-2-debug.db
e.g.,
~/Library/Application Support/iPhone Simulator/4.3/Library/Keychains/keychain-2-debug.db

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

陈甜 2024-08-02 21:47:31

嗯...第一步(正如您所说的那样)是使用 iPhone 和 C# 实现使用相同的初始化向量来加密相同的消息。 您应该得到相同的输出。 你说你没有,所以有问题。

这意味着:

  • RSA 的 iPhone 实现不正确。
  • RSA 的 .NET 实现不正确。
  • 密钥文件不同(或解释不同)。

我建议前两种可能性不大,但可能性很小。

您声明:“在不同的证书存储中安装了 .cer 文件,并且服务器加密的字符串往返得很好”...这并不能证明任何事情:所有这些证明的是,给定一组特定的随机数字,您可以成功加密/解密一个平台。 您不能保证两个平台都看到相同的随机数集。

所以我建议你把它降到尽可能低的水平。 检查两个平台上加密的直接(字节数组)输入和输出。 如果使用完全相同的(二进制)输入,您没有得到相同的输出,那么您就会遇到平台问题。 我认为这不太可能,所以我猜你会发现 IV 的解释不同。

Well... the first step (as you say you have done) is to encrypt the same messages with the same initialization vectors using both the iPhone and the C# implementation. You should get the same output. You said you didn't, so there is a problem.

This means either:

  • The iPhone implementation of RSA is incorrect.
  • The .NET implementation of RSA is incorrect.
  • The key files are different (or being interpreted differently).

I would suggest the first two are unlikely, however they are remotely possible.

You state: "Installed .cer file in different cert store and server-encrypted string roundtripped just fine"... this doesn't prove anything: all this proves is that given a particular random set of numbers you can encrypt/decrypt successfully on one platform. You are not guaranteeing that both platforms are seeing the same set of random numbers.

So I suggest you take it down to the lowest level possible here. Inspect the direct (byte array) inputs and outputs of the encryption on both platforms. If with the exact same (binary) inputs you don't get the same output, then you have a platform problem. I think this is unlikely, so I'm guessing you will find that the IVs are being interpreted differently.

娜些时光,永不杰束 2024-08-02 21:47:31

这是我在stackoverflow上的第一个回答,如果我做错了,请原谅我!

我无法给你一个完整的答案,但是当我尝试与 PHP 集成时,我遇到了非常相似的问题 - 似乎 Apple 的证书文件的格式与其他软件期望的格式(包括 openssl)有点不同。

以下是我在 PHP 中解密加密签名的方法 - 我实际上从传输的公钥中手动提取模数和 PK 并将其用于 RSA 内容,而不是尝试导入密钥:

// Public key format in hex (2 hex chars = 1 byte):
//30480241009b63495644db055437602b983f9a9e63d9af2540653ee91828483c7e302348760994e88097d223b048e42f561046c602405683524f00b4cd3eec7e67259c47e90203010001
//<IGNORE><--------------------------------------------- MODULUS --------------------------------------------------------------------------><??>< PK > 
// We're interested in the modulus and the public key.
// PK = Public key, probably 65537

// First, generate the sha1 of the hash string:
$sha1 = sha1($hashString,true);

// Unencode the user's public Key:
$pkstr = base64_decode($publicKey);
// Skip the <IGNORE> section:
$a = 4;
// Find the very last occurrence of \x02\x03 which seperates the modulus from the PK:
$d = strrpos($pkstr,"\x02\x03");
// If something went wrong, give up:
if ($a == false || $d == false) return false;
// Extract the modulus and public key:
$modulus = substr($pkstr,$a,($d-$a));
$pk = substr($pkstr,$d+2);

// 1) Take the $signature from the user
// 2) Decode it from base64 to binary
// 3) Convert the binary $pk and $modulus into (very large!) integers (stored in strings in PHP)
// 4) Run rsa_verify, from http://www.edsko.net/misc/rsa.php
$unencoded_signature = rsa_verify(base64_decode($signature), binary_to_number($pk), binary_to_number($modulus), "512");

//Finally, does the $sha1 we calculated match the $unencoded_signature (less any padding bytes on the end)?
return ($sha1 == substr($unencoded_signature,-20)); // SHA1 is only 20 bytes, whilst signature is longer than this.  

生成此公钥的 Objective-c 是:

NSData * data = [[SecKeyWrapper sharedWrapper] getPublicKeyBits];
[req addValue:[data base64Encoding] forHTTPHeaderField: @"X-Public-Key"];
data = [[SecKeyWrapper sharedWrapper] getSignatureBytes:[signatureData dataUsingEncoding:NSUTF8StringEncoding]];
[req addValue:[data base64Encoding] forHTTPHeaderField: @"X-Signature"];

使用 Apple 示例项目 CryptoExercise 中的 SecKeyWrapper(您可以在此处查看该文件: https ://developer.apple.com/iphone/library/samplecode/CryptoExercise/listing15.html

我希望这有帮助吗?

this is my first answer on stackoverflow, so please forgive me if I do it wrong!

I can't give you a complete answer, however I had very similar issues when I tried to integrate with PHP - it seems that the format of Apple's certificate files is a little different from that which other software expects (including openssl).

Here's how I decrypt an encrypted signature in PHP - I actually extract the modulus and PK from the transmitted public key manually and use that for the RSA stuff, rather than trying to import the key:

// Public key format in hex (2 hex chars = 1 byte):
//30480241009b63495644db055437602b983f9a9e63d9af2540653ee91828483c7e302348760994e88097d223b048e42f561046c602405683524f00b4cd3eec7e67259c47e90203010001
//<IGNORE><--------------------------------------------- MODULUS --------------------------------------------------------------------------><??>< PK > 
// We're interested in the modulus and the public key.
// PK = Public key, probably 65537

// First, generate the sha1 of the hash string:
$sha1 = sha1($hashString,true);

// Unencode the user's public Key:
$pkstr = base64_decode($publicKey);
// Skip the <IGNORE> section:
$a = 4;
// Find the very last occurrence of \x02\x03 which seperates the modulus from the PK:
$d = strrpos($pkstr,"\x02\x03");
// If something went wrong, give up:
if ($a == false || $d == false) return false;
// Extract the modulus and public key:
$modulus = substr($pkstr,$a,($d-$a));
$pk = substr($pkstr,$d+2);

// 1) Take the $signature from the user
// 2) Decode it from base64 to binary
// 3) Convert the binary $pk and $modulus into (very large!) integers (stored in strings in PHP)
// 4) Run rsa_verify, from http://www.edsko.net/misc/rsa.php
$unencoded_signature = rsa_verify(base64_decode($signature), binary_to_number($pk), binary_to_number($modulus), "512");

//Finally, does the $sha1 we calculated match the $unencoded_signature (less any padding bytes on the end)?
return ($sha1 == substr($unencoded_signature,-20)); // SHA1 is only 20 bytes, whilst signature is longer than this.  

The objective-c that generates this public key is:

NSData * data = [[SecKeyWrapper sharedWrapper] getPublicKeyBits];
[req addValue:[data base64Encoding] forHTTPHeaderField: @"X-Public-Key"];
data = [[SecKeyWrapper sharedWrapper] getSignatureBytes:[signatureData dataUsingEncoding:NSUTF8StringEncoding]];
[req addValue:[data base64Encoding] forHTTPHeaderField: @"X-Signature"];

Using SecKeyWrapper from Apple's example project CryptoExercise (you can view the file here: https://developer.apple.com/iphone/library/samplecode/CryptoExercise/listing15.html)

I hope this helps?

疏忽 2024-08-02 21:47:31

这对你有帮助吗?

使用 .NET 和 .NET 的非对称密钥加密 C#

  • 很抱歉这篇文章很短,时间有限等等。 不管怎样,看到了你的 Twitter 请求帮助。这展示了我如何使用 PHP 完成此操作并在 .NET 上解密,类似。 我注意到您的解密类与我的略有不同,因此本文可能会有所帮助。

Will this help you ?

Asymmetric Key Encryption w/ .NET & C#

  • Sorry for the short post, time constraints and all. Anyway, saw your Twitter request for help.. this shows how I did this with PHP and decrypted on .NET, simliar. I notice your decrypt class is slightly diff than mine, so this article might help.
月下凄凉 2024-08-02 21:47:31

我相信你自己已经回答了这个问题。 问题肯定出在字节顺序上。

这是编写双向转换方法的一种可能方法:

short convert_short(short in)
{
 short out;
 char *p_in = (char *) ∈
 char *p_out = (char *) &out;
 p_out[0] = p_in[1];
 p_out[1] = p_in[0];  
 return out;
}

long convert_long(long in)
{
 long out;
 char *p_in = (char *) ∈
 char *p_out = (char *) &out;
 p_out[0] = p_in[3];
 p_out[1] = p_in[2];
 p_out[2] = p_in[1];
 p_out[3] = p_in[0];  
 return out;
} 

这对您来说可能是一个很好的资源(维基百科除外):http://betterexplained.com/articles/understanding-big-and-little-endian-byte-order/

I believe you've answered the question yourself. The problem most certainly lies within the endianness.

This is a possible way of writing two-way conversion methods:

short convert_short(short in)
{
 short out;
 char *p_in = (char *) ∈
 char *p_out = (char *) &out;
 p_out[0] = p_in[1];
 p_out[1] = p_in[0];  
 return out;
}

long convert_long(long in)
{
 long out;
 char *p_in = (char *) ∈
 char *p_out = (char *) &out;
 p_out[0] = p_in[3];
 p_out[1] = p_in[2];
 p_out[2] = p_in[1];
 p_out[3] = p_in[0];  
 return out;
} 

This might be a good resource for you (other than wikipedia): http://betterexplained.com/articles/understanding-big-and-little-endian-byte-order/

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文