Xamarin.iOS / Akavache - Encrypted Cache with custom EncryptionProvider

723 views Asked by At

I have a Xamarin.iOS app that is using Akavache to cache data and reduce the number on unnecessary requests to the server for data. Everything is working great as expected using BlobCache.LocalMachine, love it.

Now I would like to make sure that the data at rest in encrypted. From researching this and reading documentation online I saw that the default implementation doesn't actually encrypt the cache on iOS and that I actually need to provide a custom implementation of IEncryptionProvider.

I followed this article: http://kent-boogaart.com/blog/password-protected-encryption-provider-for-akavache

Added input from this thread: https://github.com/akavache/Akavache/issues/190

And ended up with this implementation:

    public interface IPasswordProtectedEncryptionProvider : IEncryptionProvider
    {
        void SetPassword(string password);
    }

    public sealed class MyEncryptionProvider : IPasswordProtectedEncryptionProvider
    {
        static readonly byte[] salt = Encoding.ASCII.GetBytes("dVBZMQWyFRcJOIas");
        readonly IScheduler scheduler;
        readonly SymmetricAlgorithm symmetricAlgorithm;
        ICryptoTransform decryptTransform;
        ICryptoTransform encryptTransform;

        public MyEncryptionProvider()
        {
            scheduler = BlobCache.TaskpoolScheduler ?? throw new ArgumentNullException(nameof(scheduler), "Scheduler instance is null");
            symmetricAlgorithm = new RijndaelManaged();
            var securePassword = "kadhaskdhsakhaskjdhaskjdhaskdjashdkjahkfghkjhew";
            SetPassword(securePassword);
        }

        public void SetPassword(string password)
        {
            if(password == null)
               throw new ArgumentNullException(nameof(password), "password can't be null");

            var derived = new Rfc2898DeriveBytes(password, salt);
            var bytesForKey = symmetricAlgorithm.KeySize / 8;
            var bytesForIV = symmetricAlgorithm.BlockSize / 8;
            symmetricAlgorithm.Key = derived.GetBytes(bytesForKey);
            symmetricAlgorithm.IV = derived.GetBytes(bytesForIV);
            decryptTransform = symmetricAlgorithm.CreateDecryptor(symmetricAlgorithm.Key, symmetricAlgorithm.IV);
            encryptTransform = symmetricAlgorithm.CreateEncryptor(symmetricAlgorithm.Key, symmetricAlgorithm.IV);
        }

        public IObservable<byte[]> DecryptBlock(byte[] block)
        {
            if (block == null)
            {
                throw new ArgumentNullException(nameof(block), "block can't be null");
            }

            if (decryptTransform == null)
            {
                return Observable.Throw<byte[]>(new InvalidOperationException("You must call SetPassword first."));
            }

            return Observable.Start(() => InMemoryTransform(block, decryptTransform), scheduler);
        }

        public IObservable<byte[]> EncryptBlock(byte[] block)
        {
            if (block == null)
            {
                throw new ArgumentNullException(nameof(block), "block can't be null");
            }

            if (encryptTransform == null)
            {
                return Observable.Throw<byte[]>(new InvalidOperationException("You must call SetPassword first."));
            }

            return Observable.Start(() => InMemoryTransform(block, encryptTransform), scheduler);
        }

        static byte[] InMemoryTransform(byte[] bytesToTransform, ICryptoTransform transform)
        {
            using (var memoryStream = new MemoryStream())
            {
                using (var cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write))
                {
                    cryptoStream.Write(bytesToTransform, 0, bytesToTransform.Length);
                }

                return memoryStream.ToArray();
            }
        }
    }

I register this implementation like this: Locator.CurrentMutable.RegisterConstant(new MyEncryptionProvider(), typeof(IEncryptionProvider)); before calling BlobCache.Secure

And I see the EncryptBlock and DecryptBlock being called when I use .GetOrFetchObject on that secure cache. So far so good.

The problem I run into is that it fails when trying to Deserialize with this error from Newtonsoft:

{Newtonsoft.Json.JsonReaderException: Read past end of current container context. Path ''.
  at Newtonsoft.Json.Bson.BsonReader.ReadNormal () [0x0013c] in <c19705166c7c4a608e182e859c4de6d2>:0 
  at Newtonsoft.Json.Bson.BsonReader.Read () [0x00033] in <c19705166c7c4a608e182e859c4de6d2>:0 
  at Newtonsoft.Json.JsonReader.ReadAndAssert () [0x00000] in <c19705166c7c4a608e182e859c4de6d2>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) [0x000b6] in <c19705166c7c4a608e182e859c4de6d2>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) [0x0006d] in <c19705166c7c4a608e182e859c4de6d2>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize (Newtonsoft.Json.JsonReader reader, System.Type objectType, System.Boolean checkAdditionalContent) [0x000db] in <c19705166c7c4a608e182e859c4de6d2>:0 
  at Newtonsoft.Json.JsonSerializer.DeserializeInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType) [0x00053] in <c19705166c7c4a608e182e859c4de6d2>:0 
  at Newtonsoft.Json.JsonSerializer.Deserialize (Newtonsoft.Json.JsonReader reader, System.Type objectType) [0x00000] in <c19705166c7c4a608e182e859c4de6d2>:0 
  at Newtonsoft.Json.JsonSerializer.Deserialize[T] (Newtonsoft.Json.JsonReader reader) [0x00000] in <c19705166c7c4a608e182e859c4de6d2>:0 
  at Akavache.Sqlite3.SQLitePersistentBlobCache.DeserializeObject[T] (System.Byte[] data) [0x00074] in <67aced6c5c1a4c15b03e120d7300429d>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Library/Frameworks/Xamarin.iOS.framework/Versions/10.12.0.20/src/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:151 
  at System.Reactive.PlatformServices.ExceptionServicesImpl.Rethrow (System.Exception exception) [0x00006] in <427ee2007f4d40bb9d3f1fcd73e9b750>:0 
  at System.Reactive.ExceptionHelpers.ThrowIfNotNull (System.Exception exception) [0x0000d] in <ab76b1b1678341d69f8fc2c1f68d0bf5>:0 
  at System.Reactive.Subjects.AsyncSubject`1[T].GetResult () [0x00039]

When the same object is cached using the BlobCache.LocalMachine the same data structure is successfully stored like this:

[
  {
    "Name": "John Smith",
    "Date": "2017-04-03T20:00:00-04:00",
    "Number1": -8820768.6192349959,
    "Number2": -25540081.8275,
    "Number3": -0.0045255076670528034,
    "Number4": 0.0457761358483606,
    "RelatedObjects": [],
    "LastUpdateTime": "2017-04-04T17:36:06.247-04:00"
  }
]

What am I missing here? Very frustrated, really hoping someone sees something I am not. Thanks for your help.

1

There are 1 answers

4
Dmitry Samuylov On BEST ANSWER

With the help of the example in this post: Xamarin.iOS / Akavache Working Example

I was able to get this problem resolved and a working example is shown in the sample solution here: https://github.com/dmitrysamuylov/xamarin-ios-akavache-secure-example

hope it helps someone in the future