Confusion in RNDecryptor addData method

257 views Asked by At

The RNDecryptor class in ObjectiveC at HERE has a feature to decrypt file in chunks as follows:

- (IBAction)decryptWithSemaphore:(id)sender {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

__block int total = 0;
int blockSize = 32 * 1024;

NSArray *docPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *input = [[docPaths objectAtIndex:0] stringByAppendingPathComponent:@"zhuge.rncryptor"];
NSString *output = [[docPaths objectAtIndex:0] stringByAppendingPathComponent:@"zhuge.decrypted.pdf"];

NSInputStream *cryptedStream = [NSInputStream inputStreamWithFileAtPath:input];
__block NSOutputStream *decryptedStream = [NSOutputStream outputStreamToFileAtPath:output append:NO];
__block NSError *decryptionError = nil;

[cryptedStream open];
[decryptedStream open];

RNDecryptor *decryptor = [[RNDecryptor alloc] initWithPassword:@"12345678901234567890123456789012" handler:^(RNCryptor *cryptor, NSData *data) {
    @autoreleasepool {
        NSLog(@"Decryptor recevied %d bytes", data.length);
        [decryptedStream write:data.bytes maxLength:data.length];
        dispatch_semaphore_signal(semaphore);

        data = nil;
        if (cryptor.isFinished) {
            [decryptedStream close];
            decryptionError = cryptor.error;
            // call my delegate that I'm finished with decrypting
        }
    }
}];

while (cryptedStream.hasBytesAvailable) {
    @autoreleasepool {
        uint8_t buf[blockSize];
        NSUInteger bytesRead = [cryptedStream read:buf maxLength:blockSize];
        if (bytesRead > 0) {
            NSData *data = [NSData dataWithBytes:buf length:bytesRead];

            total = total + bytesRead;
            [decryptor addData:data];
            NSLog(@"New bytes to decryptor: %d Total: %d", bytesRead, total);

            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        }
    }
}

[cryptedStream close];
[decryptor finish];

dispatch_release(semaphore);

}

And the addData method of RNDecryptor is as follows:

- (void)addData:(NSData *)theData
{
  if (self.isFinished) {
    return;
  }

  [self.inData appendData:theData];
  if (!self.engine) {
    [self consumeHeaderFromData:self.inData];
  }
  if (self.engine) {
    NSUInteger HMACLength = self.HMACLength;
    if (self.inData.length > HMACLength) {
      NSData *data = [self.inData _RNConsumeToIndex:self.inData.length - HMACLength];
      [self decryptData:data];
    }
  }
}

I dont understand here what this line is actully trying to do which is called for every chunk of encrypted stream:

  NSData *data = [self.inData _RNConsumeToIndex:self.inData.length - HMACLength];

Lets say I have a Block size of 1000 Bytes and HMACLength is 32.

If I try to decrypt file larger than the size of block size, lets say 5000 bytes, then this addData method will run first iteration as this

NSData *data = [self.inData _RNConsumeToIndex:1000 - 32];

which is after consuming headers the encrypted bytes from index 0 to (1000-32), but the hash is written at the end of the encrypted stream, the last few bytes, not with every chunk. and, in the next iteration, the inputstream will be reading next 1000 bytes, what will happen to the 32 bytes that were trimmed from the first iteration chunk?

May be I am confused as this code is proven but I want to understand this.

Thanks in advance.

1

There are 1 answers

5
Maarten Bodewes On

The issue is that streams normally don't know how much data is left. In your case, it seems that the authentication tag (HMAC value) was put at the end of the ciphertext by the side sending the ciphertext.

Now the issue is that you should only update the data, not the authentication tag. As you don't know how much data is still available, it may be that you are already reading in the authentication tag at the end. Obviously the HMAC calculation will fail if you include the output of the HMAC itself in the calculation.

So basically you read the stream and update the HMAC state until the end. Then you perform the last HMAC update until the end of the ciphertext. You extract the given authentication tag from the end of the stream and you compare the calculated and given value. If they are the same then the ciphertext (and thus the plaintext) is checked for integrity and authentication - given that the secret key was never revealed to an attacker of course.

If the code is correct (and given the code of Rob, that's extremely likely) then the 32 bytes are included in the MAC calculation unless they are indeed part of the authentication tag. In other words, you always have to buffer at least the size of the authentication tag if you put the authentication tag at the end.

You could rewrite the scheme in such a way that the length of the length of the ciphertext is known in advance. You could for instance start the stream by a 64 bit number representing the length of the ciphertext. Then you would not have to do the awkward buffering, at the expense of 64 additional bits. Higher level protocols rely on ASN.1/DER encoding or even XML to separate the message and the authentication tag.