From 8c6823c4639f1bfcac774ee2870610a14f4d5cac Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Apr 2019 21:23:16 -0400 Subject: [PATCH] key chain storage service --- .../Services/PreferencesStorageService.cs | 3 +- src/Core/Services/SecureStorageService.cs | 8 +- src/iOS/Services/KeyChainStorageService.cs | 127 ++++++++++++++++++ src/iOS/iOS.csproj | 1 + 4 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 src/iOS/Services/KeyChainStorageService.cs diff --git a/src/Core/Services/PreferencesStorageService.cs b/src/Core/Services/PreferencesStorageService.cs index bfc7e4f86..3cf7d397e 100644 --- a/src/Core/Services/PreferencesStorageService.cs +++ b/src/Core/Services/PreferencesStorageService.cs @@ -8,8 +8,7 @@ namespace Bit.Core.Services { public class PreferencesStorageService : IStorageService { - private string _keyFormat = "bwPreferencesStorage:{0}"; - + private readonly string _keyFormat = "bwPreferencesStorage:{0}"; private readonly string _sharedName; private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings { diff --git a/src/Core/Services/SecureStorageService.cs b/src/Core/Services/SecureStorageService.cs index f4a01aabf..a79c5a33c 100644 --- a/src/Core/Services/SecureStorageService.cs +++ b/src/Core/Services/SecureStorageService.cs @@ -7,7 +7,7 @@ namespace Bit.Core.Services { public class SecureStorageService : IStorageService { - private string _keyFormat = "bwSecureStorage:{0}"; + private readonly string _keyFormat = "bwSecureStorage:{0}"; private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() @@ -17,8 +17,7 @@ namespace Bit.Core.Services { var formattedKey = string.Format(_keyFormat, key); var val = await Xamarin.Essentials.SecureStorage.GetAsync(formattedKey); - var objType = typeof(T); - if(objType == typeof(string)) + if(typeof(T) == typeof(string)) { return (T)(object)val; } @@ -36,8 +35,7 @@ namespace Bit.Core.Services return; } var formattedKey = string.Format(_keyFormat, key); - var objType = typeof(T); - if(objType == typeof(string)) + if(typeof(T) == typeof(string)) { await Xamarin.Essentials.SecureStorage.SetAsync(formattedKey, obj as string); } diff --git a/src/iOS/Services/KeyChainStorageService.cs b/src/iOS/Services/KeyChainStorageService.cs new file mode 100644 index 000000000..fa44bbce9 --- /dev/null +++ b/src/iOS/Services/KeyChainStorageService.cs @@ -0,0 +1,127 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Foundation; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Security; + +namespace Bit.iOS.Services +{ + public class KeyChainStorageService : IStorageService + { + private readonly string _keyFormat = "bwKeyChainStorage:{0}"; + private readonly string _service; + private readonly string _group; + private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + public KeyChainStorageService(string service, string group) + { + _service = service; + _group = group; + } + + public Task GetAsync(string key) + { + var formattedKey = string.Format(_keyFormat, key); + byte[] dataBytes = null; + using(var existingRecord = GetKeyRecord(formattedKey)) + using(var record = SecKeyChain.QueryAsRecord(existingRecord, out SecStatusCode resultCode)) + { + if(resultCode == SecStatusCode.ItemNotFound) + { + return Task.FromResult((T)(object)null); + } + + CheckError(resultCode); + dataBytes = record.Generic.ToArray(); + } + + var dataString = Encoding.UTF8.GetString(dataBytes); + if(typeof(T) == typeof(string)) + { + return Task.FromResult((T)(object)dataString); + } + else + { + return Task.FromResult(JsonConvert.DeserializeObject(dataString, _jsonSettings)); + } + } + + public async Task SaveAsync(string key, T obj) + { + if(obj == null) + { + await RemoveAsync(key); + return; + } + + string dataString = null; + if(typeof(T) == typeof(string)) + { + dataString = obj as string; + } + else + { + dataString = JsonConvert.SerializeObject(obj, _jsonSettings); + } + + var formattedKey = string.Format(_keyFormat, key); + var dataBytes = Encoding.UTF8.GetBytes(dataString); + using(var data = NSData.FromArray(dataBytes)) + using(var newRecord = GetKeyRecord(formattedKey, data)) + { + await RemoveAsync(formattedKey); + CheckError(SecKeyChain.Add(newRecord)); + } + } + + public Task RemoveAsync(string key) + { + var formattedKey = string.Format(_keyFormat, key); + using(var record = GetExistingRecord(formattedKey)) + { + if(record != null) + { + CheckError(SecKeyChain.Remove(record)); + } + } + return Task.FromResult(0); + } + + private SecRecord GetKeyRecord(string key, NSData data = null) + { + var record = new SecRecord(SecKind.GenericPassword) + { + Service = _service, + Account = key, + AccessGroup = _group + }; + if(data != null) + { + record.Generic = data; + } + return record; + } + + private SecRecord GetExistingRecord(string key) + { + var existingRecord = GetKeyRecord(key); + SecKeyChain.QueryAsRecord(existingRecord, out SecStatusCode resultCode); + return resultCode == SecStatusCode.Success ? existingRecord : null; + } + + private void CheckError(SecStatusCode resultCode, [CallerMemberName] string caller = null) + { + if(resultCode != SecStatusCode.Success) + { + throw new Exception(string.Format("Failed to execute {0}. Result code: {1}", caller, resultCode)); + } + } + } +} diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index d166cb296..9ba949d92 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -93,6 +93,7 @@ +