Відтворення зашифрованих файлів з дешифровкою "на-льоту" на iOS

image

У процесі розробки додатка на фрейворке Sencha Touch для платформи iOS потрібно реалізувати відтворення локальних відео і аудіо файлів, які повинні бути зашифровані на сервері перед скачуванням в пам'ять мобільного пристрою. Додатковою умовою була заборона на створення дешифрованою версії файлу на диску, таким чином з'явилася необхідність робити розшифровку і читання даних в оперативній пам'яті. Тому стандартний плагін від Cordova для відтворення локальних медіа файлів не підходив, хоча досвіду розробки на Objective C у мене не було, я вирішив створити свій, володіє необхідним функціоналом.

Пошук рішення привів до класу AVURLAsset фреймворку AVFoundation, який ініціалізує медіа об'єкт для компонента AVPlayer. Для завантаження ресурсу AVURLAsset використовує свій об'єкт resourceLoader класу AVAssetResourceLoader, даний об'єкт працює через AVAssetResourceLoaderDelegate, в якому потрібно визначити два методи:

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

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

Перший використовується на початку і в процесі завантаження, а другий викликається коли процес завантаження скасовано. У разі якщо буде вказана невідома схема завантаження ресурсу, то resourceLoader буде використовувати методи, визначені розробником.

Таким чином, визначивши перший метод, можна передавати розшифровані дані у вигляді NSData.

Приклад реалізації методу завантаження даних через 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;
}

В даному коді decryptedData містить результат розшифрування даних, які були завантажені з зашифрованого файлу.

Нижче я описав приклад ініціалізації плеєра:

Фейковий шлях до локального файла, важливим моментом є кастомний схема «encryptedfile://»:

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

Реальний же зашифрований файл відкритий з допомогою NSFileHandle:

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

Нижче ініціалізуємо плеєр і делегуємо власний resourceLoader:

assetPlayer = [AVURLAsset assetWithURL:resourceURL];
[assetPlayer.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];

itemPlayer = [AVPlayerItem playerItemWithAsset:assetPlayer]; 
avPlayer = [AVPlayer playerWithPlayerItem:itemPlayer];

Далі створюємо контролер для плеєра:

controller = [[AVPlayerViewController alloc] init];

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];

controller.player = avPlayer;
controller.player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
[avPlayer play];

По суті даний код повністю робочий, але залишається ще одна проблема — це обмеження пам'яті на мобільних пристроях. Ми не можемо завантажити в оперативну пам'ять розшифровані дані великого відеофайлу.

Тому я вирішив зашифровувати вихідні файли блоками, в моєму випадку по 16 мегабайт, щоб мати можливість отримати доступ до будь-якого потрібного блоку, не розшифровуючи весь файл.

Я допрацював метод об'єкта resourceLoader, який викликається при завантаження ресурсу:

- (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;
}

У коді параметр chunkMode показує, що файл був зашифрований блоками і необхідно перевіряючи параметри requestedOffset і requestedLength завантажувати необхідний блок з файлу і розшифровувати його. За це відповідає функція getDataFromFile:

- (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;
}

У моєму випадку для шифрування я використовую алгоритм AES-128 CBC, а для розшифровки бібліотеку AEScrypt-ObjC.

Мною був доданий один метод, що дозволяє розшифровувати потрібні блоки зашифрованого файлу (більш універсальний, так як в цьому конкретному випадку розмір необхідного блоку завжди дорівнює розміру зашифрованого блоку):

Метод decryptData
+ (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;
}


В результаті ми отримали плагін, який вміє відтворювати великі зашифровані файли без створення вихідної версії файлу на диску пристрою. При цьому знизилося використання оперативної пам'яті пристрою, що теж є важливим плюсом.

Корисні посилання:

Библитека AEScrypt-ObjC
Стаття на Хабре, яка допомогла розібратися в принципах AVAssetResourceLoaderDelegate
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.