Xamarin.Forms, Akavache and I: ensuring protection of sensitive data
Recap
Some of you might remember my posts about encryption for Android, iOS and Windows 10. If not, take a look here:
Xamarin Android: asymmetric encryption without any user input or hardcoded values
How to perform asymmetric encryption without user input/hardcoded values with Xamarin iOS
It is no coincidence that I wrote these three posts before starting with this Akavache series, as we’ll use those techniques to protect sensitive data with Akavache. So you might have a look first before you read on.
Creating a secure blob cache in Akavache
Akavache has a special type for saving sensitive data – based on the interface ISecureBlobCache
. The first step is to extend the IBlobCacheInstanceHelper
interface we implemented in the first post of this series:
1
2
3
4
5
6
7
8
public interface IBlobCacheInstanceHelper
{
void Init();
IBlobCache LocalMachineCache { get; set; }
ISecureBlobCache SecretLocalMachineCache { get; set; }
}
Of course, all three platform implementations of the IBlobCacheInstanceHelper
interface need to be updated as well. The code to add for all three platform is the same:
1
2
3
4
5
6
7
8
9
10
11
12
public ISecureBlobCache SecretLocalMachineCache { get; set; }
private void GetSecretLocalMachineCache()
{
var secretCache = new Lazy<ISecureBlobCache>(() =>
{
_filesystemProvider.CreateRecursive(_filesystemProvider.GetDefaultSecretCacheDirectory()).SubscribeOn(BlobCache.TaskpoolScheduler).Wait();
return new SQLiteEncryptedBlobCache(Path.Combine(_filesystemProvider.GetDefaultSecretCacheDirectory(), "secret.db"), new PlatformCustomAkavacheEncryptionProvider(), BlobCache.TaskpoolScheduler);
});
this.SecretLocalMachineCache = secretCache.Value;
}
As we will use the same name for all platform implementations, that’s already all we have to do here.
Platform specific encryption provider
Implementing the platform specific code is nothing new. Way before I used Akavache, others have already implemented solutions. The main issue is that there is no platform implementation for Android and iOS (and maybe others). My solution is inspired by this blog post by Kent Boogart, which is (as far as I can see), also broadly accepted amongst the community. The only thing I disliked about it was the requirement for a password – which either would be something reversible or causing a (maybe) bad user experience.
Akavache provides the IEncryptionProvider
interface, which contains two methods. One for encryption, the other one for decryption. Those two methods are working with byte[]
both for input and output. You should be aware and know how to convert your data to that.
Implementing the IEncryptionProvider interface
The implementation of Akavache’s encryption interface is following the same principle on all three platforms.
- provide a reference to the internal
TaskpoolScheduler
in the constructor - get an instance of our platform specific encryption provider
- get or create keys (Android and iOS)
- provide helper methods that perform encryption/decryption
Let’s have a look at the platform implementations. I will show the full class implementation and remarking them afterwards.
Android
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
[assembly: Xamarin.Forms.Dependency(typeof(PlatformCustomAkavacheEncryptionProvider))]
namespace XfAkavacheAndI.Android.PlatformImplementations
{
public class PlatformCustomAkavacheEncryptionProvider : IEncryptionProvider
{
private readonly IScheduler _scheduler;
private static readonly string KeyStoreName = $"{BlobCache.ApplicationName.ToLower()}_secureStore";
private readonly PlatformEncryptionKeyHelper _encryptionKeyHelper;
private const string TRANSFORMATION = "RSA/ECB/PKCS1Padding";
private IKey _privateKey = null;
private IKey _publicKey = null;
public PlatformCustomAkavacheEncryptionProvider()
{
_scheduler = BlobCache.TaskpoolScheduler ?? throw new ArgumentNullException(nameof(_scheduler), "Scheduler is null");
_encryptionKeyHelper = new PlatformEncryptionKeyHelper(Application.Context, KeyStoreName);
GetOrCreateKeys();
}
public IObservable<byte[]> DecryptBlock(byte[] block)
{
if (block == null)
{
throw new ArgumentNullException(nameof(block), "block cannot be null");
}
return Observable.Start(() => Decrypt(block), _scheduler);
}
public IObservable<byte[]> EncryptBlock(byte[] block)
{
if (block == null)
{
throw new ArgumentNullException(nameof(block), "block cannot be null");
}
return Observable.Start(() => Encrypt(block), _scheduler);
}
private void GetOrCreateKeys()
{
if (!_encryptionKeyHelper.KeysExist())
_encryptionKeyHelper.CreateKeyPair();
_privateKey = _encryptionKeyHelper.GetPrivateKey();
_publicKey = _encryptionKeyHelper.GetPublicKey();
}
public byte[] Encrypt(byte[] rawBytes)
{
if (_publicKey == null)
{
throw new ArgumentNullException(nameof(_publicKey), "Public key cannot be null");
}
var cipher = Cipher.GetInstance(TRANSFORMATION);
cipher.Init(CipherMode.EncryptMode, _publicKey);
return cipher.DoFinal(rawBytes);
}
public byte[] Decrypt(byte[] encyrptedBytes)
{
if (_privateKey == null)
{
throw new ArgumentNullException(nameof(_privateKey), "Private key cannot be null");
}
var cipher = Cipher.GetInstance(TRANSFORMATION);
cipher.Init(CipherMode.DecryptMode, _privateKey);
return cipher.DoFinal(encyrptedBytes);
}
}
As you can see, I am getting Akavache’s internal TaskpoolScheduler
in the constructor, like initial stated. Then, for this sample, I am using RSA encryption. The helper methods pretty much implement the same code like in the post about my KeyStore implementation. The only thing to do is to use these methods in the EncryptBlock and DecyrptBlock method implementations, which is done asynchronously via Observable.Start.
iOS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
[assembly: Xamarin.Forms.Dependency(typeof(PlatformCustomAkavacheEncryptionProvider))]
namespace XfAkavacheAndI.iOS.PlatformImplementations
{
public class PlatformCustomAkavacheEncryptionProvider : IEncryptionProvider
{
private readonly IScheduler _scheduler;
private readonly PlatformEncryptionKeyHelper _encryptionKeyHelper;
private SecKey _privateKey = null;
private SecKey _publicKey = null;
public PlatformCustomAkavacheEncryptionProvider()
{
_scheduler = BlobCache.TaskpoolScheduler ??
throw new ArgumentNullException(nameof(_scheduler), "Scheduler is null");
_encryptionKeyHelper = new PlatformEncryptionKeyHelper(BlobCache.ApplicationName.ToLower());
GetOrCreateKeys();
}
public IObservable<byte[]> DecryptBlock(byte[] block)
{
if (block == null)
{
throw new ArgumentNullException(nameof(block), "block can't be null");
}
return Observable.Start(() => Decrypt(block), _scheduler);
}
public IObservable<byte[]> EncryptBlock(byte[] block)
{
if (block == null)
{
throw new ArgumentNullException(nameof(block), "block can't be null");
}
return Observable.Start(() => Encrypt(block), _scheduler);
}
private void GetOrCreateKeys()
{
if (!_encryptionKeyHelper.KeysExist())
_encryptionKeyHelper.CreateKeyPair();
_privateKey = _encryptionKeyHelper.GetPrivateKey();
_publicKey = _encryptionKeyHelper.GetPublicKey();
}
private byte[] Encrypt(byte[] rawBytes)
{
if (_publicKey == null)
{
throw new ArgumentNullException(nameof(_publicKey), "Public key cannot be null");
}
var code = _publicKey.Encrypt(SecPadding.PKCS1, rawBytes, out var encryptedBytes);
return code == SecStatusCode.Success ? encryptedBytes : null;
}
private byte[] Decrypt(byte[] encyrptedBytes)
{
if (_privateKey == null)
{
throw new ArgumentNullException(nameof(_privateKey), "Private key cannot be null");
}
var code = _privateKey.Decrypt(SecPadding.PKCS1, encyrptedBytes, out var decryptedBytes);
return code == SecStatusCode.Success ? decryptedBytes : null;
}
}
}
The iOS implementation follows the same schema as the Android implementation. However, iOS uses the KeyChain, which makes the encryption helper methods itself different.
UWP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
[assembly: Xamarin.Forms.Dependency(typeof(PlatformCustomAkavacheEncryptionProvider))]
namespace XfAkavacheAndI.UWP.PlatformImplementations
{
public class PlatformCustomAkavacheEncryptionProvider : IEncryptionProvider
{
private readonly IScheduler _scheduler;
private string _localUserDescriptor = "LOCAL=user";
private string _localMachineDescriptor = "LOCAL=machine";
public bool UseForAllUsers { get; set; } = false;
public PlatformCustomAkavacheEncryptionProvider()
{
_scheduler = BlobCache.TaskpoolScheduler ??
throw new ArgumentNullException(nameof(_scheduler), "Scheduler is null");
}
public IObservable<byte[]> EncryptBlock(byte[] block)
{
if (block == null)
{
throw new ArgumentNullException(nameof(block), "block can't be null");
}
return Observable.Start(() => Encrypt(block).GetAwaiter().GetResult(), _scheduler);
}
public IObservable<byte[]> DecryptBlock(byte[] block)
{
if (block == null)
{
throw new ArgumentNullException(nameof(block), "block can't be null");
}
return Observable.Start(() => Decrypt(block).GetAwaiter().GetResult(), _scheduler);
}
public async Task<byte[]> Encrypt(byte[] data)
{
var provider = new DataProtectionProvider(UseForAllUsers ? _localMachineDescriptor : _localUserDescriptor);
var contentBuffer = CryptographicBuffer.CreateFromByteArray(data);
var contentInputStream = new InMemoryRandomAccessStream();
var protectedContentStream = new InMemoryRandomAccessStream();
//storing data in the stream
IOutputStream outputStream = contentInputStream.GetOutputStreamAt(0);
var dataWriter = new DataWriter(outputStream);
dataWriter.WriteBuffer(contentBuffer);
await dataWriter.StoreAsync();
await dataWriter.FlushAsync();
//reopening in input mode
IInputStream encodingInputStream = contentInputStream.GetInputStreamAt(0);
IOutputStream protectedOutputStream = protectedContentStream.GetOutputStreamAt(0);
await provider.ProtectStreamAsync(encodingInputStream, protectedOutputStream);
await protectedOutputStream.FlushAsync();
//verify that encryption happened
var inputReader = new DataReader(contentInputStream.GetInputStreamAt(0));
var protectedReader = new DataReader(protectedContentStream.GetInputStreamAt(0));
await inputReader.LoadAsync((uint)contentInputStream.Size);
await protectedReader.LoadAsync((uint)protectedContentStream.Size);
var inputBuffer = inputReader.ReadBuffer((uint)contentInputStream.Size);
var protectedBuffer = protectedReader.ReadBuffer((uint)protectedContentStream.Size);
if (!CryptographicBuffer.Compare(inputBuffer, protectedBuffer))
{
return protectedBuffer.ToArray();
}
else
{
return null;
}
}
public async Task<byte[]> Decrypt(byte[] encryptedBytes)
{
var provider = new DataProtectionProvider();
var encryptedContentBuffer = CryptographicBuffer.CreateFromByteArray(encryptedBytes);
var contentInputStream = new InMemoryRandomAccessStream();
var unprotectedContentStream = new InMemoryRandomAccessStream();
IOutputStream outputStream = contentInputStream.GetOutputStreamAt(0);
var dataWriter = new DataWriter(outputStream);
dataWriter.WriteBuffer(encryptedContentBuffer);
await dataWriter.StoreAsync();
await dataWriter.FlushAsync();
IInputStream decodingInputStream = contentInputStream.GetInputStreamAt(0);
IOutputStream protectedOutputStream = unprotectedContentStream.GetOutputStreamAt(0);
await provider.UnprotectStreamAsync(decodingInputStream, protectedOutputStream);
await protectedOutputStream.FlushAsync();
DataReader reader2 = new DataReader(unprotectedContentStream.GetInputStreamAt(0));
await reader2.LoadAsync((uint)unprotectedContentStream.Size);
IBuffer unprotectedBuffer = reader2.ReadBuffer((uint)unprotectedContentStream.Size);
return unprotectedBuffer.ToArray();
}
}
}
Last but not least, we have also an implementation for Windows applications. It is using the DataProtection API, which does handle all that key stuff and let’s us focus on the encryption itself. As the API is asynchronously, I am using .GetAwaiter().GetResult()
Task extensions to make it compatible with Observable.Start
.
Conclusion
Using the implementations above paired with our instance helper makes it easy to protect data in our apps. With all those data breach scandals and law changes around, this is one possible way secure way to handle sensitive data, as we do not have hardcoded values or any user interaction involved.
For better understanding of all that code, I made a sample project available that has all the referenced and mentioned classes implemented. Feel free to fork it and play with it (or even give me some feedback). For using the implementations, please refer to my post about common usages I wrote a few days ago. The only difference is that you would use SecretLocalMachineCache
instead of LocalMachineCache
for sensitive data.
As always, I hope this post is helpful for some of you.
Until the next post, happy coding!
P.S. Feel free to download my official app for msicc.net, which – of course – uses the implementations above:
iOS Android Windows 10