iOS 和 Python 之间的 AES 加密

AES encryption between iOS and Python

我在 iOS (CCCrypt) 和 python (pycryptdome) 上都具有使用 AES(128 和 256)encrypt/decrypt 的功能。所有测试用例都在每个平台上工作,但是......当我使用 AES 密钥和从 iOS 到 python 的加密字符串时,解密失败。我已经广泛查看并尝试了各种用例都无济于事。

我在这里创建了一个简单的测试用例,其中包含 iOS 加密和 python 解密,希望有人能告诉我我在平台上所做的不同之处。

iOS代码 测试用例

    NSString *test_aes = @"XSmTe1Eyw8JsZkreIFUpNi7BhKEReHTP";
    NSString *test_string = @"This is a test string";
    
    NSData *clearPayload = [test_string dataUsingEncoding:NSUTF8StringEncoding];
    NSData *encPayload = nil;
    char keyPtr[kCCKeySizeAES256 + 1]; // room for terminator (unused)
    bzero( keyPtr, sizeof( keyPtr ) ); // fill with zeroes (for padding)
    // fetch key data
    [test_aes getCString:keyPtr maxLength:sizeof( keyPtr ) encoding:NSUTF8StringEncoding];
    NSUInteger dataLength = clearPayload.length;
    size_t bufferSize = dataLength + kCCKeySizeAES256;
    void *buffer = malloc( bufferSize );
    
    size_t numBytesEncrypted = 0;
    CCCryptorStatus cryptStatus = CCCrypt( kCCEncrypt, kCCAlgorithmAES, kCCOptionPKCS7Padding,
    keyPtr, kCCKeySizeAES256,
    NULL /* initialization vector (optional) */,
    [clearPayload bytes], dataLength, /* input */
    buffer, bufferSize, /* output */
    &numBytesEncrypted );
    NSString *encString = @"Error";
    if( cryptStatus == kCCSuccess )
    {
       //the returned NSData takes ownership of the buffer and will free it on deallocation
       encPayload = [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
        encString = [encPayload base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    }
    //free( buffer ); //free the buffer
    
    NSLog(@"Src = %@ AES = %@ String = %@",test_string, test_aes, encString);
    
    encPayload = [[NSData alloc] initWithBase64EncodedString:encString options:NSDataBase64DecodingIgnoreUnknownCharacters];
    clearPayload = nil;
        
    char keyPtr2[kCCKeySizeAES256+1]; // room for terminator (unused)
    bzero( keyPtr2, sizeof( keyPtr2 ) ); // fill with zeroes (for padding)
    // fetch key data
    [test_aes getCString:keyPtr2 maxLength:sizeof( keyPtr2 ) encoding:NSUTF8StringEncoding];
    
    NSUInteger dataLength2 = [encPayload length];
    
    //See the doc: For block ciphers, the output size will always be less than or
    //equal to the input size plus the size of one block.
    //That's why we need to add the size of one block here
    
    size_t bufferSize2 = dataLength2 + kCCKeySizeAES256;
    void *buffer2 = malloc( bufferSize2 );
    size_t numBytesDecrypted = 0;
    CCCryptorStatus cryptStatus2 = CCCrypt( kCCDecrypt, kCCAlgorithmAES, kCCOptionPKCS7Padding,
       keyPtr, kCCKeySizeAES256,
       NULL /* initialization vector (optional) */,
       [encPayload bytes], dataLength2, /* input */
       buffer2, bufferSize2, /* output */
       &numBytesDecrypted );
    NSString *clearString = @"Error";
    if( cryptStatus2 == kCCSuccess )
    {
       //the returned NSData takes ownership of the buffer and will free it on deallocation
        clearPayload = [NSData dataWithBytesNoCopy:buffer2 length:numBytesDecrypted];
        clearString = [[NSString alloc] initWithData:clearPayload encoding:NSUTF8StringEncoding];
    }
    NSLog(@"Res = %@",clearString);

这段代码中的加密和解密工作正常,输出为:

Src = This is a test string 
AES = XSmTe1Eyw8JsZkreIFUpNi7BhKEReHTP 
String = hUbjWyXX4mB01gI0RJhYQRD0iAjQnkGTpsnKcmDpvaQ=
Res = This is a test string

当我将编码字符串和 aes 密钥带到 python 以使用此代码进行测试时:

    key = "XSmTe1Eyw8JsZkreIFUpNi7BhKEReHTP"
    data = "hUbjWyXX4mB01gI0RJhYQRD0iAjQnkGTpsnKcmDpvaQ="
    usekey = key
    useData = data
    if isinstance(key, str):
        usekey = key.encode('utf-8')
    cipher = AES.new(usekey, AES.MODE_GCM, nonce=self.nonce)
    print("nonce", cipher.nonce)
    if isinstance(data, str):
        useData = data.encode('utf-8')
        useData = b64decode(useData)
    puseData = useData # unpad(useData,32)
    print("decrypt:In bytes=", puseData)
    result = cipher.decrypt(puseData)
    print ("decrypt:Out bytes=",result)

解密失败,输出为

nonce b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
decrypt:In bytes= b'\x85F\xe3[%\xd7\xe2`t\xd6\x024D\x98XA\x10\xf4\x88\x08\xd0\x9eA\x93\xa6\xc9\xcar`\xe9\xbd\xa4'
decrypt:Out bytes= b'\x08\xc58\x962q\x94\xff#\xfa\xab\xe2\xc8{b\xed\x0b\xedw\x8f\xe3\xec\x0b\x8e\xfb\xcc\x12\x7f\x9e\xb4\x8f\xd6'

以上两个例程都可以毫无问题地处理本地加密数据,我已经破解了这里的示例(包括不释放 malloc 缓冲区 :) 用于调试目的,所以我为有点脏的代码道歉。

注意:我曾尝试将 python 模式更改为 AES.MODE_CBC(并添加了填充代码),当我看到注释 iOS 可能会使用它而不是 GCM,这也失败了... 现在我将 nonce / iv 保留为 0 的数组,因为我被告知 iOS 将使用它作为默认值,因为没有提供 CCCrypt,当这个例子工作时我将过渡到指定iv.

如有任何指示,我将不胜感激。

编辑: 我继续并在 iOS 侧指定了一个空 IV

    char iv[16]; // also tried 17
    bzero( iv, sizeof( iv ) );

行为完全没有变化...

编辑: 我在两个系统上将 IV 设置为所有字符“1”并得到相同的结果。 iOS 添加代码:

    NSString *hardCodeIV = @"1111111111111111";
    char iv[17];
    bzero( iv, sizeof( iv ) );
    [hardCodeIV getCString:iv maxLength:sizeof(iv) encoding:NSUTF8StringEncoding];

产生了

Src = This is a test string 
AES = XSmTe1Eyw8JsZkreIFUpNi7BhKEReHTP 
String = sFoZ24VRN1hyMzegXT+GFzAn/YGPvaKO8p1eD+xhGaU=
Res = This is a test string

因此 iOS 它使用字节 0 和字符 1 IV 正确加密和解​​密....

并且 python 代码在使用 IV 在本地加密和解密时也能正常工作...但是当 iOS 加密的输出用于 python 解密时失败如此处所示。

将密钥和加密消息移动到 python 以解密为:

        key = "XSmTe1Eyw8JsZkreIFUpNi7BhKEReHTP"
        data = "sFoZ24VRN1hyMzegXT+GFzAn/YGPvaKO8p1eD+xhGaU="
        usekey = key
        useData = data
        if isinstance(key, str):
            usekey = key.encode('utf-8')
        cipher = AES.new(usekey, AES.MODE_GCM, nonce=self.nonce)
        print("nonce", cipher.nonce)
        if isinstance(data, str):
            useData = data.encode('utf-8')
            useData = b64decode(useData)
        puseData = useData # unpad(useData,32)
        print("decrypt:In bytes=", puseData)
        result = cipher.decrypt(puseData)
        print ("decrypt:Out bytes=",result)

结果:

    nonce b'1111111111111111'
    decrypt:In bytes= b"\xb0Z\x19\xdb\x85Q7Xr37\xa0]?\x86\x170'\xfd\x81\x8f\xbd\xa2\x8e\xf2\x9d^\x0f\xeca\x19\xa5"
    decrypt:Out bytes= b'\xc3\x1e"w\x86:~\x86\xd3\xc9H3\xd3\xd3y)|,|\xe02(\xc6\x17\xa3\x1e\xe2\x0f\x1a#\xbbW'

所以,还是不开心...

看起来很像是算法选择的问题,但 iOS 上的选项似乎只有 GCM 或 CBC,GCM 是默认值...大多数测试都是在 GCM 上完成的。我试图在一项测试中使用 CBC(没有 IV,因为它不需要 IV),以防 iOS 实际使用它而不告诉我,但如上所示,也没有成功。

我正在继续测试方法,但确实可以使用已经完成这项工作的人的一些建议 - 我无法找到工作示例。 [作为旁注,RSA 模型工作正常 - 这就是我移动 AES 密钥的方式 - 解决方案的那部分目前是完美无缺的,这是我需要开始操作的最后一点)。

编辑为欧洲央行和加拿大央行在 iOS 和 Python 之间工作的最终答案:

感谢在以下位置构建原始 NSData_AESCrypt 代码的其他人:

//  AES Encrypt/Decrypt
//  Created by Jim Dovey and 'Jean'
//  See http://iphonedevelopment.blogspot.com/2009/02/strong-encryption-for-cocoa-cocoa-touch.html
//
//  BASE64 Encoding/Decoding
//  Copyright (c) 2001 Kyle Hammond. All rights reserved.
//  Original development by Dave Winer.
//
//  Put together by Michael Sedlaczek, Gone Coding on 2011-02-22
//

在iOS上,加密逻辑由原来的NSData+AESCrypt修改为:

@implementation NSData (AESCrypt)

- (NSData *)AES256EncryptWithKey:(NSString *)key
{
    return [self AES256EncryptWithKey:key ECB:false];
}
- (NSData *)AES256EncryptWithKey:(NSString *)key ECB:(Boolean) ecb
{
   // 'key' should be 32 bytes for AES256, will be null-padded otherwise
   char keyPtr[kCCKeySizeAES256 + 1]; // room for terminator (unused)
   bzero( keyPtr, sizeof( keyPtr ) ); // fill with zeroes (for padding)
   
   // fetch key data
   [key getCString:keyPtr maxLength:sizeof( keyPtr ) encoding:NSUTF8StringEncoding];
   
    // create results buffer with extra space for padding
   NSUInteger dataLength = [self length];
   size_t bufferSize = dataLength + kCCKeySizeAES256;
   void *buffer = malloc( bufferSize );
   
   size_t numBytesEncrypted = 0;
    NSString *hardCodeIV = @"1111111111111111";
        char iv[17];
        bzero( iv, sizeof( iv ) );
        [hardCodeIV getCString:iv maxLength:sizeof(iv) encoding:NSUTF8StringEncoding];
    //CBC
    CCCryptorStatus cryptStatus = kCCSuccess;
    if (ecb == false)
    {
   cryptStatus = CCCrypt( kCCEncrypt, kCCAlgorithmAES, kCCOptionPKCS7Padding,
                                          keyPtr, kCCKeySizeAES256,
                                          iv ,
                                          [self bytes], dataLength,
                                          buffer, bufferSize,
                                          &numBytesEncrypted );
    } else
    {
    // ECB
     cryptStatus = CCCrypt( kCCEncrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding | kCCOptionECBMode,
                                          keyPtr, kCCKeySizeAES256,
                                          NULL,
                                        [self bytes],  dataLength,
                                           buffer, bufferSize,
                                           &numBytesEncrypted );
    }
   if( cryptStatus == kCCSuccess )
   {
      //the returned NSData takes ownership of the buffer and will free it on deallocation
      return [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
   }
   
   free( buffer ); //free the buffer
   return nil;
}
- (NSData *)AES256DecryptWithKey:(NSString *)key
{
    return [self AES256DecryptWithKey:key ECB:false];
}
- (NSData *)AES256DecryptWithKey:(NSString *)key ECB:(Boolean) ecb
{
   // 'key' should be 32 bytes for AES256, will be null-padded otherwise
   char keyPtr[kCCKeySizeAES256+1]; // room for terminator (unused)
   bzero( keyPtr, sizeof( keyPtr ) ); // fill with zeroes (for padding)
   
   // fetch key data
   [key getCString:keyPtr maxLength:sizeof( keyPtr ) encoding:NSUTF8StringEncoding];
   
   NSUInteger dataLength = [self length];
   size_t bufferSize = dataLength + kCCKeySizeAES256;
   void *buffer = malloc( bufferSize );
   
   size_t numBytesDecrypted = 0;
    NSString *hardCodeIV = @"1111111111111111";
        char iv[17];
        bzero( iv, sizeof( iv ) );
        [hardCodeIV getCString:iv maxLength:sizeof(iv) encoding:NSUTF8StringEncoding];
    CCCryptorStatus cryptStatus = kCCSuccess;
    if (ecb == false)
    {
    cryptStatus = CCCrypt( kCCDecrypt, kCCAlgorithmAES, kCCOptionPKCS7Padding,
                                          keyPtr, kCCKeySizeAES256,
                                          iv ,
                                          [self bytes], dataLength,
                                          buffer, bufferSize,
                                          &numBytesDecrypted );
    } else {
     cryptStatus = CCCrypt( kCCDecrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding | kCCOptionECBMode,
                                           keyPtr, kCCKeySizeAES256,
                                           NULL,
                                           [self bytes], dataLength,
                                           buffer, bufferSize,
                                           &numBytesDecrypted );
    }
   if( cryptStatus == kCCSuccess )
   {
      //the returned NSData takes ownership of the buffer and will free it on deallocation
      return [NSData dataWithBytesNoCopy:buffer length:numBytesDecrypted];
   }
   
   free( buffer ); //free the buffer
   return nil;
}

然后使用一些助手 classes(未修改 class)将生成的 NSData 元素进行 base64 编码,如:

static char encodingTable[64] = 
{
   'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
   'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
   'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
   'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
};
+ (NSData *)dataWithBase64EncodedString:(NSString *)string
{
   return [[NSData allocWithZone:nil] initWithBase64EncodedString:string];
}

- (id)initWithBase64EncodedString:(NSString *)string
{
   NSMutableData *mutableData = nil;
   
   if( string )
   {
      unsigned long ixtext = 0;
      unsigned long lentext = 0;
      unsigned char ch = 0;
      unsigned char inbuf[4], outbuf[3];
      short i = 0, ixinbuf = 0;
      BOOL flignore = NO;
      BOOL flendtext = NO;
      NSData *base64Data = nil;
      const unsigned char *base64Bytes = nil;
      
      // Convert the string to ASCII data.
      base64Data = [string dataUsingEncoding:NSASCIIStringEncoding];
      base64Bytes = [base64Data bytes];
      mutableData = [NSMutableData dataWithCapacity:base64Data.length];
      lentext = base64Data.length;
      
      while( YES )
      {
         if( ixtext >= lentext ) break;
         ch = base64Bytes[ixtext++];
         flignore = NO;
         
         if( ( ch >= 'A' ) && ( ch <= 'Z' ) ) ch = ch - 'A';
         else if( ( ch >= 'a' ) && ( ch <= 'z' ) ) ch = ch - 'a' + 26;
         else if( ( ch >= '0' ) && ( ch <= '9' ) ) ch = ch - '0' + 52;
         else if( ch == '+' ) ch = 62;
         else if( ch == '=' ) flendtext = YES;
         else if( ch == '/' ) ch = 63;
         else flignore = YES;
         
         if( ! flignore )
         {
            short ctcharsinbuf = 3;
            BOOL flbreak = NO;
            
            if( flendtext ) 
            {
               if( ! ixinbuf ) break;
               if( ( ixinbuf == 1 ) || ( ixinbuf == 2 ) ) ctcharsinbuf = 1;
               else ctcharsinbuf = 2;
               ixinbuf = 3;
               flbreak = YES;
            }
            
            inbuf [ixinbuf++] = ch;
            
            if( ixinbuf == 4 ) 
            {
               ixinbuf = 0;
               outbuf [0] = ( inbuf[0] << 2 ) | ( ( inbuf[1] & 0x30) >> 4 );
               outbuf [1] = ( ( inbuf[1] & 0x0F ) << 4 ) | ( ( inbuf[2] & 0x3C ) >> 2 );
               outbuf [2] = ( ( inbuf[2] & 0x03 ) << 6 ) | ( inbuf[3] & 0x3F );
               
               for( i = 0; i < ctcharsinbuf; i++ )
                  [mutableData appendBytes:&outbuf[i] length:1];
            }
            
            if( flbreak )  break;
         }
      }
   }
   
   self = [self initWithData:mutableData];
   return self;
}

#pragma mark -

- (NSString *)base64Encoding
{
   return [self base64EncodingWithLineLength:0];
}

- (NSString *)base64EncodingWithLineLength:(NSUInteger)lineLength
{
   const unsigned char   *bytes = [self bytes];
   NSMutableString *result = [NSMutableString stringWithCapacity:self.length];
   unsigned long ixtext = 0;
   unsigned long lentext = self.length;
   long ctremaining = 0;
   unsigned char inbuf[3], outbuf[4];
   unsigned short i = 0;
   unsigned short charsonline = 0, ctcopy = 0;
   unsigned long ix = 0;
   
   while( YES )
   {
      ctremaining = lentext - ixtext;
      if( ctremaining <= 0 ) break;
      
      for( i = 0; i < 3; i++ )
      {
         ix = ixtext + i;
         if( ix < lentext ) inbuf[i] = bytes[ix];
         else inbuf [i] = 0;
      }
      
      outbuf [0] = (inbuf [0] & 0xFC) >> 2;
      outbuf [1] = ((inbuf [0] & 0x03) << 4) | ((inbuf [1] & 0xF0) >> 4);
      outbuf [2] = ((inbuf [1] & 0x0F) << 2) | ((inbuf [2] & 0xC0) >> 6);
      outbuf [3] = inbuf [2] & 0x3F;
      ctcopy = 4;
      
      switch( ctremaining )
      {
         case 1:
            ctcopy = 2;
            break;
         case 2:
            ctcopy = 3;
            break;
      }
      
      for( i = 0; i < ctcopy; i++ )
         [result appendFormat:@"%c", encodingTable[outbuf[i]]];
      
      for( i = ctcopy; i < 4; i++ )
         [result appendString:@"="];
      
      ixtext += 3;
      charsonline += 4;
      
      if( lineLength > 0 )
      {
         if( charsonline >= lineLength )
         {
            charsonline = 0;
            [result appendString:@"\n"];
         }
      }
   }
   
   return [NSString stringWithString:result];
}

然后将生成的 base64 编码字符串发送到云端,python pycryptodome 可以将其解密为:

    def aesCBCEncrypt(self,key,stri):
        if isinstance(key, str):
            key = key.encode('utf-8')
        cipher = AES.new(key, AES.MODE_CBC, self.nonce)  # , nonce=self.nonce)
        if isinstance(stri, str):
            data = stri.encode('utf-8')
        try:
            data = pad(data, 16)
            ciphertext = cipher.encrypt(data)
            ciphertext = b64encode(ciphertext)
            ret = ciphertext.decode('utf-8')
        except:
            print("Some Error")
            ret = ""
        return ret

    def aesCBCDecrypt(self,key,data):
        if isinstance(key, str):
            key = key.encode('utf-8')
        cipher = AES.new(key, AES.MODE_CBC, self.nonce)  # , nonce=self.nonce)
        if isinstance(data, str):
            data = data.encode('utf-8')
            data = b64decode(data)
        try:
            result = cipher.decrypt(data)
            result = unpad(result, 16)
            ret = result.decode('utf-8')
        except:
            print("Some Error")
            ret = ""
        return ret

    def aesECBEncrypt(self,key,stri):
        if isinstance(key, str):
            key = key.encode('utf-8')
        cipher = AES.new(key, AES.MODE_ECB)  # , nonce=self.nonce)
        if isinstance(stri, str):
            data = stri.encode('utf-8')
        try:
            data = pad(data,16)
            ciphertext = cipher.encrypt(data)
            ciphertext = b64encode(ciphertext)
            ret = ciphertext.decode('utf-8')
        except:
            print("Some Error")
            ret = ""
        return ret

    def aesECBDecrypt(self,key,data):
        if isinstance(key, str):
            key = key.encode('utf-8')
        cipher = AES.new(key, AES.MODE_ECB) #, nonce=self.nonce)
        if isinstance(data, str):
            data = data.encode('utf-8')
            data = b64decode(data)
        try:
            result = cipher.decrypt(data)
            result = unpad(result,16)
            ret = result.decode('utf-8')
        except:
            print("Some Error")
            ret = ""
        return ret

使用 CBC 时,iOS 和 Python 上的 IV(又名随机数)相同非常重要 - 否则它将无法工作。在这种情况下,这被设置为一个由 16 个“1”字符组成的字符串,以空值结尾。这本身并不是一个真正的秘密密钥,但可能值得更改它并保护它(可能还使用非对称发送它,在我的例子中是 RSA,加密)。然而,AES 是关键密钥,当然应该在设备之间加密发送。

最后,我建议使用 CBC,尽管需要考虑 IV,因为它更安全。当我有时间时,我将研究集成 Swift 只有 Apple Crypto Kit 库以支持其他形式......