Play encrypted files with on-the-fly decryption on iOS

    image

    In the process of developing the application on the Sencha Touch framework for the iOS platform, it was necessary to implement the playback of local video and audio files, which must be encrypted on the server before downloading to the memory of the mobile device. An additional condition was the ban on creating a decrypted version of the file on disk, so there was a need to decrypt and read data in RAM. Therefore, the standard plugin from Cordova for playing local media files was not suitable, although I did not have any experience in developing Objective-C, I decided to create my own with the required functionality.

    Finding a solution led to the AVURLAsset classthe AVFoundation framework that initializes the media object for the AVPlayer component. To load a resource, AVURLAsset uses its own resourceLoader object of class AVAssetResourceLoader, this object works through AVAssetResourceLoaderDelegate, in which two methods must be defined:

    - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
    - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;

    The first is used at the beginning and during the boot process, and the second is called when the boot process is canceled. If an unknown resource loading scheme is specified, then resourceLoader will use the methods defined by the developer.

    Thus, having defined the first method, it is possible to transmit decrypted data in the form of NSData.

    An example implementation of the method of loading data through AVAssetResourceLoaderDelegate:

    - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
        loadingRequest.contentInformationRequest.contentType    = (__bridge NSString *)kUTTypeQuickTimeMovie;
        loadingRequest.contentInformationRequest.contentLength  = movieLength;
        loadingRequest.contentInformationRequest.byteRangeAccessSupported   = YES;
        [loadingRequest.dataRequest respondWithData:[decryptedData subdataWithRange:NSMakeRange((NSUInteger)loadingRequest.dataRequest.requestedOffset, loadingRequest.dataRequest.requestedLength)]];
        [loadingRequest finishLoading];
        return YES;
    }

    In this code, decryptedData contains the result of decrypting the data that was downloaded from the encrypted file.

    Below I described an example of player initialization: A

    fake path to a local file, an important point is the custom encryptedfile: // scheme:

    resourceURL = [NSURL URLWithString:[@"encryptedfile://" stringByAppendingString:fake-path-to-file]];

    The real encrypted file is opened using NSFileHandle:

    fileHandle = [NSFileHandle fileHandleForReadingFromURL:resourceURL error:nil];

    We initialize the player below and delegate our own resourceLoader:

    assetPlayer   = [AVURLAsset assetWithURL:resourceURL];
    [assetPlayer.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
    itemPlayer = [AVPlayerItem playerItemWithAsset:assetPlayer];   
    avPlayer = [AVPlayer playerWithPlayerItem:itemPlayer];

    Next, create a controller for the player:

    controller = [[AVPlayerViewController alloc] init];
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
    controller.player = avPlayer;
    controller.player.actionAtItemEnd  = AVPlayerActionAtItemEndNone;
    [avPlayer play];

    In fact, this code is fully working, but another problem remains - the memory limit on mobile devices. We cannot load the decrypted data of a large video file into RAM.

    Therefore, I decided to encrypt the source files in blocks, in my case 16 megabytes each, in order to be able to access any block I need without decrypting the entire file.

    I modified the method of the resourceLoader object, which is called when the resource is loaded:

    - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
        loadingRequest.contentInformationRequest.contentType    = (__bridge NSString *)kUTTypeQuickTimeMovie;
        loadingRequest.contentInformationRequest.contentLength  = movieLength;
        loadingRequest.contentInformationRequest.byteRangeAccessSupported   = YES;
        if(chunkMode){
            NSUInteger offset = (NSUInteger)loadingRequest.dataRequest.requestedOffset;
            if(currentOffset != offset){
                currentOffset = offset;
                NSUInteger requestedBlock = floor(currentOffset/blockSize);
                if(currentBlockIndex != requestedBlock){
                    currentBlockIndex = requestedBlock;
                    // Loading other block of data
                    decryptedData = [self getDataFromFile:currentBlockIndex];
                }
            }
            if(currentOffset > blockSize*currentBlockIndex){
                offset = currentOffset - blockSize*currentBlockIndex;
            } else {
                offset = 0;
            }
            NSUInteger maxLength = [decryptedData length] - offset;
            if(loadingRequest.dataRequest.requestedLength < maxLength 
               && loadingRequest.dataRequest.requestedLength <= [decryptedData length]){
                maxLength = loadingRequest.dataRequest.requestedLength;
            }
            [loadingRequest.dataRequest respondWithData:[decryptedData subdataWithRange:NSMakeRange(offset, maxLength)]];
        } else {
            [loadingRequest.dataRequest respondWithData:[decryptedData subdataWithRange:NSMakeRange((NSUInteger)loadingRequest.dataRequest.requestedOffset, loadingRequest.dataRequest.requestedLength)]];
        }
        [loadingRequest finishLoading];
        return YES;
    }

    In the code, the chunkMode parameter indicates that the file has been encrypted with blocks and it is necessary to check the requestedOffset and requestedLength parameters to load the necessary block from the file and decrypt it. The getDataFromFile function is responsible for this:

    - (NSMutableData *) getDataFromFile:(NSUInteger) index
    {
        if(fileHandle){
            [fileHandle seekToFileOffset:index*chunksInBlock*chunkSize];
            return [NSMutableData dataWithData:[AESCrypt decryptData:[fileHandle readDataOfLength:chunksInBlock*chunkSize] password:PASSWORD chunkSize:blockSize iv:IV]];
        }
        return nil;
    }

    In my case, I use the AES-128 CBC algorithm for encryption, and the AEScrypt-ObjC library for decryption.

    I added one method that allows you to decrypt the necessary blocks from an encrypted file (more universal, since in this particular case the size of the necessary block is always equal to the size of the encrypted block):

    DecryptData method
    + (NSData*) decryptData:(NSData*)data password:(NSString *)password chunkSize:(NSUInteger)chunkSize
    {
        return [self decryptData:data password:password chunkSize:chunkSize offsetBlock:0 countBlock:0 iv:nil];
    }
    + (NSData*) decryptData:(NSData*)data password:(NSString *)password chunkSize:(NSUInteger)chunkSize iv: (id) iv
    {
        return [self decryptData:data password:password chunkSize:chunkSize offsetBlock:0 countBlock:0 iv:iv];
    }
    + (NSData*) decryptData:(NSData*)data
                   password:(NSString *)password
                  chunkSize:(NSUInteger)chunkSize
                offsetBlock:(NSUInteger)offsetBlock
                 countBlock:(NSUInteger)countBlock
                         iv: (id) iv
    {
        NSUInteger length = [data length];
        if (chunkSize > length) {
            chunkSize = floor(length/16)*16;
        }
        if(countBlock > 0){
            length = (offsetBlock+countBlock)*chunkSize;
        }
        if(length > [data length]){
            length = [data length];
        }
        NSUInteger offset = offsetBlock * chunkSize;
        NSMutableData *decryptedData = [NSMutableData alloc];
        NSData* encryptedPartOfData;
        do {
            NSUInteger thisChunkSize = length - offset > chunkSize ? chunkSize : length - offset;
            NSData* partOfData = [data subdataWithRange:NSMakeRange(offset, thisChunkSize)];
            if(iv == nil){
                encryptedPartOfData = [partOfData decryptedAES256DataUsingKey:[[password dataUsingEncoding:NSUTF8StringEncoding] SHA256Hash] error:nil];
            } else {
                encryptedPartOfData = [partOfData decryptedAES256DataUsingKey:[password dataUsingEncoding:NSUTF8StringEncoding] initializationVector:iv error:nil];
            }
            [decryptedData appendData:encryptedPartOfData];
            offset += thisChunkSize;
        } while (offset < length);
        return decryptedData;
    }


    As a result, we got a plugin that can play large encrypted files without creating the original version of the file on the device’s disk. At the same time, the use of the device’s RAM was reduced, which is also an important plus.

    Useful links:

    AEScrypt-ObjC library The
    article on Habr who helped to understand principles of AVAssetResourceLoaderDelegate

    Also popular now: