diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs index 89c4e098f..f5422ed5d 100644 --- a/src/Core/Services/CipherService.cs +++ b/src/Core/Services/CipherService.cs @@ -1,12 +1,17 @@ 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; @@ -26,11 +31,12 @@ namespace Bit.Core.Services private readonly ISettingsService _settingsService; private readonly IApiService _apiService; private readonly IStorageService _storageService; - private readonly II18nService _i18NService; + 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, @@ -45,7 +51,7 @@ namespace Bit.Core.Services _settingsService = settingsService; _apiService = apiService; _storageService = storageService; - _i18NService = i18nService; + _i18nService = i18nService; } private List DecryptedCipherCache @@ -67,7 +73,7 @@ namespace Bit.Core.Services DecryptedCipherCache = null; } - public async Task Encrypt(CipherView model, SymmetricCryptoKey key = null, + public async Task EncryptAsync(CipherView model, SymmetricCryptoKey key = null, Cipher originalCipher = null) { // Adjust password history @@ -370,8 +376,225 @@ namespace Bit.Core.Services 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 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 RelaceAsync(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(); + } + // 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, @@ -627,7 +850,7 @@ namespace Bit.Core.Services { switch(cipher.Type) { - case Enums.CipherType.Login: + case CipherType.Login: cipher.Login = new Login { PasswordRevisionDate = model.Login.PasswordRevisionDate @@ -652,13 +875,13 @@ namespace Bit.Core.Services } } break; - case Enums.CipherType.SecureNote: + case CipherType.SecureNote: cipher.SecureNote = new SecureNote { Type = model.SecureNote.Type }; break; - case Enums.CipherType.Card: + case CipherType.Card: cipher.Card = new Card(); await EncryptObjPropertyAsync(model.Card, cipher.Card, new HashSet { @@ -670,7 +893,7 @@ namespace Bit.Core.Services "Code" }, key); break; - case Enums.CipherType.Identity: + case CipherType.Identity: cipher.Identity = new Identity(); await EncryptObjPropertyAsync(model.Identity, cipher.Identity, new HashSet { @@ -759,5 +982,99 @@ namespace Bit.Core.Services 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); + } + } } }