Воспроизведение зашифрованных файлов с дешифровкой “на-лету” на 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
    Аркадия 38,89
    Заказная разработка, IT-консалтинг
    Поделиться публикацией
    Похожие публикации
    Комментарии 2
    • 0
      Если не секрет, симметричный ключ хранится на диске устройства рядом с зашифрованным файлом? Или зашит в приложении? Или с сервера приходит и нельзя воспроизводить уже скаченные файлы?
      • 0
        Задача как раз была реализовать просмотр файлов в режиме оффлайн, поэтому секретный ключ хранится на девайсе в KeyChain с последующим преобразованием (уникальным для каждого файла) в коде для получения параметров PASSWORD и IV.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое