目的:暗号化動画データの生成と自由なシークを含む動画再生
- 環境:UWP
- 動画ファイルに暗号化を施したい場合、再生するには「対称暗号化」が行える暗号化技術を採用しなければいけない。
- さらに自由にシークさせたい場合は暗号化済み動画ストリームに対するランダムアクセスが必要となり、その場合にはセクター毎に独立した暗号化・復号化が行えなければいけない。
ランダムアクセス可能な暗号化ストリーム技術が必要
単に対称暗号化するだけであればWindows.Security.Cryptographyが提供するAesManaged等を利用すれば十分ではあるが、それらは暗号化・復号化がデータの順序に依存する。このため高速なランダムアクセスが出来ず、「ストリームを復号化しながらの自由なシークを含む動画再生」という目的に合致しない。
XTSSharp を利用する
前述の問題を解消するにあたり、暗号化データストリームのままランダムアクセスを提供してくれるXTSSharpを利用したい。
サンプルコード(UWP)
UWPで動くサンプルコードを作成した。流れとしては
- VideoFileSelectButton_Click でボタンクリックを待ち受けて
- .mp4形式の動画をファイルピッカーで選択させて
- outputVideoFile に対して入力動画を暗号化して書き込み
var decryptedVideoStream = new XtsStream(~)
で復号化可能なストリームを生成して動画として再生
となっている。
usingSystem;usingSystem.Diagnostics;usingSystem.IO;usingSystem.Text;usingSystem.Threading.Tasks;usingWindows.Media.Core;usingWindows.Media.Playback;usingWindows.Storage;usingWindows.Storage.Pickers;usingWindows.UI.Xaml;usingWindows.UI.Xaml.Controls;usingXTSSharp;namespaceCryptoStreamTest{publicsealedpartialclassMainPage:Page{publicMainPage(){this.InitializeComponent();_mediaPlayer=newMediaPlayer();MediaPlayerelement.SetMediaPlayer(_mediaPlayer);}MediaPlayer_mediaPlayer;byte[]KEY=UTF8Encoding.UTF8.GetBytes("iajdoaijdoaadasdasdasdadadasdasd");privateasyncvoidVideoFileSelectButton_Click(objectsender,RoutedEventArgse){if(MediaPlayerelement.SourceisMediaSourcesource){MediaPlayerelement.Source=null;source.Dispose();}varopenFileDialog=newFileOpenPicker(){FileTypeFilter={".mp4"},ViewMode=PickerViewMode.Thumbnail,};varfile=awaitopenFileDialog.PickSingleFileAsync();if(fileisnull){return;}varoutputVideoFile=awaitApplicationData.Current.TemporaryFolder.CreateFileAsync(file.Name+".hohoemacv",CreationCollisionOption.ReplaceExisting);varxts=XtsAes128.Create(KEY);{StatusText.Text="開始";Stopwatchsw=newStopwatch();sw.Start();awaitTask.Run(async()=>{byte[]inputBuffer=newbyte[XtsSectorStream.DEFAULT_SECTOR_SIZE];byte[]outputBuffer=newbyte[XtsSectorStream.DEFAULT_SECTOR_SIZE];using(varrawVideoStream=awaitfile.OpenStreamForReadAsync())using(varoutputFileStream=awaitoutputVideoFile.OpenStreamForWriteAsync())using(varencryptor=xts.CreateEncryptor()){ulongcurrentSector=0;intreadLength=-1;while(readLength!=0){readLength=awaitrawVideoStream.ReadAsync(inputBuffer,0,inputBuffer.Length);encryptor.TransformBlock(inputBuffer,0,inputBuffer.Length,outputBuffer,0,currentSector);currentSector++;awaitoutputFileStream.WriteAsync(outputBuffer,0,outputBuffer.Length);ReportProgress(rawVideoStream.Position,rawVideoStream.Length);}}});sw.Stop();StatusText.Text="完了 time: "+sw.Elapsed;vardecryptedVideoStream=newXtsStream((awaitoutputVideoFile.OpenAsync(FileAccessMode.Read)).AsStreamForRead(),xts);varmediaSource=MediaSource.CreateFromStream(decryptedVideoStream.AsRandomAccessStream(),contentType:"movie/mp4");_mediaPlayer.Source=mediaSource;_mediaPlayer.Play();}Debug.WriteLine("crypted video: "+outputVideoFile.Path);}privatevoidReportProgress(longprogressBytes,longtotalBytes){_=Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,()=>{StatusText.Text="処理中 : "+((progressBytes/(float)totalBytes)*100.0f).ToString("f0");});}}}
<Pagex:Class="CryptoStreamTest.MainPage"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="using:CryptoStreamTest"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"mc:Ignorable="d"Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"><Grid><StackPanelSpacing="32"><Buttonx:Name="VideoFileSelectButton"Click="VideoFileSelectButton_Click">選択</Button><TextBlockx:Name="StatusText"/><MediaPlayerElementx:Name="MediaPlayerelement"Width="400"Stretch="UniformToFill"/></StackPanel></Grid></Page>
本記事掲載のソースコードは各自の責任において自由に利用して構わない。
サンプルコードの解説
暗号化とファイルへの書き出し
XTSSharpに含まれるXtsStreamを利用すれば単にStreamとしてWriteするだけで書き出し処理を完了できるが、サンプルコードではあえてusing (var encryptor = xts.CreateEncryptor())
を利用した暗号化処理を行っている。
これは処理性能問題のためで、例えば
using(varxtsStream=newXtsStream(outputFileStream,xts)
というように書き出しファイルを直接指定することでも暗号化済み動画ファイルを作れるが、およそ300MBの動画を処理するのに1分30秒ほど掛かってしまった。(Ryzen 5 3600 + SATA接続SSD)(メモリ使用量はほぼ変化無し)
であればと、
using(varmemStream=newMemoryStream())using(varxtsStream=newXtsStream(memStream,xts))
とメモリ上で暗号化を処理して、その後でファイルに書き出す流れを試してみると、300MBの動画ファイルでおよそ5秒程度と非常に高速に処理できたが、今度はメモリ使用量が動画ファイルの大きさと同じだけ必要になる問題が浮上してきた。
MemoryStreamのキャパシティを制限してメモリ使用量を抑えようとすると、(XtsStreamの実装上)内部のセクター数指定がBaseStream(MemoryStream)のPositionに依存しているために、セクター数のオフセット指定が出来なければ復号時とセクター数指定がズレてしまう問題が発生する。そのためXTSSharpのソースコードを改変してXtsStreamにセクター数のオフセット指定する機能を付け加えるか、自前でセクター数を指定して暗号化を処理する必要がある。
ここまで検討して、ようやくサンプルコードのように直接 using (var encryptor = xts.CreateEncryptor())
を利用して暗号化処理を施す形に落ち着いた。
セクター数指定(currentSector
)が暗号化・復号化の際にキーになるようなので注意したい。
復号化と動画として再生
varxts=XtsAes128.Create(KEY);vardecryptedVideoStream=newXtsStream((awaitoutputVideoFile.OpenAsync(FileAccessMode.Read)).AsStreamForRead(),xts);varmediaSource=MediaSource.CreateFromStream(decryptedVideoStream.AsRandomAccessStream(),contentType:"movie/mp4");_mediaPlayer.Source=mediaSource;_mediaPlayer.Play();
UWPであれば動画再生は非常に手厚くサポートされているので、これだけのコードで実装が完了する。
暗号化したときと同じxtsオブジェクトを生成してXtsStreamに渡すことでStream.Readした際に復号化されたデータを読み取らせることが出来る。
コンテンツタイプの指定に関しては実際のアプリケーションでは別途情報を持たせなければいけない。ファイルのアトリビュートやアプリケーションのDB等で管理する形になるだろうか。