diff --git a/iOSbackup/__init__.py b/iOSbackup/__init__.py index 05273a0..9e2a5ef 100644 --- a/iOSbackup/__init__.py +++ b/iOSbackup/__init__.py @@ -976,6 +976,10 @@ def getFileDecryptedCopy(self, relativePath=None, manifestEntry=None, targetName decryptor = AES.new(key, AES.MODE_CBC, b'\x00'*16) + chunkIndex=0 + bytesWritten=0 + decryptedChunk=None + finalByteWritten=None # {BACKUP_ROOT}/{UDID}/ae/ae2c3d4e5f6... with open(os.path.join(self.backupRoot, self.udid, fileNameHash[:2], fileNameHash), 'rb') as inFile: @@ -985,18 +989,50 @@ def getFileDecryptedCopy(self, relativePath=None, manifestEntry=None, targetName mappedInFile = mmap.mmap(inFile.fileno(), length=0, prot=mmap.PROT_READ) with open(targetFileName, 'wb') as outFile: - - chunkIndex=0 while True: chunk = mappedInFile[chunkIndex*chunkSize:(chunkIndex+1)*chunkSize] - if len(chunk) == 0: break - outFile.write(decryptor.decrypt(chunk)) + decryptedChunk = decryptor.decrypt(chunk) + outFile.write(decryptedChunk) + chunkIndex+=1 + bytesWritten+=len(decryptedChunk) + finalByteWritten=decryptedChunk[-1] + + def hasAesPadding(expectedPaddingSize): + """Checks the content of the last N bytes, which should be filled with 'N' repeating""" + if finalByteWritten != expectedPaddingSize: + return False + + # Checks each byte by counting occurrences + potentialPadding = decryptedChunk[-finalByteWritten:] + return potentialPadding.count(finalByteWritten) == finalByteWritten + + # Compare file sizes across 1) Manifest.db record, 2) original file, and 3) decrypted output + # (decrypted output sometimes adds RFC 1423 padding, aligning data on a 16-byte boundary) + originalSize=os.path.getsize(os.path.join(self.backupRoot, self.udid, fileNameHash[:2], fileNameHash)) + if bytesWritten - info['size'] > 16: + # Check if we have a final encryption pass of 16 bytes appended, for some reason + if hasAesPadding(16): + outFile.truncate(bytesWritten - finalByteWritten) + + if bytesWritten - info['size'] == 16: + if hasAesPadding(16): + outFile.truncate(bytesWritten - 16) + + if bytesWritten - info['size'] < 16 and bytesWritten - info['size'] > 0: + # This is the "normal" case, where we added a few bytes of extra padding + if hasAesPadding(bytesWritten - info['size']): + outFile.truncate(bytesWritten - finalByteWritten) + + if bytesWritten - info['size'] < 0: + # For an over-reported size, do nothing because we can't conjure data from nowhere + # Still, check if the last 16 bytes looks like padding + if hasAesPadding(16): + outFile.truncate(bytesWritten - finalByteWritten) - outFile.truncate(info['size']) elif info['isFolder']: # Plain folder Path(targetFileName).mkdir(parents=True, exist_ok=True)