using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; using Bit.Core.Models.Response; using Bit.Core.Models.View; using Bit.Core.Utilities; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Bit.Core.Services { public class CipherService : ICipherService { private const string Keys_CiphersFormat = "ciphers_{0}"; private const string Keys_LocalData = "ciphersLocalData"; private const string Keys_NeverDomains = "neverDomains"; private readonly string[] _ignoredSearchTerms = new string[] { "com", "net", "org", "android", "io", "co", "uk", "au", "nz", "fr", "de", "tv", "info", "app", "apps", "eu", "me", "dev", "jp", "mobile" }; 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" } }; private readonly HttpClient _httpClient = new HttpClient(); 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 EncryptAsync(CipherView model, SymmetricCryptoKey key = null, Cipher originalCipher = null) { // Adjust password history if(model.Id != null) { if(originalCipher == null) { originalCipher = await GetAsync(model.Id); } if(originalCipher != null) { var existingCipher = await originalCipher.DecryptAsync(); if(model.PasswordHistory == null) { model.PasswordHistory = new List(); } if(model.Type == CipherType.Login && existingCipher.Type == CipherType.Login) { if(!string.IsNullOrWhiteSpace(existingCipher.Login.Password) && existingCipher.Login.Password != model.Login.Password) { var now = DateTime.UtcNow; var ph = new PasswordHistoryView { Password = existingCipher.Login.Password, LastUsedDate = now }; model.Login.PasswordRevisionDate = now; model.PasswordHistory.Insert(0, ph); } else { model.Login.PasswordRevisionDate = DateTime.UtcNow; } } if(existingCipher.HasFields) { var existingHiddenFields = existingCipher.Fields.Where(f => f.Type == FieldType.Hidden && !string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value)); var hiddenFields = model.Fields?.Where(f => f.Type == FieldType.Hidden && !string.IsNullOrWhiteSpace(f.Name)) ?? new List(); foreach(var ef in existingHiddenFields) { var matchedField = hiddenFields.FirstOrDefault(f => f.Name == ef.Name); if(matchedField == null || matchedField.Value != ef.Value) { var ph = new PasswordHistoryView { Password = string.Format("{0}: {1}", ef.Name, ef.Value), LastUsedDate = DateTime.UtcNow }; model.PasswordHistory.Insert(0, ph); } } } } if(!model.PasswordHistory?.Any() ?? false) { model.PasswordHistory = null; } else if(model.PasswordHistory != null && model.PasswordHistory.Count > 5) { model.PasswordHistory = model.PasswordHistory.Take(5).ToList(); } } var cipher = new Cipher { Id = model.Id, FolderId = model.FolderId, Favorite = model.Favorite, OrganizationId = model.OrganizationId, Type = model.Type, CollectionIds = model.CollectionIds }; 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 { EncryptObjPropertyAsync(model, cipher, new HashSet { "Name", "Notes" }, key), EncryptCipherDataAsync(cipher, model, key), EncryptFieldsAsync(model.Fields, key) .ContinueWith(async fields => cipher.Fields = await fields), EncryptPasswordHistoriesAsync(model.PasswordHistory, key) .ContinueWith(async phs => cipher.PasswordHistory = await phs), EncryptAttachmentsAsync(model.Attachments, key) .ContinueWith(async attachments => cipher.Attachments = await attachments) }; await Task.WhenAll(tasks); return cipher; } public async Task GetAsync(string id) { var userId = await _userService.GetUserIdAsync(); var localData = await _storageService.GetAsync>>( Keys_LocalData); var ciphers = await _storageService.GetAsync>( string.Format(Keys_CiphersFormat, userId)); if(!ciphers?.ContainsKey(id) ?? true) { return null; } return new Cipher(ciphers[id], false, localData?.ContainsKey(id) ?? false ? localData[id] : null); } public async Task> GetAllAsync() { var userId = await _userService.GetUserIdAsync(); var localData = await _storageService.GetAsync>>( Keys_LocalData); var ciphers = await _storageService.GetAsync>( string.Format(Keys_CiphersFormat, userId)); var response = ciphers?.Select(c => new Cipher(c.Value, false, localData?.ContainsKey(c.Key) ?? false ? localData[c.Key] : null)); return response?.ToList() ?? new List(); } // TODO: sequentialize? public async Task> GetAllDecryptedAsync() { if(DecryptedCipherCache != null) { return DecryptedCipherCache; } var hashKey = await _cryptoService.HasKeyAsync(); if(!hashKey) { throw new Exception("No key."); } var decCiphers = new List(); var tasks = new List(); var ciphers = await GetAllAsync(); foreach(var cipher in ciphers) { tasks.Add(cipher.DecryptAsync().ContinueWith(async c => decCiphers.Add(await c))); } await Task.WhenAll(tasks); decCiphers = decCiphers.OrderBy(c => c, new CipherLocaleComparer(_i18nService)).ToList(); DecryptedCipherCache = decCiphers; return DecryptedCipherCache; } public async Task> GetAllDecryptedForGroupingAsync(string groupingId, bool folder = true) { var ciphers = await GetAllDecryptedAsync(); return ciphers.Where(cipher => { if(folder && cipher.FolderId == groupingId) { return true; } if(!folder && cipher.CollectionIds != null && cipher.CollectionIds.Contains(groupingId)) { return true; } return false; }).ToList(); } public async Task> GetAllDecryptedForUrlAsync(string url) { var all = await GetAllDecryptedByUrlAsync(url); return all.Item1; } public async Task, List, List>> GetAllDecryptedByUrlAsync( string url, List includeOtherTypes = null) { if(string.IsNullOrWhiteSpace(url) && includeOtherTypes == null) { return new Tuple, List, List>( new List(), new List(), new List()); } var domain = CoreHelpers.GetDomain(url); var mobileApp = UrlIsMobileApp(url); var mobileAppInfo = InfoFromMobileAppUrl(url); var mobileAppWebUriString = mobileAppInfo?.Item1; var mobileAppSearchTerms = mobileAppInfo?.Item2; var matchingDomainsTask = GetMatchingDomainsAsync(url, domain, mobileApp, mobileAppWebUriString); var ciphersTask = GetAllDecryptedAsync(); await Task.WhenAll(new List { matchingDomainsTask, ciphersTask }); var matchingDomains = await matchingDomainsTask; var matchingDomainsSet = matchingDomains.Item1; var matchingFuzzyDomainsSet = matchingDomains.Item2; var matchingLogins = new List(); var matchingFuzzyLogins = new List(); var others = new List(); var ciphers = await ciphersTask; var defaultMatch = await _storageService.GetAsync(Constants.DefaultUriMatch); if(defaultMatch == null) { defaultMatch = UriMatchType.Domain; } foreach(var cipher in ciphers) { if(cipher.Type != CipherType.Login && (includeOtherTypes?.Any(t => t == cipher.Type) ?? false)) { others.Add(cipher); continue; } if(cipher.Type != CipherType.Login || cipher.Login?.Uris == null || !cipher.Login.Uris.Any()) { continue; } foreach(var u in cipher.Login.Uris) { if(string.IsNullOrWhiteSpace(u.Uri)) { continue; } var match = false; switch(u.Match) { case null: case UriMatchType.Domain: match = CheckDefaultUriMatch(cipher, u, matchingLogins, matchingFuzzyLogins, matchingDomainsSet, matchingFuzzyDomainsSet, mobileApp, mobileAppSearchTerms); if(match && u.Domain != null) { if(_domainMatchBlacklist.ContainsKey(u.Domain)) { var domainUrlHost = CoreHelpers.GetHost(url); if(_domainMatchBlacklist[u.Domain].Contains(domainUrlHost)) { match = false; } } } break; case UriMatchType.Host: var urlHost = CoreHelpers.GetHost(url); match = urlHost != null && urlHost == CoreHelpers.GetHost(u.Uri); if(match) { AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); } break; case UriMatchType.Exact: match = url == u.Uri; if(match) { AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); } break; case UriMatchType.StartsWith: match = url.StartsWith(u.Uri); if(match) { AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); } break; case UriMatchType.RegularExpression: var regex = new Regex(u.Uri, RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); match = regex.IsMatch(url); if(match) { AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); } break; case UriMatchType.Never: default: break; } if(match) { break; } } } return new Tuple, List, List>( matchingLogins, matchingFuzzyLogins, others); } public async Task GetLastUsedForUrlAsync(string url) { var ciphers = await GetAllDecryptedForUrlAsync(url); return ciphers.OrderBy(c => c, new CipherLastUsedComparer()).FirstOrDefault(); } public async Task UpdateLastUsedDateAsync(string id) { var ciphersLocalData = await _storageService.GetAsync>>( Keys_LocalData); if(ciphersLocalData == null) { ciphersLocalData = new Dictionary>(); } if(!ciphersLocalData.ContainsKey(id)) { ciphersLocalData.Add(id, new Dictionary()); } if(ciphersLocalData[id].ContainsKey("lastUsedDate")) { ciphersLocalData[id]["lastUsedDate"] = DateTime.UtcNow; } else { ciphersLocalData[id].Add("lastUsedDate", DateTime.UtcNow); } await _storageService.SaveAsync(Keys_LocalData, ciphersLocalData); // Update cache if(DecryptedCipherCache == null) { return; } var cached = DecryptedCipherCache.FirstOrDefault(c => c.Id == id); if(cached != null) { cached.LocalData = ciphersLocalData[id]; } } public async Task SaveNeverDomainAsync(string domain) { if(string.IsNullOrWhiteSpace(domain)) { return; } var domains = await _storageService.GetAsync>(Keys_NeverDomains); if(domains == null) { domains = new HashSet(); } domains.Add(domain); await _storageService.SaveAsync(Keys_NeverDomains, domains); } public async Task SaveWithServerAsync(Cipher cipher) { CipherResponse response; if(cipher.Id == null) { if(cipher.CollectionIds != null) { var request = new CipherCreateRequest(cipher); response = await _apiService.PostCipherCreateAsync(request); } else { var request = new CipherRequest(cipher); response = await _apiService.PostCipherAsync(request); } cipher.Id = response.Id; } else { var request = new CipherRequest(cipher); response = await _apiService.PutCipherAsync(cipher.Id, request); } var userId = await _userService.GetUserIdAsync(); var data = new CipherData(response, userId, cipher.CollectionIds); await UpsertAsync(data); } public async Task ShareWithServerAsync(CipherView cipher, string organizationId, HashSet collectionIds) { var attachmentTasks = new List(); if(cipher.Attachments != null) { foreach(var attachment in cipher.Attachments) { if(attachment.Key == null) { attachmentTasks.Add(ShareAttachmentWithServerAsync(attachment, cipher.Id, organizationId)); } } } await Task.WhenAll(attachmentTasks); cipher.OrganizationId = organizationId; cipher.CollectionIds = collectionIds; var encCipher = await EncryptAsync(cipher); var request = new CipherShareRequest(encCipher); var response = await _apiService.PutShareCipherAsync(cipher.Id, request); var userId = await _userService.GetUserIdAsync(); var data = new CipherData(response, userId, collectionIds); await UpsertAsync(data); } public async Task SaveAttachmentRawWithServerAsync(Cipher cipher, string filename, byte[] data) { var key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId); var encFileName = await _cryptoService.EncryptAsync(filename, key); var dataEncKey = await _cryptoService.MakeEncKeyAsync(key); var encData = await _cryptoService.EncryptToBytesAsync(data, dataEncKey.Item1); CipherResponse response; try { using(var fd = new MultipartFormDataContent(string.Concat("Upload----", DateTime.UtcNow))) { fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString); fd.Add(new StringContent(string.Empty), "key", dataEncKey.Item2.EncryptedString); response = await _apiService.PostCipherAttachmentAsync(cipher.Id, fd); } } catch(ApiException e) { throw new Exception(e.Error.GetSingleMessage()); } var userId = await _userService.GetUserIdAsync(); var cData = new CipherData(response, userId, cipher.CollectionIds); await UpsertAsync(cData); return new Cipher(cData); } public async Task SaveCollectionsWithServerAsync(Cipher cipher) { var request = new CipherCollectionsRequest(cipher.CollectionIds?.ToList()); await _apiService.PutCipherCollectionsAsync(cipher.Id, request); var userId = await _userService.GetUserIdAsync(); var data = cipher.ToCipherData(userId); await UpsertAsync(data); } public async Task UpsertAsync(CipherData cipher) { var userId = await _userService.GetUserIdAsync(); var storageKey = string.Format(Keys_CiphersFormat, userId); var ciphers = await _storageService.GetAsync>(storageKey); if(ciphers == null) { ciphers = new Dictionary(); } if(!ciphers.ContainsKey(cipher.Id)) { ciphers.Add(cipher.Id, null); } ciphers[cipher.Id] = cipher; await _storageService.SaveAsync(storageKey, ciphers); DecryptedCipherCache = null; } public async Task UpsertAsync(List cipher) { var userId = await _userService.GetUserIdAsync(); var storageKey = string.Format(Keys_CiphersFormat, userId); var ciphers = await _storageService.GetAsync>(storageKey); if(ciphers == null) { ciphers = new Dictionary(); } foreach(var c in cipher) { if(!ciphers.ContainsKey(c.Id)) { ciphers.Add(c.Id, null); } ciphers[c.Id] = c; } await _storageService.SaveAsync(storageKey, ciphers); DecryptedCipherCache = null; } public async Task ReplaceAsync(Dictionary ciphers) { var userId = await _userService.GetUserIdAsync(); await _storageService.SaveAsync(string.Format(Keys_CiphersFormat, userId), ciphers); DecryptedCipherCache = null; } public async Task ClearAsync(string userId) { await _storageService.RemoveAsync(string.Format(Keys_CiphersFormat, userId)); ClearCache(); } public async Task DeleteAsync(string id) { var userId = await _userService.GetUserIdAsync(); var cipherKey = string.Format(Keys_CiphersFormat, userId); var ciphers = await _storageService.GetAsync>(cipherKey); if(ciphers == null) { return; } if(!ciphers.ContainsKey(id)) { return; } ciphers.Remove(id); await _storageService.SaveAsync(cipherKey, ciphers); DecryptedCipherCache = null; } public async Task DeleteAsync(List ids) { var userId = await _userService.GetUserIdAsync(); var cipherKey = string.Format(Keys_CiphersFormat, userId); var ciphers = await _storageService.GetAsync>(cipherKey); if(ciphers == null) { return; } foreach(var id in ids) { if(!ciphers.ContainsKey(id)) { return; } ciphers.Remove(id); } await _storageService.SaveAsync(cipherKey, ciphers); DecryptedCipherCache = null; } public async Task DeleteWithServerAsync(string id) { await _apiService.DeleteCipherAsync(id); await DeleteAsync(id); } public async Task DeleteAttachmentAsync(string id, string attachmentId) { var userId = await _userService.GetUserIdAsync(); var cipherKey = string.Format(Keys_CiphersFormat, userId); var ciphers = await _storageService.GetAsync>(cipherKey); if(ciphers == null || !ciphers.ContainsKey(id) || ciphers[id].Attachments == null) { return; } var attachment = ciphers[id].Attachments.FirstOrDefault(a => a.Id == attachmentId); if(attachment != null) { ciphers[id].Attachments.Remove(attachment); } await _storageService.SaveAsync(cipherKey, ciphers); DecryptedCipherCache = null; } public async Task DeleteAttachmentWithServerAsync(string id, string attachmentId) { try { await _apiService.DeleteCipherAttachmentAsync(id, attachmentId); } catch(ApiException e) { throw new Exception(e.Error.GetSingleMessage()); } await DeleteAttachmentAsync(id, attachmentId); } // Helpers private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId, string organizationId) { var attachmentResponse = await _httpClient.GetAsync(attachmentView.Url); if(!attachmentResponse.IsSuccessStatusCode) { throw new Exception("Failed to download attachment: " + attachmentResponse.StatusCode); } var bytes = await attachmentResponse.Content.ReadAsByteArrayAsync(); var decBytes = await _cryptoService.DecryptFromBytesAsync(bytes, null); var key = await _cryptoService.GetOrgKeyAsync(organizationId); var encFileName = await _cryptoService.EncryptAsync(attachmentView.FileName, key); var dataEncKey = await _cryptoService.MakeEncKeyAsync(key); var encData = await _cryptoService.EncryptToBytesAsync(decBytes, dataEncKey.Item1); try { using(var fd = new MultipartFormDataContent(string.Concat("Upload----", DateTime.UtcNow))) { fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString); fd.Add(new StringContent(string.Empty), "key", dataEncKey.Item2.EncryptedString); await _apiService.PostShareCipherAttachmentAsync(cipherId, attachmentView.Id, fd, organizationId); } } catch(ApiException e) { throw new Exception(e.Error.GetSingleMessage()); } } private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri, List matchingLogins, List matchingFuzzyLogins, HashSet matchingDomainsSet, HashSet matchingFuzzyDomainsSet, bool mobileApp, string[] mobileAppSearchTerms) { var loginUriString = loginUri.Uri; var loginUriDomain = loginUri.Domain; if(matchingDomainsSet.Contains(loginUriString)) { AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); return true; } else if(mobileApp && matchingFuzzyDomainsSet.Contains(loginUriString)) { AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); return false; } else if(!mobileApp) { var info = InfoFromMobileAppUrl(loginUriString); if(info?.Item1 != null && matchingDomainsSet.Contains(info.Item1)) { AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); return false; } } if(!loginUri.Uri.Contains("://") && loginUriString.Contains(".")) { loginUriString = "http://" + loginUriString; } if(loginUriDomain != null) { loginUriDomain = loginUriDomain.ToLowerInvariant(); if(matchingDomainsSet.Contains(loginUriDomain)) { AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); return true; } else if(mobileApp && matchingFuzzyDomainsSet.Contains(loginUriDomain)) { AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); return false; } } if(mobileApp && (mobileAppSearchTerms?.Any() ?? false)) { var addedFromSearchTerm = false; var loginName = cipher.Name?.ToLowerInvariant(); foreach(var term in mobileAppSearchTerms) { addedFromSearchTerm = (loginUriDomain != null && loginUriDomain.Contains(term)) || (loginName != null && loginName.Contains(term)); if(!addedFromSearchTerm) { var domainTerm = loginUriDomain?.Split('.')[0]; addedFromSearchTerm = (domainTerm != null && domainTerm.Length > 2 && term.Contains(domainTerm)) || (loginName != null && term.Contains(loginName)); } if(addedFromSearchTerm) { AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); return false; } } } return false; } private void AddMatchingLogin(CipherView cipher, List matchingLogins, List matchingFuzzyLogins) { if(matchingFuzzyLogins.Contains(cipher)) { matchingFuzzyLogins.Remove(cipher); } matchingLogins.Add(cipher); } private void AddMatchingFuzzyLogin(CipherView cipher, List matchingLogins, List matchingFuzzyLogins) { if(!matchingFuzzyLogins.Contains(cipher) && !matchingLogins.Contains(cipher)) { matchingFuzzyLogins.Add(cipher); } } private async Task, HashSet>> GetMatchingDomainsAsync(string url, string domain, bool mobileApp, string mobileAppWebUriString) { var matchingDomains = new HashSet(); var matchingFuzzyDomains = new HashSet(); var eqDomains = await _settingsService.GetEquivalentDomainsAsync(); foreach(var eqDomain in eqDomains) { var eqDomainSet = new HashSet(eqDomain); if(mobileApp) { if(eqDomainSet.Contains(url)) { eqDomain.Select(d => matchingDomains.Add(d)); } else if(mobileAppWebUriString != null && eqDomainSet.Contains(mobileAppWebUriString)) { eqDomain.Select(d => matchingFuzzyDomains.Add(d)); } } else if(eqDomainSet.Contains(url)) { eqDomain.Select(d => matchingDomains.Add(d)); } } if(!matchingDomains.Any()) { matchingDomains.Add(mobileApp ? url : domain); } if(mobileApp && mobileAppWebUriString != null && !matchingFuzzyDomains.Any() && !matchingDomains.Contains(mobileAppWebUriString)) { matchingFuzzyDomains.Add(mobileAppWebUriString); } return new Tuple, HashSet>(matchingDomains, matchingFuzzyDomains); } private Tuple InfoFromMobileAppUrl(string mobileAppUrlString) { if(UrlIsAndroidApp(mobileAppUrlString)) { return InfoFromAndroidAppUri(mobileAppUrlString); } else if(UrlIsiOSApp(mobileAppUrlString)) { return InfoFromiOSAppUrl(mobileAppUrlString); } return null; } private Tuple InfoFromAndroidAppUri(string androidAppUrlString) { if(!UrlIsAndroidApp(androidAppUrlString)) { return null; } var androidUrlParts = androidAppUrlString.Replace(Constants.AndroidAppProtocol, string.Empty).Split('.'); if(androidUrlParts.Length >= 2) { var webUri = string.Join(".", androidUrlParts[1], androidUrlParts[0]); var searchTerms = androidUrlParts.Where(p => !_ignoredSearchTerms.Contains(p)) .Select(p => p.ToLowerInvariant()).ToArray(); return new Tuple(webUri, searchTerms); } return null; } private Tuple InfoFromiOSAppUrl(string iosAppUrlString) { if(!UrlIsiOSApp(iosAppUrlString)) { return null; } var webUri = iosAppUrlString.Replace(Constants.iOSAppProtocol, string.Empty); return new Tuple(webUri, null); } private bool UrlIsMobileApp(string url) { return UrlIsAndroidApp(url) || UrlIsiOSApp(url); } private bool UrlIsAndroidApp(string url) { return url.StartsWith(Constants.AndroidAppProtocol); } private bool UrlIsiOSApp(string url) { return url.StartsWith(Constants.iOSAppProtocol); } 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); } private async Task> EncryptAttachmentsAsync( List attachmentsModel, SymmetricCryptoKey key) { if(!attachmentsModel?.Any() ?? true) { return null; } var tasks = new List(); var encAttachments = new List(); foreach(var model in attachmentsModel) { var attachment = new Attachment { Id = model.Id, Size = model.Size, SizeName = model.SizeName, Url = model.Url }; var task = EncryptObjPropertyAsync(model, attachment, new HashSet { "FileName" }, key).ContinueWith(async (t) => { if(model.Key != null) { attachment.Key = await _cryptoService.EncryptAsync(model.Key.Key, key); } encAttachments.Add(attachment); }); tasks.Add(task); } await Task.WhenAll(tasks); return encAttachments; } private async Task EncryptCipherDataAsync(Cipher cipher, CipherView model, SymmetricCryptoKey key) { switch(cipher.Type) { case CipherType.Login: cipher.Login = new Login { PasswordRevisionDate = model.Login.PasswordRevisionDate }; await EncryptObjPropertyAsync(model.Login, cipher.Login, new HashSet { "Username", "Password", "Totp" }, key); if(model.Login.Uris != null) { cipher.Login.Uris = new List(); foreach(var uri in model.Login.Uris) { var loginUri = new LoginUri { Match = uri.Match }; await EncryptObjPropertyAsync(uri, loginUri, new HashSet { "Uri" }, key); cipher.Login.Uris.Add(loginUri); } } break; case CipherType.SecureNote: cipher.SecureNote = new SecureNote { Type = model.SecureNote.Type }; break; case CipherType.Card: cipher.Card = new Card(); await EncryptObjPropertyAsync(model.Card, cipher.Card, new HashSet { "CardholderName", "Brand", "Number", "ExpMonth", "ExpYear", "Code" }, key); break; case CipherType.Identity: cipher.Identity = new Identity(); await EncryptObjPropertyAsync(model.Identity, cipher.Identity, new HashSet { "Title", "FirstName", "MiddleName", "LastName", "Address1", "Address2", "Address3", "City", "State", "PostalCode", "Country", "Company", "Email", "Phone", "SSN", "Username", "PassportNumber", "LicenseNumber" }, key); break; default: throw new Exception("Unknown cipher type."); } } private async Task> EncryptFieldsAsync(List fieldsModel, SymmetricCryptoKey key) { if(!fieldsModel?.Any() ?? true) { return null; } var tasks = new List(); var encFields = new List(); foreach(var model in fieldsModel) { var field = new Field { Type = model.Type }; // normalize boolean type field values if(model.Type == FieldType.Boolean && model.Value != "true") { model.Value = "false"; } var task = EncryptObjPropertyAsync(model, field, new HashSet { "Name", "Value" }, key).ContinueWith((t) => { encFields.Add(field); }); tasks.Add(task); } await Task.WhenAll(tasks); return encFields; } private async Task> EncryptPasswordHistoriesAsync(List phModels, SymmetricCryptoKey key) { if(!phModels?.Any() ?? true) { return null; } var tasks = new List(); var encPhs = new List(); foreach(var model in phModels) { var ph = new PasswordHistory { LastUsedDate = model.LastUsedDate }; var task = EncryptObjPropertyAsync(model, ph, new HashSet { "Password" }, key).ContinueWith((t) => { encPhs.Add(ph); }); tasks.Add(task); } await Task.WhenAll(tasks); return encPhs; } private class CipherLocaleComparer : IComparer { private readonly II18nService _i18nService; public CipherLocaleComparer(II18nService i18nService) { _i18nService = i18nService; } public int Compare(CipherView a, CipherView b) { var aName = a?.Name; var bName = b?.Name; if(aName == null && bName != null) { return -1; } if(aName != null && bName == null) { return 1; } if(aName == null && bName == null) { return 0; } var result = _i18nService.StringComparer.Compare(aName, bName); if(result != 0 || a.Type != CipherType.Login || b.Type != CipherType.Login) { return result; } if(a.Login.Username != null) { aName += a.Login.Username; } if(b.Login.Username != null) { bName += b.Login.Username; } return _i18nService.StringComparer.Compare(aName, bName); } } private class CipherLastUsedComparer : IComparer { public int Compare(CipherView a, CipherView b) { var aLastUsed = a.LocalData != null && a.LocalData.ContainsKey("lastUsedDate") ? a.LocalData["lastUsedDate"] as DateTime? : null; var bLastUsed = b.LocalData != null && b.LocalData.ContainsKey("lastUsedDate") ? b.LocalData["lastUsedDate"] as DateTime? : null; var bothNotNull = aLastUsed != null && bLastUsed != null; if(bothNotNull && aLastUsed.Value < bLastUsed.Value) { return 1; } if(aLastUsed != null && bLastUsed == null) { return -1; } if(bothNotNull && aLastUsed.Value > bLastUsed.Value) { return -1; } if(bLastUsed != null && aLastUsed == null) { return 1; } return 0; } } private class CipherLastUsedThenNameComparer : IComparer { private CipherLastUsedComparer _cipherLastUsedComparer; private CipherLocaleComparer _cipherLocaleComparer; public CipherLastUsedThenNameComparer(II18nService i18nService) { _cipherLastUsedComparer = new CipherLastUsedComparer(); _cipherLocaleComparer = new CipherLocaleComparer(i18nService); } public int Compare(CipherView a, CipherView b) { var result = _cipherLastUsedComparer.Compare(a, b); if(result != 0) { return result; } return _cipherLocaleComparer.Compare(a, b); } } } }