🐡

UnityのPlayAssetDelivaryで暗号化されたアセバンを扱う方法

2023/04/14に公開

googleのPlay Asset Delibary(PAD)を使用して独自に暗号化されたアセバンを読み込む方法の覚書です。

結論

PlayAssetPackRequest#getassetlocationで保存先のパス/Offset/Sizeが取れるので、
そこからアセットパックを切り出します。

https://developer.android.com/reference/unity/class/Google/Play/AssetDelivery/PlayAssetPackRequest#classGoogle_1_1Play_1_1AssetDelivery_1_1PlayAssetPackRequest_1abf75a8c64a3a947b578bffeaee66a78b

環境

unity 2021.3.17
google pad sdk 1.7.0

PADでアセバンを扱う方法

まずは通常のシナリオです。

公式マニュアルに記載されている通り、AssetBundleである場合はPlayAssetPackRequest.LoadAssetBundleAsyncPlayAssetDelivery.RetrieveAssetBundleAsyncが使用できます。

アセットが AssetBundle である場合は、PlayAssetPackRequest.LoadAssetBundleAsync(assetPath) コンビニエンス メソッドを使用できます。メソッドに渡すアセットパスは、アセットパック内の AssetBundle へのパスに対応している必要があります。これにより、AssetBundleCreateRequest が返されます。

https://developer.android.com/guide/playcore/asset-delivery/integrate-unity?hl=ja&language=api#load-assets-into-memory

デモでも提供されているこれらのAPIは暗号化をかけると使うことができなくなります。
これは内部でUnityのAssetBundleAPIを直に使用しており、復号化の処理を差し込めないからです。

PADで暗号化されたアセバンを扱う方法

暗号化Streamを用意して復号化とAssetBundleの読み込みを行うシナリオです。
getassetlocationから保存先のパス/Offset/Sizeを取得してアセットを切り出します。

//この実装はサンプル実装のため、実際には使用しないでください。
PlayAssetPackRequest request = PlayAssetDelivery.RetrieveAssetPackAsync("streaming");
var location = request.GetAssetLocation("prefabs/streamingchara_test_streaming");
var bytes = File.ReadAllBytes(location.Path).AsSpan((int)location.Offset,(int)location.Size).ToArray();
var stream = new MemoryStream(bytes);
var bundleCryptStream = new BundleCryptStream(stream);
var assetBundle = AssetBundle.LoadFromStream(bundleCryptStream);

https://developer.android.com/reference/unity/class/Google/Play/AssetDelivery/PlayAssetPackRequest#getassetlocation

サンプルで使用しているBundleCryptStreamは以下になります。

BundleCryptStream
    // 既存Streamを上書きして暗号化と複合化を行うストリーム
    // 既存ストリームをもとに作成した場合該当ストリームの解放責任を持つ
    // 参考 : https://gist.github.com/tsubaki/4c15f08f592fc303cce97f13c686243f
    public class BundleCryptStream : Stream
    {
        private Stream _baseStream;

        public BundleCryptStream(Stream baseStream)
        {
            _baseStream = baseStream;
        }

        ~BundleCryptStream()
        {
            Dispose(true);
        }

        public override void Flush() => _baseStream.Flush();

        public override long Seek(long offset, SeekOrigin origin) => _baseStream.Seek(offset, origin);

        public override void SetLength(long value) => _baseStream.SetLength(value);

        public override bool CanRead => _baseStream.CanRead;
        public override bool CanSeek => _baseStream.CanSeek;
        public override bool CanWrite => _baseStream.CanWrite;
        public override long Length => _baseStream.Length;
        public override long Position 
        { 
            get => _baseStream.Position;
            set => _baseStream.Position = value;
        }
        
        public override int Read(byte[] buffer, int offset, int count)
        {
            var ret = _baseStream.Read(buffer, offset, count);
            
            ApplyXOR(buffer,offset,count);
            return ret;
        }
        
        public override void Write(byte[] buffer, int offset, int count)
        {
            ApplyXOR(buffer,offset,count);
            _baseStream.Write(buffer, offset, count);
        }
        
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _baseStream?.Dispose();
            }

            base.Dispose(disposing);
        }

        private void ApplyXOR(byte[] buffer, int offset, int count)
        {
            if (buffer is null || buffer.Length == 0 || count == 0)
            {
                return ;
            }

            //サンプルkeyなのでハードコードしてるが、runtime導出の方がセキュア
            byte kay = 110;

            for (int i = offset; i < count; i++)
            {
                buffer[i] ^= kay;
            }
        }
    }


Discussion