From 2bb72076ae97b18b3d96209c1633458192720014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Federico=20Andre=CC=81s=20Maccaroni?= Date: Mon, 20 Mar 2023 13:41:21 -0300 Subject: [PATCH] PM-115 Added new cipher key and encryption/decryption mechanisms on cipher --- src/Core/Constants.cs | 1 + src/Core/Models/Data/CipherData.cs | 4 ++ src/Core/Models/Domain/Attachment.cs | 35 ++++++------ src/Core/Models/Domain/Card.cs | 4 +- src/Core/Models/Domain/Cipher.cs | 62 +++++++++++++++------- src/Core/Models/Domain/Field.cs | 4 +- src/Core/Models/Domain/Identity.cs | 4 +- src/Core/Models/Domain/Login.cs | 6 +-- src/Core/Models/Domain/LoginUri.cs | 4 +- src/Core/Models/Domain/PasswordHistory.cs | 4 +- src/Core/Models/Domain/SecureNote.cs | 2 +- src/Core/Models/Request/CipherRequest.cs | 4 ++ src/Core/Models/Response/CipherResponse.cs | 2 + src/Core/Models/View/CipherView.cs | 3 ++ src/Core/Services/CipherService.cs | 54 +++++++++++++------ 15 files changed, 130 insertions(+), 63 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5db98b756..95bc90d74 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -59,6 +59,7 @@ public const int Argon2MemoryInMB = 64; public const int Argon2Parallelism = 4; public const int MasterPasswordMinimumChars = 12; + public const int CipherKeyRandomBytesLength = 64; public static readonly string[] AndroidAllClearCipherCacheKeys = { diff --git a/src/Core/Models/Data/CipherData.cs b/src/Core/Models/Data/CipherData.cs index c571771a7..9599cee78 100644 --- a/src/Core/Models/Data/CipherData.cs +++ b/src/Core/Models/Data/CipherData.cs @@ -26,6 +26,8 @@ namespace Bit.Core.Models.Data Notes = response.Notes; CollectionIds = collectionIds?.ToList() ?? response.CollectionIds; Reprompt = response.Reprompt; + Key = response.Key; + ForceKeyRotation = response.ForceKeyRotation; try // Added to address Issue (https://github.com/bitwarden/mobile/issues/1006) { @@ -86,5 +88,7 @@ namespace Bit.Core.Models.Data public List CollectionIds { get; set; } public DateTime? DeletedDate { get; set; } public Enums.CipherRepromptType Reprompt { get; set; } + public string Key { get; set; } + public bool ForceKeyRotation { get; set; } } } diff --git a/src/Core/Models/Domain/Attachment.cs b/src/Core/Models/Domain/Attachment.cs index 3e71d6107..b12395a58 100644 --- a/src/Core/Models/Domain/Attachment.cs +++ b/src/Core/Models/Domain/Attachment.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Abstractions; using Bit.Core.Models.Data; using Bit.Core.Models.View; +using Bit.Core.Services; using Bit.Core.Utilities; namespace Bit.Core.Models.Domain @@ -11,11 +13,11 @@ namespace Bit.Core.Models.Domain { private HashSet _map = new HashSet { - "Id", - "Url", - "SizeName", - "FileName", - "Key" + nameof(Id), + nameof(Url), + nameof(SizeName), + nameof(FileName), + nameof(Key) }; public Attachment() { } @@ -23,7 +25,7 @@ namespace Bit.Core.Models.Domain public Attachment(AttachmentData obj, bool alreadyEncrypted = false) { Size = obj.Size; - BuildDomainModel(this, obj, _map, alreadyEncrypted, new HashSet { "Id", "Url", "SizeName" }); + BuildDomainModel(this, obj, _map, alreadyEncrypted, new HashSet { nameof(Id), nameof(Url), nameof(SizeName) }); } public string Id { get; set; } @@ -33,25 +35,26 @@ namespace Bit.Core.Models.Domain public EncString Key { get; set; } public EncString FileName { get; set; } - public async Task DecryptAsync(string orgId) + public async Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) { var view = await DecryptObjAsync(new AttachmentView(this), this, new HashSet { - "FileName" - }, orgId); + nameof(FileName) + }, orgId, key); if (Key != null) { - var cryptoService = ServiceContainer.Resolve("cryptoService"); try { - var orgKey = await cryptoService.GetOrgKeyAsync(orgId); - var decValue = await cryptoService.DecryptToBytesAsync(Key, orgKey); + var cryptoService = ServiceContainer.Resolve(); + + var decryptKey = key ?? await cryptoService.GetOrgKeyAsync(orgId); + var decValue = await cryptoService.DecryptToBytesAsync(Key, decryptKey); view.Key = new SymmetricCryptoKey(decValue); } - catch + catch (Exception ex) { - // TODO: error? + LoggerHelper.LogEvenIfCantBeResolved(ex); } } return view; @@ -61,7 +64,7 @@ namespace Bit.Core.Models.Domain { var a = new AttachmentData(); a.Size = Size; - BuildDataModel(this, a, _map, new HashSet { "Id", "Url", "SizeName" }); + BuildDataModel(this, a, _map, new HashSet { nameof(Id), nameof(Url), nameof(SizeName) }); return a; } } diff --git a/src/Core/Models/Domain/Card.cs b/src/Core/Models/Domain/Card.cs index 8a7ffff23..dfde0a437 100644 --- a/src/Core/Models/Domain/Card.cs +++ b/src/Core/Models/Domain/Card.cs @@ -31,9 +31,9 @@ namespace Bit.Core.Models.Domain public EncString ExpYear { get; set; } public EncString Code { get; set; } - public Task DecryptAsync(string orgId) + public Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) { - return DecryptObjAsync(new CardView(this), this, _map, orgId); + return DecryptObjAsync(new CardView(this), this, _map, orgId, key); } public CardData ToCardData() diff --git a/src/Core/Models/Domain/Cipher.cs b/src/Core/Models/Domain/Cipher.cs index d744f6a57..a8aba796a 100644 --- a/src/Core/Models/Domain/Cipher.cs +++ b/src/Core/Models/Domain/Cipher.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.View; +using Bit.Core.Utilities; namespace Bit.Core.Models.Domain { @@ -16,12 +19,12 @@ namespace Bit.Core.Models.Domain { BuildDomainModel(this, obj, new HashSet { - "Id", - "OrganizationId", - "FolderId", - "Name", - "Notes" - }, alreadyEncrypted, new HashSet { "Id", "OrganizationId", "FolderId" }); + nameof(Id), + nameof(OrganizationId), + nameof(FolderId), + nameof(Name), + nameof(Notes) + }, alreadyEncrypted, new HashSet { nameof(Id), nameof(OrganizationId), nameof(FolderId) }); Type = obj.Type; Favorite = obj.Favorite; @@ -32,6 +35,12 @@ namespace Bit.Core.Models.Domain CollectionIds = obj.CollectionIds != null ? new HashSet(obj.CollectionIds) : null; LocalData = localData; Reprompt = obj.Reprompt; + ForceKeyRotation = obj.ForceKeyRotation; + + if (obj.Key != null) + { + Key = new EncString(obj.Key); + } switch (Type) { @@ -79,29 +88,44 @@ namespace Bit.Core.Models.Domain public HashSet CollectionIds { get; set; } public DateTime? DeletedDate { get; set; } public CipherRepromptType Reprompt { get; set; } + public EncString Key { get; set; } + public bool ForceKeyRotation { get; set; } public async Task DecryptAsync() { var model = new CipherView(this); + + if(Key != null) + { + // HACK: I don't like resolving this here but I can't see a better way without + // refactoring a lot of things. + var cryptoService = ServiceContainer.Resolve(); + + var orgKey = await cryptoService.GetOrgKeyAsync(OrganizationId); + + var key = await cryptoService.DecryptToBytesAsync(Key, orgKey); + model.Key = new SymmetricCryptoKey(key); + } + await DecryptObjAsync(model, this, new HashSet { - "Name", - "Notes" - }, OrganizationId); + nameof(Name), + nameof(Notes) + }, OrganizationId, model.Key); switch (Type) { case Enums.CipherType.Login: - model.Login = await Login.DecryptAsync(OrganizationId); + model.Login = await Login.DecryptAsync(OrganizationId, model.Key); break; case Enums.CipherType.SecureNote: - model.SecureNote = await SecureNote.DecryptAsync(OrganizationId); + model.SecureNote = await SecureNote.DecryptAsync(OrganizationId, model.Key); break; case Enums.CipherType.Card: - model.Card = await Card.DecryptAsync(OrganizationId); + model.Card = await Card.DecryptAsync(OrganizationId, model.Key); break; case Enums.CipherType.Identity: - model.Identity = await Identity.DecryptAsync(OrganizationId); + model.Identity = await Identity.DecryptAsync(OrganizationId, model.Key); break; default: break; @@ -113,7 +137,7 @@ namespace Bit.Core.Models.Domain var tasks = new List(); async Task decryptAndAddAttachmentAsync(Attachment attachment) { - var decAttachment = await attachment.DecryptAsync(OrganizationId); + var decAttachment = await attachment.DecryptAsync(OrganizationId, model.Key); model.Attachments.Add(decAttachment); } foreach (var attachment in Attachments) @@ -128,7 +152,7 @@ namespace Bit.Core.Models.Domain var tasks = new List(); async Task decryptAndAddFieldAsync(Field field) { - var decField = await field.DecryptAsync(OrganizationId); + var decField = await field.DecryptAsync(OrganizationId, model.Key); model.Fields.Add(decField); } foreach (var field in Fields) @@ -143,7 +167,7 @@ namespace Bit.Core.Models.Domain var tasks = new List(); async Task decryptAndAddHistoryAsync(PasswordHistory ph) { - var decPh = await ph.DecryptAsync(OrganizationId); + var decPh = await ph.DecryptAsync(OrganizationId, model.Key); model.PasswordHistory.Add(decPh); } foreach (var ph in PasswordHistory) @@ -171,11 +195,13 @@ namespace Bit.Core.Models.Domain CollectionIds = CollectionIds.ToList(), DeletedDate = DeletedDate, Reprompt = Reprompt, + Key = Key?.EncryptedString, + ForceKeyRotation = ForceKeyRotation }; BuildDataModel(this, c, new HashSet { - "Name", - "Notes" + nameof(Name), + nameof(Notes) }); switch (c.Type) { diff --git a/src/Core/Models/Domain/Field.cs b/src/Core/Models/Domain/Field.cs index cbfdcbca3..7d962a669 100644 --- a/src/Core/Models/Domain/Field.cs +++ b/src/Core/Models/Domain/Field.cs @@ -28,9 +28,9 @@ namespace Bit.Core.Models.Domain public FieldType Type { get; set; } public LinkedIdType? LinkedId { get; set; } - public Task DecryptAsync(string orgId) + public Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) { - return DecryptObjAsync(new FieldView(this), this, _map, orgId); + return DecryptObjAsync(new FieldView(this), this, _map, orgId, key); } public FieldData ToFieldData() diff --git a/src/Core/Models/Domain/Identity.cs b/src/Core/Models/Domain/Identity.cs index 4a705dcb6..5585eeddb 100644 --- a/src/Core/Models/Domain/Identity.cs +++ b/src/Core/Models/Domain/Identity.cs @@ -55,9 +55,9 @@ namespace Bit.Core.Models.Domain public EncString PassportNumber { get; set; } public EncString LicenseNumber { get; set; } - public Task DecryptAsync(string orgId) + public Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) { - return DecryptObjAsync(new IdentityView(this), this, _map, orgId); + return DecryptObjAsync(new IdentityView(this), this, _map, orgId, key); } public IdentityData ToIdentityData() diff --git a/src/Core/Models/Domain/Login.cs b/src/Core/Models/Domain/Login.cs index 00c0ab07b..11b47f2ec 100644 --- a/src/Core/Models/Domain/Login.cs +++ b/src/Core/Models/Domain/Login.cs @@ -29,20 +29,20 @@ namespace Bit.Core.Models.Domain public DateTime? PasswordRevisionDate { get; set; } public EncString Totp { get; set; } - public async Task DecryptAsync(string orgId) + public async Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) { var view = await DecryptObjAsync(new LoginView(this), this, new HashSet { "Username", "Password", "Totp" - }, orgId); + }, orgId, key); if (Uris != null) { view.Uris = new List(); foreach (var uri in Uris) { - view.Uris.Add(await uri.DecryptAsync(orgId)); + view.Uris.Add(await uri.DecryptAsync(orgId, key)); } } return view; diff --git a/src/Core/Models/Domain/LoginUri.cs b/src/Core/Models/Domain/LoginUri.cs index 31747fd96..e9b26d6d1 100644 --- a/src/Core/Models/Domain/LoginUri.cs +++ b/src/Core/Models/Domain/LoginUri.cs @@ -24,9 +24,9 @@ namespace Bit.Core.Models.Domain public EncString Uri { get; set; } public UriMatchType? Match { get; set; } - public Task DecryptAsync(string orgId) + public Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) { - return DecryptObjAsync(new LoginUriView(this), this, _map, orgId); + return DecryptObjAsync(new LoginUriView(this), this, _map, orgId, key); } public LoginUriData ToLoginUriData() diff --git a/src/Core/Models/Domain/PasswordHistory.cs b/src/Core/Models/Domain/PasswordHistory.cs index 6ae1ab1f1..47a969fdd 100644 --- a/src/Core/Models/Domain/PasswordHistory.cs +++ b/src/Core/Models/Domain/PasswordHistory.cs @@ -24,9 +24,9 @@ namespace Bit.Core.Models.Domain public EncString Password { get; set; } public DateTime LastUsedDate { get; set; } - public Task DecryptAsync(string orgId) + public Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) { - return DecryptObjAsync(new PasswordHistoryView(this), this, _map, orgId); + return DecryptObjAsync(new PasswordHistoryView(this), this, _map, orgId, key); } public PasswordHistoryData ToPasswordHistoryData() diff --git a/src/Core/Models/Domain/SecureNote.cs b/src/Core/Models/Domain/SecureNote.cs index 55817ab15..b30be2fdb 100644 --- a/src/Core/Models/Domain/SecureNote.cs +++ b/src/Core/Models/Domain/SecureNote.cs @@ -16,7 +16,7 @@ namespace Bit.Core.Models.Domain public SecureNoteType Type { get; set; } - public Task DecryptAsync(string orgId) + public Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) { return Task.FromResult(new SecureNoteView(this)); } diff --git a/src/Core/Models/Request/CipherRequest.cs b/src/Core/Models/Request/CipherRequest.cs index 82c029751..3a1a18eb0 100644 --- a/src/Core/Models/Request/CipherRequest.cs +++ b/src/Core/Models/Request/CipherRequest.cs @@ -19,6 +19,8 @@ namespace Bit.Core.Models.Request Favorite = cipher.Favorite; LastKnownRevisionDate = cipher.RevisionDate; Reprompt = cipher.Reprompt; + Key = cipher.Key?.EncryptedString; + ForceKeyRotation = cipher.ForceKeyRotation; switch (Type) { @@ -124,5 +126,7 @@ namespace Bit.Core.Models.Request public Dictionary Attachments2 { get; set; } public DateTime LastKnownRevisionDate { get; set; } public CipherRepromptType Reprompt { get; set; } + public string Key { get; set; } + public bool ForceKeyRotation { get; set; } } } diff --git a/src/Core/Models/Response/CipherResponse.cs b/src/Core/Models/Response/CipherResponse.cs index 070d41eee..373987b16 100644 --- a/src/Core/Models/Response/CipherResponse.cs +++ b/src/Core/Models/Response/CipherResponse.cs @@ -28,5 +28,7 @@ namespace Bit.Core.Models.Response public List CollectionIds { get; set; } public DateTime? DeletedDate { get; set; } public CipherRepromptType Reprompt { get; set; } + public string Key { get; set; } + public bool ForceKeyRotation { get; set; } } } diff --git a/src/Core/Models/View/CipherView.cs b/src/Core/Models/View/CipherView.cs index 6bec74ee4..7047accd9 100644 --- a/src/Core/Models/View/CipherView.cs +++ b/src/Core/Models/View/CipherView.cs @@ -25,6 +25,7 @@ namespace Bit.Core.Models.View RevisionDate = c.RevisionDate; DeletedDate = c.DeletedDate; Reprompt = c.Reprompt; + ForceKeyRotation = c.ForceKeyRotation; } public string Id { get; set; } @@ -49,6 +50,8 @@ namespace Bit.Core.Models.View public DateTime RevisionDate { get; set; } public DateTime? DeletedDate { get; set; } public CipherRepromptType Reprompt { get; set; } + public SymmetricCryptoKey Key { get; set; } + public bool ForceKeyRotation { get; set; } public ItemView Item { diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs index e289a1ae6..6aeec2a47 100644 --- a/src/Core/Services/CipherService.cs +++ b/src/Core/Services/CipherService.cs @@ -177,9 +177,30 @@ namespace Bit.Core.Services Type = model.Type, CollectionIds = model.CollectionIds, RevisionDate = model.RevisionDate, - Reprompt = model.Reprompt + Reprompt = model.Reprompt, + ForceKeyRotation = model.ForceKeyRotation }; + key = await UpdateCipherAndGetCipherKeyAsync(cipher, model, key); + + var tasks = new List + { + EncryptObjPropertyAsync(model, cipher, new HashSet + { + nameof(CipherView.Name), + nameof(CipherView.Notes) + }, key), + EncryptCipherDataAsync(cipher, model, key), + EncryptFieldsAsync(model.Fields, key, cipher), + EncryptPasswordHistoriesAsync(model.PasswordHistory, key, cipher), + EncryptAttachmentsAsync(model.Attachments, key, cipher) + }; + await Task.WhenAll(tasks); + return cipher; + } + + private async Task UpdateCipherAndGetCipherKeyAsync(Cipher cipher, CipherView cipherView, SymmetricCryptoKey key = null) + { if (key == null && cipher.OrganizationId != null) { key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId); @@ -189,20 +210,21 @@ namespace Bit.Core.Services } } - var tasks = new List + if (cipherView.Key != null && !cipherView.ForceKeyRotation) { - EncryptObjPropertyAsync(model, cipher, new HashSet - { - "Name", - "Notes" - }, key), - EncryptCipherDataAsync(cipher, model, key), - EncryptFieldsAsync(model.Fields, key, cipher), - EncryptPasswordHistoriesAsync(model.PasswordHistory, key, cipher), - EncryptAttachmentsAsync(model.Attachments, key, cipher) - }; - await Task.WhenAll(tasks); - return cipher; + cipher.Key = await _cryptoService.EncryptAsync(cipherView.Key.Key, key); + return cipherView.Key; + } +#if DEBUG + // turned on, only on debug to check that the enc/decryption is working fine at the cipher level. + // this will be allowed on production on a later release. + var cfs = ServiceContainer.Resolve(); + var newKey = new SymmetricCryptoKey(await cfs.RandomBytesAsync(Core.Constants.CipherKeyRandomBytesLength)); + cipher.Key = await _cryptoService.EncryptAsync(newKey.Key, key); + return newKey; +#else + return key; +#endif } public async Task GetAsync(string id) @@ -526,6 +548,7 @@ namespace Bit.Core.Services var request = new CipherRequest(cipher); response = await _apiService.PutCipherAsync(cipher.Id, request); } + var userId = await _stateService.GetActiveUserIdAsync(); var data = new CipherData(response, userId, cipher.CollectionIds); await UpsertAsync(data); @@ -574,6 +597,7 @@ namespace Bit.Core.Services var uploadDataResponse = await _apiService.PostCipherAttachmentAsync(cipher.Id, request); response = uploadDataResponse.CipherResponse; + await _fileUploadService.UploadCipherAttachmentFileAsync(uploadDataResponse, encFileName, encFileData); } catch (ApiException e) when (e.Error.StatusCode == System.Net.HttpStatusCode.NotFound || e.Error.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed) @@ -1054,7 +1078,7 @@ namespace Bit.Core.Services { await EncryptObjPropertyAsync(model, attachment, new HashSet { - "FileName" + nameof(AttachmentView.FileName) }, key); if (model.Key != null) {