Correct way to store encryption key for SqlCipher database
Asked Answered
S

2

10

I have a Xamarin application and have managed to download my data from my server to my device. I have also got it set up so that it can take a SqlCipher Encryption key to encrypt the data.

My question is where is the correct location to store my key that I use to encrypt this data? Is it to you KeyStore / KeyChain? Which mono classes should I be looking to use?

Stigmasterol answered 10/2, 2015 at 10:10 Comment(4)
As far as I know your best options are Keychain for iOS and KeyStore for Android. Mono example for KeychainOutherod
And also a Mono example for KeyStoreOutherod
@Outherod I have looked at the android mono example and it seems to be using the nuget Xamarin.Auth. Because I can't reference Xamarin.Auth in my PCL this means I can't use the methods from my PCL via an interface using dependency injection. I would therefore say this is not a good way to store encryption keys using XamarinStigmasterol
As far as I know Xamarin.Auth is using Keystore and keychain for storing auth tokens, you will need a custom implementation for your goal and the code above should give you some hints on how to use such native APIs in mono.Outherod
S
7

Due to the popularity of this question I am going to post my implementation of this:

PCL interface

public interface IAuth
{
    void CreateStore();
    IEnumerable<string> FindAccountsForService(string serviceId);
    void Save(string pin,string serviceId);
    void Delete(string serviceId);
}

Android

public class IAuthImplementation : IAuth
{
    Context context;
    KeyStore ks;
    KeyStore.PasswordProtection prot;

    static readonly object fileLock = new object();

    const string FileName = "MyProg.Accounts";
    static readonly char[] Password = null;

    public void CreateStore()
    {

        this.context = Android.App.Application.Context;

        ks = KeyStore.GetInstance(KeyStore.DefaultType);

        prot = new KeyStore.PasswordProtection(Password);

        try
        {
            lock (fileLock)
            {
                using (var s = context.OpenFileInput(FileName))
                {
                    ks.Load(s, Password);
                }
            }
        }
        catch (Java.IO.FileNotFoundException)
        {
            //ks.Load (null, Password);
            LoadEmptyKeyStore(Password);
        }
    }

    public IEnumerable<string> FindAccountsForService(string serviceId)
    {
        var r = new List<string>();

        var postfix = "-" + serviceId;

        var aliases = ks.Aliases();
        while (aliases.HasMoreElements)
        {
            var alias = aliases.NextElement().ToString();
            if (alias.EndsWith(postfix))
            {
                var e = ks.GetEntry(alias, prot) as KeyStore.SecretKeyEntry;
                if (e != null)
                {
                    var bytes = e.SecretKey.GetEncoded();
                    var password = System.Text.Encoding.UTF8.GetString(bytes);
                    r.Add(password);
                }
            }
        }
        return r;
    }

    public void Delete(string serviceId)
    {
        var alias = MakeAlias(serviceId);

        ks.DeleteEntry(alias);
        Save();
    }

    public void Save(string pin, string serviceId)
    {
        var alias = MakeAlias(serviceId);

        var secretKey = new SecretAccount(pin);
        var entry = new KeyStore.SecretKeyEntry(secretKey);
        ks.SetEntry(alias, entry, prot);

        Save();
    }

    void Save()
    {
        lock (fileLock)
        {
            using (var s = context.OpenFileOutput(FileName, FileCreationMode.Private))
            {
                ks.Store(s, Password);
            }
        }
    }

    static string MakeAlias(string serviceId)
    {
        return "-" + serviceId;
    }

    class SecretAccount : Java.Lang.Object, ISecretKey
    {
        byte[] bytes;
        public SecretAccount(string password)
        {
            bytes = System.Text.Encoding.UTF8.GetBytes(password);
        }
        public byte[] GetEncoded()
        {
            return bytes;
        }
        public string Algorithm
        {
            get
            {
                return "RAW";
            }
        }
        public string Format
        {
            get
            {
                return "RAW";
            }
        }
    }

    static IntPtr id_load_Ljava_io_InputStream_arrayC;

    void LoadEmptyKeyStore(char[] password)
    {
        if (id_load_Ljava_io_InputStream_arrayC == IntPtr.Zero)
        {
            id_load_Ljava_io_InputStream_arrayC = JNIEnv.GetMethodID(ks.Class.Handle, "load", "(Ljava/io/InputStream;[C)V");
        }
        IntPtr intPtr = IntPtr.Zero;
        IntPtr intPtr2 = JNIEnv.NewArray(password);
        JNIEnv.CallVoidMethod(ks.Handle, id_load_Ljava_io_InputStream_arrayC, new JValue[]
            {
                new JValue (intPtr),
                new JValue (intPtr2)
            });
        JNIEnv.DeleteLocalRef(intPtr);
        if (password != null)
        {
            JNIEnv.CopyArray(intPtr2, password);
            JNIEnv.DeleteLocalRef(intPtr2);
        }
    }

Call Create Store in the main activity of Android app first. - This could possibly be improved and remove CreateStrore() from the interface by checking if ks == null in Save and Delete and calling the method if true

iOS

public class IAuthImplementation : IAuth
{
    public IEnumerable<string> FindAccountsForService(string serviceId)
    {
        var query = new SecRecord(SecKind.GenericPassword);
        query.Service = serviceId;

        SecStatusCode result;
        var records = SecKeyChain.QueryAsRecord(query, 1000, out result);

        return records != null ?
            records.Select(GetAccountFromRecord).ToList() :
            new List<string>();
    }

    public void Save(string pin, string serviceId)
    {
        var statusCode = SecStatusCode.Success;
        var serializedAccount = pin;
        var data = NSData.FromString(serializedAccount, NSStringEncoding.UTF8);

        //
        // Remove any existing record
        //
        var existing = FindAccount(serviceId);

        if (existing != null)
        {
            var query = new SecRecord(SecKind.GenericPassword);
            query.Service = serviceId;

            statusCode = SecKeyChain.Remove(query);
            if (statusCode != SecStatusCode.Success)
            {
                throw new Exception("Could not save account to KeyChain: " + statusCode);
            }
        }

        //
        // Add this record
        //
        var record = new SecRecord(SecKind.GenericPassword);
        record.Service = serviceId;
        record.Generic = data;
        record.Accessible = SecAccessible.WhenUnlocked;

        statusCode = SecKeyChain.Add(record);

        if (statusCode != SecStatusCode.Success)
        {
            throw new Exception("Could not save account to KeyChain: " + statusCode);
        }
    }

    public void Delete(string serviceId)
    {
        var query = new SecRecord(SecKind.GenericPassword);
        query.Service = serviceId;

        var statusCode = SecKeyChain.Remove(query);

        if (statusCode != SecStatusCode.Success)
        {
            throw new Exception("Could not delete account from KeyChain: " + statusCode);
        }
    }

    string GetAccountFromRecord(SecRecord r)
    {
        return NSString.FromData(r.Generic, NSStringEncoding.UTF8);
    }

    string FindAccount(string serviceId)
    {
        var query = new SecRecord(SecKind.GenericPassword);
        query.Service = serviceId;

        SecStatusCode result;
        var record = SecKeyChain.QueryAsRecord(query, out result);

        return record != null ? GetAccountFromRecord(record) : null;
    }

    public void CreateStore()
    {
        throw new NotImplementedException();
    }
}

WP

public class IAuthImplementation : IAuth
{
    public IEnumerable<string> FindAccountsForService(string serviceId)
    {
        using (var store = IsolatedStorageFile.GetUserStoreForApplication())
        {
            string[] auths = store.GetFileNames("MyProg");
            foreach (string path in auths)
            {
                using (var stream = new BinaryReader(new IsolatedStorageFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, store)))
                {
                    int length = stream.ReadInt32();
                    byte[] data = stream.ReadBytes(length);

                    byte[] unprot = ProtectedData.Unprotect(data, null);
                    yield return Encoding.UTF8.GetString(unprot, 0, unprot.Length);
                }
            }
        }
    }

    public void Save(string pin, string serviceId)
    {
        byte[] data = Encoding.UTF8.GetBytes(pin);
        byte[] prot = ProtectedData.Protect(data, null);

        var path = GetAccountPath(serviceId);

        using (var store = IsolatedStorageFile.GetUserStoreForApplication())
        using (var stream = new IsolatedStorageFileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, store))
        {
            stream.WriteAsync(BitConverter.GetBytes(prot.Length), 0, sizeof(int)).Wait();
            stream.WriteAsync(prot, 0, prot.Length).Wait();
        }
    }

    public void Delete(string serviceId)
    {
        var path = GetAccountPath(serviceId);
        using (var store = IsolatedStorageFile.GetUserStoreForApplication())
        {
            store.DeleteFile(path);
        }
    }

    private string GetAccountPath(string serviceId)
    {
        return String.Format("{0}", serviceId);
    }

    public void CreateStore()
    {
        throw new NotImplementedException();
    }
}

This is an adaptation of the Xamarin.Auth library (Found Here) but removes the dependency from the Xamarin.Auth library to provide cross platform use through the interface in the PCL. For this reason I have simplified it to only save one string. This is probably not the best implementation but it works in my case. Feel free to expand upon this

Stigmasterol answered 17/2, 2015 at 9:10 Comment(6)
Thanks for sharing this, just out of interest how do you handle the pin? Is this a user entered value? Where do you store that value? Thanks again.Courtesy
@Courtesy yes the pin is a user entered value. This code stores the value in the KeyStore on Android, KeyChain on iOS and IsolatedStorage on WP, they are all basically the same thing and all 3 are encrypted making it secureStigmasterol
I understood that bit. I'm building something similar at the moment. My question was really about whether you then store this pin or prompt for it any time it's needed?Courtesy
@Courtesy In my case ask the user to create a pin initially and store it. Then on application start up I prompt the user for the pin and compare it the the stored version to unlock my applicationStigmasterol
Gotcha, that does seem to be a common approach. Thanks for your time chap.Courtesy
Thanks for sharing. How do you handle when someone tries to unlock an existing Keystore with the wrong password?Bag
A
2

There is a nuget package called KeyChain.NET that encapsulated this logic for iOs, Android and Windows Phone.

It's open source and you have find sample at its github repository

More info at this blog post

Arguello answered 9/5, 2016 at 23:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.