mirror of
https://github.com/bitwarden/mobile.git
synced 2025-02-16 01:11:25 +01:00
encrypted private key and org keys at rest
This commit is contained in:
parent
15a9f80430
commit
c8219b29c0
@ -10,5 +10,6 @@ namespace Bit.App.Abstractions
|
||||
Task<ApiResult> PostPasswordHintAsync(PasswordHintRequest requestObj);
|
||||
Task<ApiResult<DateTime?>> GetAccountRevisionDateAsync();
|
||||
Task<ApiResult<ProfileResponse>> GetProfileAsync();
|
||||
Task<ApiResult<KeysResponse>> GetKeys();
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Models.Api;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@ -10,13 +11,13 @@ namespace Bit.App.Abstractions
|
||||
SymmetricCryptoKey PreviousKey { get; }
|
||||
bool KeyChanged { get; }
|
||||
byte[] PrivateKey { get; }
|
||||
IDictionary<string, SymmetricCryptoKey> OrgKeys { get; set; }
|
||||
IDictionary<string, SymmetricCryptoKey> OrgKeys { get; }
|
||||
|
||||
void SetPrivateKey(CipherString privateKeyEnc, SymmetricCryptoKey key);
|
||||
void SetPrivateKey(CipherString privateKeyEnc);
|
||||
void SetOrgKeys(ProfileResponse profile);
|
||||
void SetOrgKeys(Dictionary<string, string> orgKeysEncDict);
|
||||
SymmetricCryptoKey GetOrgKey(string orgId);
|
||||
void ClearOrgKey(string orgId);
|
||||
void ClearKeys();
|
||||
SymmetricCryptoKey AddOrgKey(string orgId, CipherString encOrgKey, byte[] privateKey);
|
||||
string Decrypt(CipherString encyptedValue, SymmetricCryptoKey key = null);
|
||||
byte[] DecryptToBytes(CipherString encyptedValue, SymmetricCryptoKey key = null);
|
||||
byte[] RsaDecryptToBytes(CipherString encyptedValue, byte[] privateKey);
|
||||
|
@ -107,6 +107,7 @@
|
||||
<Compile Include="Models\Api\Response\DeviceResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\LoginResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\ProfileOrganizationResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\KeysResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\TokenResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\ProfileResponse.cs" />
|
||||
<Compile Include="Models\Api\LoginDataModel.cs" />
|
||||
|
8
src/App/Models/Api/Response/KeysResponse.cs
Normal file
8
src/App/Models/Api/Response/KeysResponse.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class KeysResponse
|
||||
{
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
}
|
||||
}
|
@ -173,5 +173,45 @@ namespace Bit.App.Repositories
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<ApiResult<KeysResponse>> GetKeys()
|
||||
{
|
||||
if(!Connectivity.IsConnected)
|
||||
{
|
||||
return HandledNotConnected<KeysResponse>();
|
||||
}
|
||||
|
||||
var tokenStateResponse = await HandleTokenStateAsync<KeysResponse>();
|
||||
if(!tokenStateResponse.Succeeded)
|
||||
{
|
||||
return tokenStateResponse;
|
||||
}
|
||||
|
||||
using(var client = HttpService.Client)
|
||||
{
|
||||
var requestMessage = new TokenHttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/keys")),
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return await HandleErrorAsync<KeysResponse>(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var responseObj = JsonConvert.DeserializeObject<KeysResponse>(responseContent);
|
||||
return ApiResult<KeysResponse>.Success(responseObj, response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException<KeysResponse>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +275,7 @@ namespace Bit.App.Services
|
||||
{
|
||||
if(response.PrivateKey != null)
|
||||
{
|
||||
_cryptoService.SetPrivateKey(new CipherString(response.PrivateKey), key);
|
||||
_cryptoService.SetPrivateKey(new CipherString(response.PrivateKey));
|
||||
}
|
||||
|
||||
_cryptoService.Key = key;
|
||||
@ -288,25 +288,10 @@ namespace Bit.App.Services
|
||||
if(response.PrivateKey != null)
|
||||
{
|
||||
var profile = await _accountsApiRepository.GetProfileAsync();
|
||||
var orgKeysDict = new Dictionary<string, SymmetricCryptoKey>();
|
||||
|
||||
if(profile.Succeeded && (profile.Result.Organizations?.Any() ?? false))
|
||||
if(profile.Succeeded)
|
||||
{
|
||||
foreach(var org in profile.Result.Organizations)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decBytes = _cryptoService.RsaDecryptToBytes(new CipherString(org.Key), null);
|
||||
orgKeysDict.Add(org.Id, new SymmetricCryptoKey(decBytes));
|
||||
}
|
||||
catch
|
||||
{
|
||||
Debug.WriteLine($"Cannot set org key {org.Id}. Decryption failed.");
|
||||
}
|
||||
}
|
||||
_cryptoService.SetOrgKeys(profile.Result);
|
||||
}
|
||||
|
||||
_cryptoService.OrgKeys = orgKeysDict;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ using System.Linq;
|
||||
using Bit.App.Enums;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Plugin.Settings.Abstractions;
|
||||
using Bit.App.Models.Api;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
@ -15,10 +17,11 @@ namespace Bit.App.Services
|
||||
{
|
||||
private const string KeyKey = "key";
|
||||
private const string PreviousKeyKey = "previousKey";
|
||||
private const string PrivateKeyKey = "privateKey";
|
||||
private const string OrgKeysKey = "orgKeys";
|
||||
private const string PrivateKeyKey = "encPrivateKey";
|
||||
private const string OrgKeysKey = "encOrgKeys";
|
||||
private const int InitializationVectorSize = 16;
|
||||
|
||||
private readonly ISettings _settings;
|
||||
private readonly ISecureStorageService _secureStorage;
|
||||
private readonly IKeyDerivationService _keyDerivationService;
|
||||
private SymmetricCryptoKey _key;
|
||||
@ -28,9 +31,11 @@ namespace Bit.App.Services
|
||||
private byte[] _privateKey;
|
||||
|
||||
public CryptoService(
|
||||
ISettings settings,
|
||||
ISecureStorageService secureStorage,
|
||||
IKeyDerivationService keyDerivationService)
|
||||
{
|
||||
_settings = settings;
|
||||
_secureStorage = secureStorage;
|
||||
_keyDerivationService = keyDerivationService;
|
||||
}
|
||||
@ -113,45 +118,47 @@ namespace Bit.App.Services
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_privateKey == null && _secureStorage.Contains(PrivateKeyKey))
|
||||
if(_privateKey == null && _settings.Contains(PrivateKeyKey))
|
||||
{
|
||||
_privateKey = _secureStorage.Retrieve(PrivateKeyKey);
|
||||
var encPrivateKey = _settings.GetValueOrDefault<string>(PrivateKeyKey);
|
||||
var encPrivateKeyCs = new CipherString(encPrivateKey);
|
||||
try
|
||||
{
|
||||
_privateKey = DecryptToBytes(encPrivateKeyCs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_privateKey = null;
|
||||
Debug.WriteLine($"Cannot set private key. Decryption failed.");
|
||||
}
|
||||
}
|
||||
|
||||
return _privateKey;
|
||||
}
|
||||
private set
|
||||
{
|
||||
if(value != null)
|
||||
{
|
||||
_secureStorage.Store(PrivateKeyKey, value);
|
||||
_privateKey = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_secureStorage.Delete(PrivateKeyKey);
|
||||
_privateKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<string, SymmetricCryptoKey> OrgKeys
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_orgKeys == null && _secureStorage.Contains(OrgKeysKey))
|
||||
if((!_orgKeys?.Any() ?? true) && _settings.Contains(OrgKeysKey))
|
||||
{
|
||||
var orgKeysDictBytes = _secureStorage.Retrieve(OrgKeysKey);
|
||||
if(orgKeysDictBytes != null)
|
||||
var orgKeysEncDictJson = _settings.GetValueOrDefault<string>(OrgKeysKey);
|
||||
if(!string.IsNullOrWhiteSpace(orgKeysEncDictJson))
|
||||
{
|
||||
var orgKeysDictJson = Encoding.UTF8.GetString(orgKeysDictBytes, 0, orgKeysDictBytes.Length);
|
||||
if(!string.IsNullOrWhiteSpace(orgKeysDictJson))
|
||||
_orgKeys = new Dictionary<string, SymmetricCryptoKey>();
|
||||
var orgKeysDict = JsonConvert.DeserializeObject<IDictionary<string, string>>(orgKeysEncDictJson);
|
||||
foreach(var item in orgKeysDict)
|
||||
{
|
||||
_orgKeys = new Dictionary<string, SymmetricCryptoKey>();
|
||||
var orgKeysDict = JsonConvert.DeserializeObject<IDictionary<string, byte[]>>(orgKeysDictJson);
|
||||
foreach(var item in orgKeysDict)
|
||||
try
|
||||
{
|
||||
_orgKeys.Add(item.Key, new SymmetricCryptoKey(item.Value));
|
||||
var orgKeyCs = new CipherString(item.Value);
|
||||
var decOrgKeyBytes = RsaDecryptToBytes(orgKeyCs, PrivateKey);
|
||||
_orgKeys.Add(item.Key, new SymmetricCryptoKey(decOrgKeyBytes));
|
||||
}
|
||||
catch
|
||||
{
|
||||
Debug.WriteLine($"Cannot set org key {item.Key}. Decryption failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,33 +166,56 @@ namespace Bit.App.Services
|
||||
|
||||
return _orgKeys;
|
||||
}
|
||||
set
|
||||
{
|
||||
if(value != null && value.Any())
|
||||
{
|
||||
var dict = new Dictionary<string, byte[]>();
|
||||
foreach(var item in value)
|
||||
{
|
||||
dict.Add(item.Key, item.Value.Key);
|
||||
}
|
||||
}
|
||||
|
||||
var dictJson = JsonConvert.SerializeObject(dict);
|
||||
var dictBytes = Encoding.UTF8.GetBytes(dictJson);
|
||||
_secureStorage.Store(OrgKeysKey, dictBytes);
|
||||
_orgKeys = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_secureStorage.Delete(OrgKeysKey);
|
||||
_orgKeys = null;
|
||||
}
|
||||
public void SetPrivateKey(CipherString privateKeyEnc)
|
||||
{
|
||||
if(privateKeyEnc != null)
|
||||
{
|
||||
_settings.AddOrUpdateValue(PrivateKeyKey, privateKeyEnc.EncryptedString);
|
||||
}
|
||||
else if(_settings.Contains(PrivateKeyKey))
|
||||
{
|
||||
_settings.Remove(PrivateKeyKey);
|
||||
_privateKey = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_privateKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPrivateKey(CipherString privateKeyEnc, SymmetricCryptoKey key)
|
||||
public void SetOrgKeys(ProfileResponse profile)
|
||||
{
|
||||
var bytes = DecryptToBytes(privateKeyEnc, key);
|
||||
PrivateKey = bytes;
|
||||
var orgKeysEncDict = new Dictionary<string, string>();
|
||||
|
||||
if(profile?.Organizations?.Any() ?? false)
|
||||
{
|
||||
foreach(var org in profile.Organizations)
|
||||
{
|
||||
orgKeysEncDict.Add(org.Id, org.Key);
|
||||
}
|
||||
}
|
||||
|
||||
SetOrgKeys(orgKeysEncDict);
|
||||
}
|
||||
|
||||
public void SetOrgKeys(Dictionary<string, string> orgKeysEncDict)
|
||||
{
|
||||
if(orgKeysEncDict?.Any() ?? false)
|
||||
{
|
||||
var dictJson = JsonConvert.SerializeObject(orgKeysEncDict);
|
||||
_settings.AddOrUpdateValue(OrgKeysKey, dictJson);
|
||||
}
|
||||
else if(_settings.Contains(OrgKeysKey))
|
||||
{
|
||||
_settings.Remove(OrgKeysKey);
|
||||
_orgKeys = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_orgKeys = null;
|
||||
}
|
||||
}
|
||||
|
||||
public SymmetricCryptoKey GetOrgKey(string orgId)
|
||||
@ -198,51 +228,11 @@ namespace Bit.App.Services
|
||||
return OrgKeys[orgId];
|
||||
}
|
||||
|
||||
public void ClearOrgKey(string orgId)
|
||||
{
|
||||
var localOrgKeys = OrgKeys;
|
||||
if(localOrgKeys == null || !localOrgKeys.ContainsKey(orgId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
localOrgKeys.Remove(orgId);
|
||||
// invoke setter
|
||||
OrgKeys = localOrgKeys;
|
||||
}
|
||||
|
||||
public void ClearKeys()
|
||||
{
|
||||
OrgKeys = null;
|
||||
SetOrgKeys((Dictionary<string, string>)null);
|
||||
Key = null;
|
||||
PrivateKey = null;
|
||||
}
|
||||
|
||||
public SymmetricCryptoKey AddOrgKey(string orgId, CipherString encOrgKey, byte[] privateKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var localOrgKeys = OrgKeys;
|
||||
var decBytes = RsaDecryptToBytes(encOrgKey, privateKey);
|
||||
var key = new SymmetricCryptoKey(decBytes);
|
||||
if(localOrgKeys.ContainsKey(orgId))
|
||||
{
|
||||
localOrgKeys[orgId] = key;
|
||||
}
|
||||
else
|
||||
{
|
||||
localOrgKeys.Add(orgId, key);
|
||||
}
|
||||
|
||||
// invoke setter
|
||||
OrgKeys = localOrgKeys;
|
||||
return key;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Debug.WriteLine("Cannot set org key. Decryption failed.");
|
||||
return null;
|
||||
}
|
||||
SetPrivateKey(null);
|
||||
}
|
||||
|
||||
public CipherString Encrypt(string plaintextValue, SymmetricCryptoKey key = null)
|
||||
@ -270,7 +260,7 @@ namespace Bit.App.Services
|
||||
var encryptedBytes = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plaintextBytes, iv);
|
||||
var mac = key.MacKey != null ? ComputeMac(encryptedBytes, iv, key.MacKey) : null;
|
||||
|
||||
return new CipherString(key.EncryptionType, Convert.ToBase64String(iv),
|
||||
return new CipherString(key.EncryptionType, Convert.ToBase64String(iv),
|
||||
Convert.ToBase64String(encryptedBytes), mac);
|
||||
}
|
||||
|
||||
|
@ -187,6 +187,7 @@ namespace Bit.App.Services
|
||||
SyncCompleted(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncProfileAsync()
|
||||
{
|
||||
if(!_authService.IsAuthenticated)
|
||||
@ -202,7 +203,7 @@ namespace Bit.App.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
SyncOrgKeys(profile.Result);
|
||||
await SyncOrgKeysAsync(profile.Result);
|
||||
|
||||
SyncCompleted(true);
|
||||
return true;
|
||||
@ -257,10 +258,11 @@ namespace Bit.App.Services
|
||||
var loginTask = SyncLoginsAsync(loginsDict);
|
||||
var folderTask = SyncFoldersAsync(foldersDict);
|
||||
var domainsTask = SyncDomainsAsync(domains.Result);
|
||||
SyncOrgKeys(profile.Result);
|
||||
await Task.WhenAll(loginTask, folderTask, domainsTask).ConfigureAwait(false);
|
||||
var orgKeysTask = SyncOrgKeysAsync(profile.Result);
|
||||
await Task.WhenAll(loginTask, folderTask, domainsTask, orgKeysTask).ConfigureAwait(false);
|
||||
|
||||
if(folderTask.Exception != null || loginTask.Exception != null || domainsTask.Exception != null)
|
||||
if(folderTask.Exception != null || loginTask.Exception != null || domainsTask.Exception != null ||
|
||||
orgKeysTask.Exception != null)
|
||||
{
|
||||
SyncCompleted(false);
|
||||
return false;
|
||||
@ -389,27 +391,23 @@ namespace Bit.App.Services
|
||||
catch(SQLite.SQLiteException) { }
|
||||
}
|
||||
|
||||
private void SyncOrgKeys(ProfileResponse profile)
|
||||
private async Task SyncOrgKeysAsync(ProfileResponse profile)
|
||||
{
|
||||
var orgKeysDict = new Dictionary<string, SymmetricCryptoKey>();
|
||||
|
||||
if(profile.Organizations != null)
|
||||
if(_cryptoService.PrivateKey == null)
|
||||
{
|
||||
foreach(var org in profile.Organizations)
|
||||
var keys = await _accountsApiRepository.GetKeys();
|
||||
if(!CheckSuccess(keys))
|
||||
{
|
||||
try
|
||||
{
|
||||
var decBytes = _cryptoService.RsaDecryptToBytes(new CipherString(org.Key), null);
|
||||
orgKeysDict.Add(org.Id, new SymmetricCryptoKey(decBytes));
|
||||
}
|
||||
catch
|
||||
{
|
||||
Debug.WriteLine($"Cannot set org key {org.Id}. Decryption failed.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(keys.Result.PrivateKey))
|
||||
{
|
||||
_cryptoService.SetPrivateKey(new CipherString(keys.Result.PrivateKey));
|
||||
}
|
||||
}
|
||||
|
||||
_cryptoService.OrgKeys = orgKeysDict;
|
||||
_cryptoService.SetOrgKeys(profile);
|
||||
}
|
||||
|
||||
private void SyncStarted()
|
||||
|
@ -3,6 +3,7 @@ using Bit.App.Abstractions;
|
||||
using Bit.App.Services;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Plugin.Settings.Abstractions;
|
||||
|
||||
namespace Bit.App.Test
|
||||
{
|
||||
@ -26,11 +27,12 @@ namespace Bit.App.Test
|
||||
{
|
||||
var storageService = Substitute.For<ISecureStorageService>();
|
||||
var keyService = Substitute.For<IKeyDerivationService>();
|
||||
var settingsService = Substitute.For<ISettings>();
|
||||
storageService.Contains("key").Returns(true);
|
||||
storageService.Retrieve("key").Returns(
|
||||
Convert.FromBase64String("QpSYI5k0bLQXEygUEHn4wMII3ERatuWDFBszk7JAhbQ="));
|
||||
|
||||
var service = new CryptoService(storageService, keyService);
|
||||
var service = new CryptoService(settingsService, storageService, keyService);
|
||||
var encryptedValue = service.Encrypt(value);
|
||||
return service.Decrypt(encryptedValue);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user