views:

463

answers:

3

Hi, I have been banging my head on a wall with this one. I need to code my iPhone application to encrypt a 4 digit "pin" using 3DES in ECB mode for transmission to a webservice which I believe is written in .NET.

+ (NSData *)TripleDESEncryptWithKey:(NSString *)key dataToEncrypt:(NSData*)encryptData {
NSLog(@"kCCKeySize3DES=%d", kCCKeySize3DES);
char keyBuffer[kCCKeySize3DES+1]; // room for terminator (unused)
bzero( keyBuffer, sizeof(keyBuffer) ); // fill with zeroes (for padding)

[key getCString: keyBuffer maxLength: sizeof(keyBuffer) encoding: NSUTF8StringEncoding];

// encrypts in-place, since this is a mutable data object
size_t numBytesEncrypted = 0;

size_t returnLength = ([encryptData length] + kCCBlockSize3DES) & ~(kCCBlockSize3DES - 1);

// NSMutableData* returnBuffer = [NSMutableData dataWithLength:returnLength];
char* returnBuffer = malloc(returnLength * sizeof(uint8_t) );

CCCryptorStatus ccStatus = CCCrypt(kCCEncrypt, kCCAlgorithm3DES , kCCOptionECBMode,
                                 keyBuffer, kCCKeySize3DES, nil,
                                 [encryptData bytes], [encryptData length], 
                                 returnBuffer, returnLength,
                                 &numBytesEncrypted);

if (ccStatus == kCCParamError) NSLog(@"PARAM ERROR");
else if (ccStatus == kCCBufferTooSmall) NSLog(@"BUFFER TOO SMALL");
else if (ccStatus == kCCMemoryFailure) NSLog(@"MEMORY FAILURE");
else if (ccStatus == kCCAlignmentError) NSLog(@"ALIGNMENT");
else if (ccStatus == kCCDecodeError) NSLog(@"DECODE ERROR");
else if (ccStatus == kCCUnimplemented) NSLog(@"UNIMPLEMENTED");

if(ccStatus == kCCSuccess) {
    NSLog(@"TripleDESEncryptWithKey encrypted: %@", [NSData dataWithBytes:returnBuffer length:numBytesEncrypted]);
    return [NSData dataWithBytes:returnBuffer length:numBytesEncrypted];
}
else 
    return nil;
} }

I do get a value encrypted using the above code, however it does not match the value from the .NET web service.

I believe the issue is that the encryption key I have been supplied by the web service developers is 48 characters long.

I see that the iPhone SDK constant "kCCKeySize3DES" is 24. So I SUSPECT, but don't know, that the commoncrypto API call is only using the first 24 characters of the supplied key.

Is this correct?

Is there ANY way I can get this to generate the correct encrypted pin? I have output the data bytes from the encryption PRIOR to base64 encoding it and have attempted to match this against those generated from the .NET code (with the help of a .NET developer who sent the byte array output to me). Neither the non-base64 encoded byte array nor the final base64 encoded strings match.

A: 

Maybe you need to use padding? Try setting the options to:

(kCCOptionPKCS7Padding | kCCOptionECBMode)
St3fan
Thanks for the suggestion, however I have been told that the .NET code sets padding to "PaddingMode.None". It's therefore an assumption of mine that I don't need to pad from the iPhone app. I may be wrong of course!
Russell Hill
Do you have access to the .Net code? If you do then I suggest to read it to reverse engineer the actual encryption used.
St3fan
+1  A: 

3DES is a symmetric block cipher. Using a 24-byte key, 3DES encrypts an 8-byte block into another 8-byte block. With the same 24-byte key, the encryption is reversible (i.e. you can decrypt).

The key is an arbitrary sequence of bytes. That's not the same as "characters". In particular, having one of those bytes with value zero is perfectly legal. Similarly, input and output may be arbitrary bytes.

If the key you were given consists in "characters" then it must be transformed into an appropriate sequence of bytes in some way. Since you got a 48-character "key string" and 48 is exactly 24*2, a plausible guess is that the key is given in hexadecimal notation: see if it contains only digits, and letters from 'a' to 'f'.

As for padding: 3DES encrypts only 8-byte blocks. When a "message" is to be encrypted and has some length distinct from 8 bytes, then it is customary to format and split and process the message so that it can be encrypted in a number of invocations to 3DES. The two keywords are padding and chaining. Padding is about adding some extra bytes at the end (in such a way that those byte can be unambiguously removed) so that the length is appropriate (e.g. multiple of 8). Chaining is about deciding what exactly goes into each 3DES invocation (simply splitting the padded message into independently encrypted blocks is known as "ECB" and has weaknesses).

If your PIN code contains 4 digits, then there must be some convention on how these four digits become at least 8 bytes, to be fed to 3DES. If the iPhone behaves similarly to what this man page for MacOS X describes, then your code should not run successfully unless the length of encryptData is a multiple of eight. Which means that the code you do not show, which converts a 4-digit PIN into an 8-byte buffer, already does some non-trivial transformations. For instance, that code might put the four digits into four bytes (using ASCII encoding) and set the four other bytes to zero. Or maybe it fails to do so. Either way, each of the 64 input bits to 3DES is important, and you have to get it exactly in the same way than the server. You should inspect that code as well.

Thomas Pornin
Thanks Thomas for your detailed answer here. The concepts you mention are exactly what I ended up looking into in more detail. I was lucky in that I had access to the .NET version of the encryption code and that gave me valuable pointers.The 4 digit PIN was turned into an 8 digit value to be fed into the 3DES algorithm by appending 4 0's. I added my code to this answer, but marked your as the solution because your knowledge was totally correct.
Russell Hill
A: 

Hi, well, I managed to resolve this with a lot of reading and the comments here on stackoverflow. There where several issues. The key I had been given by the .NET developers was 48 characters. This of course needed to be read as a hex string and converted down to 24 characters instead.

I added code to do this, and the complete routine is as follows. I am not sure it will be of any use as it's quite specific to our implementation.

+ (NSString *)doCipher3DES:(NSString *)sTextIn key:(NSString *)sKey {
NSMutableData * dTextIn;
CCCryptorStatus ccStatus = kCCSuccess;

// need to add 4 zeros as sTextIn will be a 4 digit PIN
sTextIn = [sTextIn stringByAppendingString:@"0000"];

// convert to data
dTextIn = [[sTextIn dataUsingEncoding: NSASCIIStringEncoding] mutableCopy];           

// key will be a 48 char hex stream, so process it down to 24 chars
const char * bytes = [sKey cStringUsingEncoding: NSUTF8StringEncoding];
NSUInteger length = strlen(bytes);
unsigned char * r = (unsigned char *) malloc(length / 2 + 1);
unsigned char * index = r;

while ((*bytes) && (*(bytes +1))) {
    *index = strToChar(*bytes, *(bytes +1));
    index++;
    bytes+=2;
}
*index = '\0';

NSData *dKey = [NSData dataWithBytes: r length: length / 2];
free(r);

NSLog(@"doCipher3DES - key: %@", dKey);

uint8_t *bufferPtr1 = NULL;    
size_t bufferPtrSize1 = 0;    
size_t movedBytes1 = 0;    
uint8_t iv[kCCBlockSize3DES];    
memset((void *) iv, 0x0, (size_t) sizeof(iv));    
bufferPtrSize1 = ([sTextIn length] + kCCBlockSize3DES) & ~(kCCBlockSize3DES -1);    
bufferPtr1 = malloc(bufferPtrSize1 * sizeof(uint8_t));    
memset((void *)bufferPtr1, 0x00, bufferPtrSize1);    

ccStatus = CCCrypt(kCCEncrypt, // CCOperation op    
                   kCCAlgorithm3DES, // CCAlgorithm alg    
                   kCCOptionECBMode, // CCOptions options    
                   (const void *)[dKey bytes], // const void *key    
                   kCCKeySize3DES, // size_t keyLength    
                   nil, // const void *iv    
                   (const void *)[dTextIn bytes], // const void *dataIn
                   [dTextIn length],  // size_t dataInLength    
                   (void *)bufferPtr1, // void *dataOut    
                   bufferPtrSize1,     // size_t dataOutAvailable 
                   &movedBytes1);      // size_t *dataOutMoved     

if (ccStatus == kCCParamError) NSLog(@"PARAM ERROR");
else if (ccStatus == kCCBufferTooSmall) NSLog(@"BUFFER TOO SMALL");
else if (ccStatus == kCCMemoryFailure) NSLog(@"MEMORY FAILURE");
else if (ccStatus == kCCAlignmentError) NSLog(@"ALIGNMENT");
else if (ccStatus == kCCDecodeError) NSLog(@"DECODE ERROR");
else if (ccStatus == kCCUnimplemented) NSLog(@"UNIMPLEMENTED");

NSString * sResult;    
NSData *dResult = [NSData dataWithBytes:bufferPtr1 length:movedBytes1];    

NSLog(@"doCipher3DES encrypted: %@", dResult);

sResult = [Base64 encode:dResult];    

return sResult; }

The code for strToChar is as follows:

unsigned char strToChar (char a, char b) {
char encoder[3] = {'\0','\0','\0'};
encoder[0] = a;
encoder[1] = b;
return (char) strtol(encoder,NULL,16); }

I hope this helps someone...

Russell Hill