diff --git a/src/Core/Models/Api/LoginApi.cs b/src/Core/Models/Api/LoginApi.cs new file mode 100644 index 000000000..b611c2eb8 --- /dev/null +++ b/src/Core/Models/Api/LoginApi.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace Bit.Core.Models.Api +{ + public class LoginApi + { + public List Uris { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public string Totp { get; set; } + } +} diff --git a/src/Core/Models/Api/LoginUriApi.cs b/src/Core/Models/Api/LoginUriApi.cs new file mode 100644 index 000000000..d9e59c65d --- /dev/null +++ b/src/Core/Models/Api/LoginUriApi.cs @@ -0,0 +1,10 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class LoginUriApi + { + public string Uri { get; set; } + public UriMatchType? Match { get; set; } + } +} diff --git a/src/Core/Models/Data/Data.cs b/src/Core/Models/Data/Data.cs new file mode 100644 index 000000000..2fae03c94 --- /dev/null +++ b/src/Core/Models/Data/Data.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Bit.Core.Models.Data +{ + public abstract class Data + { + } +} diff --git a/src/Core/Models/Data/LoginData.cs b/src/Core/Models/Data/LoginData.cs new file mode 100644 index 000000000..7afff7e36 --- /dev/null +++ b/src/Core/Models/Data/LoginData.cs @@ -0,0 +1,27 @@ +using Bit.Core.Models.Api; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.Data +{ + public class LoginData : Data + { + public LoginData() { } + + public LoginData(LoginApi data) + { + Username = data.Username; + Password = data.Password; + PasswordRevisionDate = data.PasswordRevisionDate; + Totp = data.Totp; + Uris = data.Uris?.Select(u => new LoginUriData(u)).ToList(); + } + + public List Uris { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public string Totp { get; set; } + } +} diff --git a/src/Core/Models/Data/LoginUriData.cs b/src/Core/Models/Data/LoginUriData.cs new file mode 100644 index 000000000..52294c523 --- /dev/null +++ b/src/Core/Models/Data/LoginUriData.cs @@ -0,0 +1,19 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class LoginUriData : Data + { + public LoginUriData() { } + + public LoginUriData(LoginUriApi data) + { + Uri = data.Uri; + Match = data.Match; + } + + public string Uri { get; set; } + public UriMatchType? Match { get; set; } + } +} diff --git a/src/Core/Models/Data/OrganizationData.cs b/src/Core/Models/Data/OrganizationData.cs index ac9d2d71f..e88c3ac3b 100644 --- a/src/Core/Models/Data/OrganizationData.cs +++ b/src/Core/Models/Data/OrganizationData.cs @@ -3,7 +3,7 @@ using Bit.Core.Models.Response; namespace Bit.Core.Models.Data { - public class OrganizationData + public class OrganizationData : Data { public OrganizationData(ProfileOrganizationResponse response) { diff --git a/src/Core/Models/Domain/Cipher.cs b/src/Core/Models/Domain/Cipher.cs index 3bf083b8d..3a84343d6 100644 --- a/src/Core/Models/Domain/Cipher.cs +++ b/src/Core/Models/Domain/Cipher.cs @@ -9,5 +9,6 @@ namespace Bit.Core.Models.Domain public string Id { get; set; } public string OrganizationId { get; set; } public CipherString Name { get; set; } + public Login Login { get; set; } } } diff --git a/src/Core/Models/Domain/CipherString.cs b/src/Core/Models/Domain/CipherString.cs index 5f2b65ebc..8b0f60f72 100644 --- a/src/Core/Models/Domain/CipherString.cs +++ b/src/Core/Models/Domain/CipherString.cs @@ -1,5 +1,6 @@ using Bit.Core.Enums; using System; +using System.Threading.Tasks; namespace Bit.Core.Models.Domain { @@ -96,14 +97,14 @@ namespace Bit.Core.Models.Domain public string Data { get; private set; } public string Mac { get; private set; } - public string Decrypt(string orgId = null) + public Task DecryptAsync(string orgId = null) { if(_decryptedValue == null) { // TODO } - return _decryptedValue; + return Task.FromResult(_decryptedValue); } } } diff --git a/src/Core/Models/Domain/Domain.cs b/src/Core/Models/Domain/Domain.cs index 983f627e1..6cb5aaac8 100644 --- a/src/Core/Models/Domain/Domain.cs +++ b/src/Core/Models/Domain/Domain.cs @@ -1,11 +1,86 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; +using System.Threading.Tasks; namespace Bit.Core.Models.Domain { public abstract class Domain { - // TODO + protected void BuildDomainModel(D domain, O dataObj, HashSet map, bool alreadyEncrypted, + HashSet notEncList = null) + where D : Domain + where O : Data.Data + { + var domainType = domain.GetType(); + var dataObjType = dataObj.GetType(); + foreach(var prop in map) + { + var dataObjPropInfo = dataObjType.GetProperty(prop); + var dataObjProp = dataObjPropInfo.GetValue(dataObj); + var domainPropInfo = domainType.GetProperty(prop); + if(alreadyEncrypted || (notEncList?.Contains(prop) ?? false)) + { + domainPropInfo.SetValue(domain, dataObjProp, null); + } + else + { + domainPropInfo.SetValue(domain, + dataObjProp != null ? new CipherString(dataObjProp as string) : null, null); + } + } + } + + protected void BuildDataModel(D domain, O dataObj, HashSet map, + HashSet notCipherStringList = null) + where D : Domain + where O : Data.Data + { + var domainType = domain.GetType(); + var dataObjType = dataObj.GetType(); + foreach(var prop in map) + { + var domainPropInfo = domainType.GetProperty(prop); + var domainProp = domainPropInfo.GetValue(domain); + var dataObjPropInfo = dataObjType.GetProperty(prop); + if(notCipherStringList?.Contains(prop) ?? false) + { + dataObjPropInfo.SetValue(dataObj, domainProp, null); + } + else + { + dataObjPropInfo.SetValue(dataObj, (domainProp as CipherString)?.EncryptedString, null); + } + } + } + + protected async Task DecryptObjAsync(V viewModel, D domain, HashSet map, string orgId) + where V : View.View + { + var viewModelType = viewModel.GetType(); + var domainType = domain.GetType(); + + Task decCs(string propName) + { + var domainPropInfo = domainType.GetProperty(propName); + var domainProp = domainPropInfo.GetValue(domain) as CipherString; + if(domainProp != null) + { + return domainProp.DecryptAsync(orgId); + } + return Task.FromResult((string)null); + }; + void setDec(string propName, string val) + { + var viewModelPropInfo = viewModelType.GetProperty(propName); + viewModelPropInfo.SetValue(viewModel, val, null); + }; + + var tasks = new List(); + foreach(var prop in map) + { + tasks.Add(decCs(prop).ContinueWith(async val => setDec(prop, await val))); + } + await Task.WhenAll(tasks); + return viewModel; + } } } diff --git a/src/Core/Models/Domain/Login.cs b/src/Core/Models/Domain/Login.cs new file mode 100644 index 000000000..5c5c5c2aa --- /dev/null +++ b/src/Core/Models/Domain/Login.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Bit.Core.Models.Domain +{ + public class Login : Domain + { + public List Uris { get; set; } + public CipherString Username { get; set; } + public CipherString Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public CipherString Totp { get; set; } + } +} diff --git a/src/Core/Models/Domain/LoginUri.cs b/src/Core/Models/Domain/LoginUri.cs new file mode 100644 index 000000000..32f6b73bb --- /dev/null +++ b/src/Core/Models/Domain/LoginUri.cs @@ -0,0 +1,43 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class LoginUri : Domain + { + public LoginUri() { } + + public LoginUri(LoginUriData obj, bool alreadyEncrypted = false) + { + Match = obj.Match; + BuildDomainModel(this, obj, new HashSet + { + "Uri" + }, alreadyEncrypted); + } + + public CipherString Uri { get; set; } + public UriMatchType? Match { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new LoginUriView(this), this, new HashSet + { + "Uri" + }, orgId); + } + + public LoginUriData ToLoginUriData() + { + var u = new LoginUriData(); + BuildDataModel(this, u, new HashSet + { + "Uri" + }, new HashSet { "Match" }); + return u; + } + } +} diff --git a/src/Core/Models/View/LoginUriView.cs b/src/Core/Models/View/LoginUriView.cs new file mode 100644 index 000000000..23d1a99c4 --- /dev/null +++ b/src/Core/Models/View/LoginUriView.cs @@ -0,0 +1,34 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class LoginUriView : View + { + private string _uri; + private string _domain; + private string _hostname; + private bool? _canLaunch; + + public LoginUriView() { } + + public LoginUriView(LoginUri u) + { + Match = u.Match; + } + + public UriMatchType? Match { get; set; } + public string Uri + { + get => _uri; + set + { + _uri = value; + _domain = null; + _canLaunch = null; + } + } + + // TODO + } +} diff --git a/src/Core/Models/View/LoginView.cs b/src/Core/Models/View/LoginView.cs new file mode 100644 index 000000000..e8adb7844 --- /dev/null +++ b/src/Core/Models/View/LoginView.cs @@ -0,0 +1,27 @@ +using Bit.Core.Models.Domain; +using System; +using System.Collections.Generic; + +namespace Bit.Core.Models.View +{ + public class LoginView : View + { + public LoginView() { } + + public LoginView(Login l) + { + PasswordRevisionDate = l.PasswordRevisionDate; + } + + public string Username { get; set; } + public string Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public string Totp { get; set; } + public List Uris { get; set; } + public string Uri => HashUris ? Uris[0].Uri : null; + public string MaskedPassword => Password != null ? "••••••••" : null; + public string SubTitle => Username; + // TODO: uri launch props + public bool HashUris => (Uris?.Count ?? 0) > 0; + } +} diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs new file mode 100644 index 000000000..fa943b8bd --- /dev/null +++ b/src/Core/Services/CipherService.cs @@ -0,0 +1,129 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class CipherService + { + private const string Keys_CiphersFormat = "ciphers_{0}"; + private const string Keys_LocalData = "ciphersLocalData"; + private const string Keys_NeverDomains = "neverDomains"; + + private List _decryptedCipherCache; + private readonly ICryptoService _cryptoService; + private readonly IUserService _userService; + private readonly ISettingsService _settingsService; + private readonly IApiService _apiService; + private readonly IStorageService _storageService; + private readonly II18nService _i18NService; + private Dictionary> _domainMatchBlacklist = new Dictionary> + { + ["google.com"] = new HashSet { "script.google.com" } + }; + + public CipherService( + ICryptoService cryptoService, + IUserService userService, + ISettingsService settingsService, + IApiService apiService, + IStorageService storageService, + II18nService i18nService) + { + _cryptoService = cryptoService; + _userService = userService; + _settingsService = settingsService; + _apiService = apiService; + _storageService = storageService; + _i18NService = i18nService; + } + + private List DecryptedCipherCache + { + get => _decryptedCipherCache; + set + { + if(value == null) + { + _decryptedCipherCache.Clear(); + } + _decryptedCipherCache = value; + // TODO: update search index + } + } + + public void ClearCache() + { + DecryptedCipherCache = null; + } + + public async Task Encrypt(CipherView model, SymmetricCryptoKey key = null, + Cipher originalCipher = null) + { + // Adjust password history + if(model.Id != null) + { + // TODO + } + + var cipher = new Cipher(); + cipher.Id = model.Id; + // TODO others + + if(key == null && cipher.OrganizationId != null) + { + key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId); + if(key == null) + { + throw new Exception("Cannot encrypt cipher for organization. No key."); + } + } + + var tasks = new List(); + tasks.Add(EncryptObjPropertyAsync(model, cipher, new HashSet + { + nameof(model.Name) + }, key)); + + await Task.WhenAll(tasks); + return cipher; + } + + private Task EncryptObjPropertyAsync(V model, D obj, HashSet map, SymmetricCryptoKey key) + where V : View + where D : Domain + { + var modelType = model.GetType(); + var objType = obj.GetType(); + + Task makeCs(string propName) + { + var modelPropInfo = modelType.GetProperty(propName); + var modelProp = modelPropInfo.GetValue(model) as string; + if(!string.IsNullOrWhiteSpace(modelProp)) + { + return _cryptoService.EncryptAsync(modelProp, key); + } + return Task.FromResult((CipherString)null); + }; + void setCs(string propName, CipherString val) + { + var objPropInfo = objType.GetProperty(propName); + objPropInfo.SetValue(obj, val, null); + }; + + var tasks = new List(); + foreach(var prop in map) + { + tasks.Add(makeCs(prop).ContinueWith(async val => setCs(prop, await val))); + } + return Task.WhenAll(tasks); + } + } +}