SqlCipher数据库存储加密密钥的正确方法

10
我有一个Xamarin应用程序,并成功将数据从服务器下载到设备上。 我还设置好了使用SqlCipher加密密钥来加密数据。我的问题是,存储用于加密这些数据的密钥的正确位置在哪里?是要使用KeyStore / KeyChain吗?我应该使用哪些mono类?

据我所知,iOS 的最佳选项是 Keychain,而 Android 的最佳选项是 KeyStore。Keychain 的 Mono 示例 - Wizche
并且还有一个 KeyStore 的Mono示例 - Wizche
@Wizche 我看了一下 Android Mono 示例,它似乎使用了 nuget 的 Xamarin.Auth。由于我无法在我的PCL中引用Xamarin.Auth,这意味着我无法通过接口使用依赖注入从我的PCL中使用该方法。因此,我认为这不是使用 Xamarin 存储加密密钥的好方法。 - JKennedy
据我所知,Xamarin.Auth使用Keystore和keychain来存储身份验证令牌,您需要自定义实现以实现您的目标,并且上面的代码应该为您提供了如何在mono中使用此类本机API的一些提示。 - Wizche
2个回答

7
由于这个问题很受欢迎,我要发布我的实现方式:
PCL接口
public interface IAuth
{
    void CreateStore();
    IEnumerable<string> FindAccountsForService(string serviceId);
    void Save(string pin,string serviceId);
    void Delete(string serviceId);
}

安卓系统

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);
        }
    }

请在Android应用的主活动中首先调用Create Store。- 通过在Save和Delete中检查ks == null并在为真时调用该方法,可以改进此过程并从接口中删除CreateStrore()。

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();
    }
}

这是 Xamarin.Auth 库的改编版(在这里找到),但移除了对 Xamarin.Auth 库的依赖,通过 PCL 中的接口提供跨平台使用。因此,我将其简化为只保存一个字符串。虽然这可能不是最佳实现,但在我的情况下可以正常工作。欢迎扩展此功能。

谢谢分享,只是出于兴趣,您如何处理引脚?这是用户输入的值吗?您将该值存储在哪里?再次感谢。 - Jammer
@Jammer 是的,该引脚是由用户输入的值。此代码将该值存储在Android上的KeyStore,iOS上的KeyChain和WP上的IsolatedStorage中,它们基本上都是相同的东西,并且所有3个都经过加密,因此非常安全。 - JKennedy
我理解了那一部分。我目前正在构建类似的东西。我的问题实际上是关于您是否存储此 PIN 或在每次需要时提示它? - Jammer
@Jammer 在我的情况下,要求用户最初创建一个 PIN 码并将其存储。然后,在应用程序启动时,我提示用户输入 PIN 码并将其与存储的版本进行比较以解锁我的应用程序。 - JKennedy
明白了,那似乎是一种常见的方法。谢谢你的时间,伙计。 - Jammer
谢谢分享。当有人试图使用错误的密码解锁现有的密钥库时,您如何处理? - AhmedW

2
有一个名为KeyChain.NET的NuGet包,它封装了iOs、Android和Windows Phone的逻辑。这是开源的,你可以在它的Github库中找到示例。更多信息请参阅这篇博客文章。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接