diff --git a/src/Core/Models/Domain/Collection.cs b/src/Core/Models/Domain/Collection.cs index b917c2207..5e0578507 100644 --- a/src/Core/Models/Domain/Collection.cs +++ b/src/Core/Models/Domain/Collection.cs @@ -33,9 +33,9 @@ namespace Bit.Core.Models.Domain public string ExternalId { get; set; } public bool ReadOnly { get; set; } - public Task DecryptAsync(string orgId) + public Task DecryptAsync() { - return DecryptObjAsync(new CollectionView(this), this, new HashSet { "Name" }, orgId); + return DecryptObjAsync(new CollectionView(this), this, new HashSet { "Name" }, OrganizationId); } } } diff --git a/src/Core/Services/CollectionService.cs b/src/Core/Services/CollectionService.cs new file mode 100644 index 000000000..4fdff9432 --- /dev/null +++ b/src/Core/Services/CollectionService.cs @@ -0,0 +1,245 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class CollectionService + { + private const string Keys_CollectionsFormat = "collections_{0}"; + private const char NestingDelimiter = '/'; + + private List _decryptedCollectionCache; + private readonly ICryptoService _cryptoService; + private readonly IUserService _userService; + private readonly IApiService _apiService; + private readonly IStorageService _storageService; + private readonly II18nService _i18nService; + private readonly ICipherService _cipherService; + + public CollectionService( + ICryptoService cryptoService, + IUserService userService, + IApiService apiService, + IStorageService storageService, + II18nService i18nService, + ICipherService cipherService) + { + _cryptoService = cryptoService; + _userService = userService; + _apiService = apiService; + _storageService = storageService; + _i18nService = i18nService; + _cipherService = cipherService; + } + + public void ClearCache() + { + _decryptedCollectionCache = null; + } + + public async Task EncryptAsync(CollectionView model) + { + if(model.OrganizationId == null) + { + throw new Exception("Collection has no organization id."); + } + var key = await _cryptoService.GetOrgKeyAsync(model.OrganizationId); + if(key == null) + { + throw new Exception("No key for this collection's organization."); + } + var collection = new Collection + { + Id = model.Id, + OrganizationId = model.OrganizationId, + ReadOnly = model.ReadOnly, + Name = await _cryptoService.EncryptAsync(model.Name, key) + }; + return collection; + } + + public async Task> DecryptManyAsync(List collections) + { + if(collections == null) + { + return new List(); + } + var decCollections = new List(); + var tasks = new List(); + foreach(var collection in collections) + { + tasks.Add(collection.DecryptAsync().ContinueWith(async c => decCollections.Add(await c))); + } + await Task.WhenAll(tasks); + return decCollections.OrderBy(c => c, new CollectionLocaleComparer(_i18nService)).ToList(); + } + + public async Task GetAsync(string id) + { + var userId = await _userService.GetUserIdAsync(); + var collections = await _storageService.GetAsync>( + string.Format(Keys_CollectionsFormat, userId)); + if(!collections?.ContainsKey(id) ?? true) + { + return null; + } + return new Collection(collections[id]); + } + + public async Task> GetAllAsync() + { + var userId = await _userService.GetUserIdAsync(); + var collections = await _storageService.GetAsync>( + string.Format(Keys_CollectionsFormat, userId)); + var response = collections.Select(c => new Collection(c.Value)); + return response.ToList(); + } + + // TODO: sequentialize? + public async Task> GetAllDecryptedAsync() + { + if(_decryptedCollectionCache != null) + { + return _decryptedCollectionCache; + } + var hasKey = await _cryptoService.HasKeyAsync(); + if(!hasKey) + { + throw new Exception("No key."); + } + var collections = await GetAllAsync(); + _decryptedCollectionCache = await DecryptManyAsync(collections); + return _decryptedCollectionCache; + } + + public async Task>> GetAllNestedAsync(List collections = null) + { + if(collections == null) + { + collections = await GetAllDecryptedAsync(); + } + var nodes = new List>(); + foreach(var c in collections) + { + var collectionCopy = new CollectionView + { + Id = c.Id, + OrganizationId = c.OrganizationId + }; + CoreHelpers.NestedTraverse(nodes, 0, + Regex.Replace(c.Name, "^\\/+|\\/+$", string.Empty).Split(NestingDelimiter), + collectionCopy, null, NestingDelimiter); + } + return nodes; + } + + public async Task> GetNestedAsync(string id) + { + var collections = await GetAllNestedAsync(); + return CoreHelpers.GetTreeNodeObject(collections, id); + } + + public async Task UpsertAsync(CollectionData collection) + { + var userId = await _userService.GetUserIdAsync(); + var storageKey = string.Format(Keys_CollectionsFormat, userId); + var collections = await _storageService.GetAsync>(storageKey); + if(collections == null) + { + collections = new Dictionary(); + } + if(!collections.ContainsKey(collection.Id)) + { + collections.Add(collection.Id, null); + } + collections[collection.Id] = collection; + await _storageService.SaveAsync(storageKey, collections); + _decryptedCollectionCache = null; + } + + public async Task UpsertAsync(List collection) + { + var userId = await _userService.GetUserIdAsync(); + var storageKey = string.Format(Keys_CollectionsFormat, userId); + var collections = await _storageService.GetAsync>(storageKey); + if(collections == null) + { + collections = new Dictionary(); + } + foreach(var c in collection) + { + if(!collections.ContainsKey(c.Id)) + { + collections.Add(c.Id, null); + } + collections[c.Id] = c; + } + await _storageService.SaveAsync(storageKey, collections); + _decryptedCollectionCache = null; + } + + public async Task ReplaceAsync(Dictionary collections) + { + var userId = await _userService.GetUserIdAsync(); + await _storageService.SaveAsync(string.Format(Keys_CollectionsFormat, userId), collections); + _decryptedCollectionCache = null; + } + + public async Task ClearAsync(string userId) + { + await _storageService.RemoveAsync(string.Format(Keys_CollectionsFormat, userId)); + _decryptedCollectionCache = null; + } + + public async Task DeleteAsync(string id) + { + var userId = await _userService.GetUserIdAsync(); + var collectionKey = string.Format(Keys_CollectionsFormat, userId); + var collections = await _storageService.GetAsync>(collectionKey); + if(collections == null || !collections.ContainsKey(id)) + { + return; + } + collections.Remove(id); + await _storageService.SaveAsync(collectionKey, collections); + _decryptedCollectionCache = null; + } + + private class CollectionLocaleComparer : IComparer + { + private readonly II18nService _i18nService; + + public CollectionLocaleComparer(II18nService i18nService) + { + _i18nService = i18nService; + } + + public int Compare(CollectionView a, CollectionView 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; + } + return _i18nService.StringComparer.Compare(aName, bName); + } + } + } +} diff --git a/src/Core/Services/FolderService.cs b/src/Core/Services/FolderService.cs index 154a5ccca..2d2a6bb57 100644 --- a/src/Core/Services/FolderService.cs +++ b/src/Core/Services/FolderService.cs @@ -8,6 +8,7 @@ using Bit.Core.Utilities; using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Bit.Core.Services @@ -85,8 +86,8 @@ namespace Bit.Core.Services { return _decryptedFolderCache; } - var hashKey = await _cryptoService.HasKeyAsync(); - if(!hashKey) + var hasKey = await _cryptoService.HasKeyAsync(); + if(!hasKey) { throw new Exception("No key."); } @@ -121,8 +122,9 @@ namespace Bit.Core.Services Id = f.Id, RevisionDate = f.RevisionDate }; - CoreHelpers.NestedTraverse(nodes, 0, f.Name.Split(NestingDelimiter), folderCopy, null, - NestingDelimiter); + CoreHelpers.NestedTraverse(nodes, 0, + Regex.Replace(f.Name, "^\\/+|\\/+$", string.Empty).Split(NestingDelimiter), + folderCopy, null, NestingDelimiter); } return nodes; }