diff --git a/src/App/Abstractions/Repositories/IAccountsApiRepository.cs b/src/App/Abstractions/Repositories/IAccountsApiRepository.cs index 26ab8a1aa..980526eb2 100644 --- a/src/App/Abstractions/Repositories/IAccountsApiRepository.cs +++ b/src/App/Abstractions/Repositories/IAccountsApiRepository.cs @@ -8,6 +8,7 @@ namespace Bit.App.Abstractions { Task PostRegisterAsync(RegisterRequest requestObj); Task PostPasswordHintAsync(PasswordHintRequest requestObj); - Task> GetAccountRevisionDate(); + Task> GetAccountRevisionDateAsync(); + Task> GetProfileAsync(); } } \ No newline at end of file diff --git a/src/App/Abstractions/Services/ICryptoService.cs b/src/App/Abstractions/Services/ICryptoService.cs index e0bc27996..934141674 100644 --- a/src/App/Abstractions/Services/ICryptoService.cs +++ b/src/App/Abstractions/Services/ICryptoService.cs @@ -10,14 +10,16 @@ namespace Bit.App.Abstractions CryptoKey PreviousKey { get; } bool KeyChanged { get; } byte[] PrivateKey { get; } - IDictionary OrgKeys { get; set; } + IDictionary OrgKeys { get; set; } void SetPrivateKey(CipherString privateKeyEnc, CryptoKey key); - CryptoKey GetOrgKey(Guid orgId); - void ClearOrgKey(Guid orgId); + CryptoKey GetOrgKey(string orgId); + void ClearOrgKey(string orgId); void ClearKeys(); - CryptoKey AddOrgKey(Guid orgId, CipherString encOrgKey, byte[] privateKey); + CryptoKey AddOrgKey(string orgId, CipherString encOrgKey, byte[] privateKey); string Decrypt(CipherString encyptedValue, CryptoKey key = null); + byte[] DecryptToBytes(CipherString encyptedValue, CryptoKey key = null); + byte[] RsaDecryptToBytes(CipherString encyptedValue, byte[] privateKey); CipherString Encrypt(string plaintextValue, CryptoKey key = null); CryptoKey MakeKeyFromPassword(string password, string salt); string MakeKeyFromPasswordBase64(string password, string salt); diff --git a/src/App/App.csproj b/src/App/App.csproj index 79928db08..72a24b612 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -81,8 +81,10 @@ + + @@ -104,6 +106,7 @@ + diff --git a/src/App/Enums/OrganizationUserStatusType.cs b/src/App/Enums/OrganizationUserStatusType.cs new file mode 100644 index 000000000..de0c87d57 --- /dev/null +++ b/src/App/Enums/OrganizationUserStatusType.cs @@ -0,0 +1,9 @@ +namespace Bit.App.Enums +{ + public enum OrganizationUserStatusType : byte + { + Invited = 0, + Accepted = 1, + Confirmed = 2 + } +} diff --git a/src/App/Enums/OrganizationUserType.cs b/src/App/Enums/OrganizationUserType.cs new file mode 100644 index 000000000..7fd409193 --- /dev/null +++ b/src/App/Enums/OrganizationUserType.cs @@ -0,0 +1,9 @@ +namespace Bit.App.Enums +{ + public enum OrganizationUserType : byte + { + Owner = 0, + Admin = 1, + User = 2 + } +} diff --git a/src/App/Models/Api/Response/ProfileOrganizationResponse.cs b/src/App/Models/Api/Response/ProfileOrganizationResponse.cs new file mode 100644 index 000000000..0d406931c --- /dev/null +++ b/src/App/Models/Api/Response/ProfileOrganizationResponse.cs @@ -0,0 +1,14 @@ +using Bit.App.Enums; + +namespace Bit.App.Models.Api +{ + public class ProfileOrganizationResponseModel + { + public string Id { get; set; } + public string Name { get; set; } + public string Key { get; set; } + public OrganizationUserStatusType Status { get; set; } + public OrganizationUserType Type { get; set; } + public bool Enabled { get; set; } + } +} diff --git a/src/App/Models/Api/Response/ProfileResponse.cs b/src/App/Models/Api/Response/ProfileResponse.cs index 51ed2bc99..1a537e517 100644 --- a/src/App/Models/Api/Response/ProfileResponse.cs +++ b/src/App/Models/Api/Response/ProfileResponse.cs @@ -1,4 +1,6 @@ -namespace Bit.App.Models.Api +using System.Collections.Generic; + +namespace Bit.App.Models.Api { public class ProfileResponse { @@ -8,5 +10,6 @@ public string MasterPasswordHint { get; set; } public string Culture { get; set; } public bool TwoFactorEnabled { get; set; } + public IEnumerable Organizations { get; set; } } } diff --git a/src/App/Models/CipherString.cs b/src/App/Models/CipherString.cs index ae093b2a0..48e3c288f 100644 --- a/src/App/Models/CipherString.cs +++ b/src/App/Models/CipherString.cs @@ -102,12 +102,19 @@ namespace Bit.App.Models public byte[] CipherTextBytes => Convert.FromBase64String(CipherText); public byte[] MacBytes => Mac == null ? null : Convert.FromBase64String(Mac); - public string Decrypt() + public string Decrypt(string orgId = null) { if(_decryptedValue == null) { var cryptoService = Resolver.Resolve(); - _decryptedValue = cryptoService.Decrypt(this); + if(!string.IsNullOrWhiteSpace(orgId)) + { + _decryptedValue = cryptoService.Decrypt(this, cryptoService.GetOrgKey(orgId)); + } + else + { + _decryptedValue = cryptoService.Decrypt(this); + } } return _decryptedValue; diff --git a/src/App/Models/Page/VaultListPageModel.cs b/src/App/Models/Page/VaultListPageModel.cs index 08c4cae27..4f3b986c1 100644 --- a/src/App/Models/Page/VaultListPageModel.cs +++ b/src/App/Models/Page/VaultListPageModel.cs @@ -12,10 +12,10 @@ namespace Bit.App.Models.Page { Id = login.Id; FolderId = login.FolderId; - Name = login.Name?.Decrypt(); - Username = login.Username?.Decrypt() ?? " "; - Password = new Lazy(() => login.Password?.Decrypt()); - Uri = new Lazy(() => login.Uri?.Decrypt()); + Name = login.Name?.Decrypt(login.OrganizationId); + Username = login.Username?.Decrypt(login.OrganizationId) ?? " "; + Password = new Lazy(() => login.Password?.Decrypt(login.OrganizationId)); + Uri = new Lazy(() => login.Uri?.Decrypt(login.OrganizationId)); } public string Id { get; set; } diff --git a/src/App/Models/Page/VaultViewLoginPageModel.cs b/src/App/Models/Page/VaultViewLoginPageModel.cs index dabd31292..af0cb6574 100644 --- a/src/App/Models/Page/VaultViewLoginPageModel.cs +++ b/src/App/Models/Page/VaultViewLoginPageModel.cs @@ -171,11 +171,11 @@ namespace Bit.App.Models.Page public void Update(Login login) { - Name = login.Name?.Decrypt(); - Username = login.Username?.Decrypt(); - Password = login.Password?.Decrypt(); - Uri = login.Uri?.Decrypt(); - Notes = login.Notes?.Decrypt(); + Name = login.Name?.Decrypt(login.OrganizationId); + Username = login.Username?.Decrypt(login.OrganizationId); + Password = login.Password?.Decrypt(login.OrganizationId); + Uri = login.Uri?.Decrypt(login.OrganizationId); + Notes = login.Notes?.Decrypt(login.OrganizationId); } } } diff --git a/src/App/Pages/Vault/VaultEditLoginPage.cs b/src/App/Pages/Vault/VaultEditLoginPage.cs index 9e78b408b..46bbd3dc2 100644 --- a/src/App/Pages/Vault/VaultEditLoginPage.cs +++ b/src/App/Pages/Vault/VaultEditLoginPage.cs @@ -51,25 +51,25 @@ namespace Bit.App.Pages } NotesCell = new FormEditorCell(height: 90); - NotesCell.Editor.Text = login.Notes?.Decrypt(); + NotesCell.Editor.Text = login.Notes?.Decrypt(login.OrganizationId); PasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, nextElement: NotesCell.Editor, useButton: true); - PasswordCell.Entry.Text = login.Password?.Decrypt(); + PasswordCell.Entry.Text = login.Password?.Decrypt(login.OrganizationId); PasswordCell.Button.Image = "eye"; PasswordCell.Entry.DisableAutocapitalize = true; PasswordCell.Entry.Autocorrect = false; PasswordCell.Entry.FontFamily = Device.OnPlatform(iOS: "Courier", Android: "monospace", WinPhone: "Courier"); UsernameCell = new FormEntryCell(AppResources.Username, nextElement: PasswordCell.Entry); - UsernameCell.Entry.Text = login.Username?.Decrypt(); + UsernameCell.Entry.Text = login.Username?.Decrypt(login.OrganizationId); UsernameCell.Entry.DisableAutocapitalize = true; UsernameCell.Entry.Autocorrect = false; UriCell = new FormEntryCell(AppResources.URI, Keyboard.Url, nextElement: UsernameCell.Entry); - UriCell.Entry.Text = login.Uri?.Decrypt(); + UriCell.Entry.Text = login.Uri?.Decrypt(login.OrganizationId); NameCell = new FormEntryCell(AppResources.Name, nextElement: UriCell.Entry); - NameCell.Entry.Text = login.Name?.Decrypt(); + NameCell.Entry.Text = login.Name?.Decrypt(login.OrganizationId); GenerateCell = new ExtendedTextCell { diff --git a/src/App/Repositories/AccountsApiRepository.cs b/src/App/Repositories/AccountsApiRepository.cs index 387be9081..002bd1590 100644 --- a/src/App/Repositories/AccountsApiRepository.cs +++ b/src/App/Repositories/AccountsApiRepository.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Models.Api; using Plugin.Connectivity.Abstractions; +using Newtonsoft.Json; namespace Bit.App.Repositories { @@ -84,7 +85,7 @@ namespace Bit.App.Repositories } } - public virtual async Task> GetAccountRevisionDate() + public virtual async Task> GetAccountRevisionDateAsync() { if(!Connectivity.IsConnected) { @@ -132,5 +133,45 @@ namespace Bit.App.Repositories } } } + + public virtual async Task> GetProfileAsync() + { + if(!Connectivity.IsConnected) + { + return HandledNotConnected(); + } + + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + + using(var client = HttpService.Client) + { + var requestMessage = new TokenHttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/profile")), + }; + + try + { + var response = await client.SendAsync(requestMessage).ConfigureAwait(false); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response).ConfigureAwait(false); + } + + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var responseObj = JsonConvert.DeserializeObject(responseContent); + return ApiResult.Success(responseObj, response.StatusCode); + } + catch + { + return HandledWebException(); + } + } + } } } diff --git a/src/App/Services/AuthService.cs b/src/App/Services/AuthService.cs index d99b46cf6..68f1d8a0b 100644 --- a/src/App/Services/AuthService.cs +++ b/src/App/Services/AuthService.cs @@ -6,6 +6,8 @@ using Bit.App.Models.Api; using Plugin.Settings.Abstractions; using Bit.App.Models; using System.Linq; +using System.Collections.Generic; +using System.Diagnostics; namespace Bit.App.Services { @@ -21,6 +23,7 @@ namespace Bit.App.Services private readonly ISettings _settings; private readonly ICryptoService _cryptoService; private readonly IConnectApiRepository _connectApiRepository; + private readonly IAccountsApiRepository _accountsApiRepository; private readonly IAppIdService _appIdService; private readonly IDeviceInfoService _deviceInfoService; @@ -35,6 +38,7 @@ namespace Bit.App.Services ISettings settings, ICryptoService cryptoService, IConnectApiRepository connectApiRepository, + IAccountsApiRepository accountsApiRepository, IAppIdService appIdService, IDeviceInfoService deviceInfoService) { @@ -43,6 +47,7 @@ namespace Bit.App.Services _settings = settings; _cryptoService = cryptoService; _connectApiRepository = connectApiRepository; + _accountsApiRepository = accountsApiRepository; _appIdService = appIdService; _deviceInfoService = deviceInfoService; } @@ -230,7 +235,7 @@ namespace Bit.App.Services return result; } - ProcessLoginSuccess(key, response.Result); + await ProcessLoginSuccessAsync(key, response.Result); return result; } @@ -257,11 +262,11 @@ namespace Bit.App.Services } result.Success = true; - ProcessLoginSuccess(key, response.Result); + await ProcessLoginSuccessAsync(key, response.Result); return result; } - private void ProcessLoginSuccess(CryptoKey key, TokenResponse response) + private async Task ProcessLoginSuccessAsync(CryptoKey key, TokenResponse response) { if(response.PrivateKey != null) { @@ -274,6 +279,30 @@ namespace Bit.App.Services UserId = _tokenService.TokenUserId; Email = _tokenService.TokenEmail; _settings.AddOrUpdateValue(Constants.LastLoginEmail, Email); + + if(response.PrivateKey != null) + { + var profile = await _accountsApiRepository.GetProfileAsync(); + var orgKeysDict = new Dictionary(); + + if(profile.Succeeded && (profile.Result.Organizations?.Any() ?? false)) + { + foreach(var org in profile.Result.Organizations) + { + try + { + var decBytes = _cryptoService.RsaDecryptToBytes(new CipherString(org.Key), null); + orgKeysDict.Add(org.Id, new CryptoKey(decBytes)); + } + catch + { + Debug.WriteLine($"Cannot set org key {org.Id}. Decryption failed."); + } + } + } + + _cryptoService.OrgKeys = orgKeysDict; + } } } } diff --git a/src/App/Services/CryptoService.cs b/src/App/Services/CryptoService.cs index d8d32b2ac..f398b5dce 100644 --- a/src/App/Services/CryptoService.cs +++ b/src/App/Services/CryptoService.cs @@ -24,7 +24,7 @@ namespace Bit.App.Services private CryptoKey _key; private CryptoKey _legacyEtmKey; private CryptoKey _previousKey; - private IDictionary _orgKeys; + private IDictionary _orgKeys; private byte[] _privateKey; public CryptoService( @@ -127,7 +127,7 @@ namespace Bit.App.Services } } - public IDictionary OrgKeys + public IDictionary OrgKeys { get { @@ -139,8 +139,8 @@ namespace Bit.App.Services var orgKeysDictJson = Encoding.UTF8.GetString(orgKeysDictBytes, 0, orgKeysDictBytes.Length); if(!string.IsNullOrWhiteSpace(orgKeysDictJson)) { - _orgKeys = new Dictionary(); - var orgKeysDict = JsonConvert.DeserializeObject>(orgKeysDictJson); + _orgKeys = new Dictionary(); + var orgKeysDict = JsonConvert.DeserializeObject>(orgKeysDictJson); foreach(var item in orgKeysDict) { _orgKeys.Add(item.Key, new CryptoKey(item.Value)); @@ -155,7 +155,7 @@ namespace Bit.App.Services { if(value != null && value.Any()) { - var dict = new Dictionary(); + var dict = new Dictionary(); foreach(var item in value) { dict.Add(item.Key, item.Value.Key); @@ -180,7 +180,7 @@ namespace Bit.App.Services PrivateKey = bytes; } - public CryptoKey GetOrgKey(Guid orgId) + public CryptoKey GetOrgKey(string orgId) { if(OrgKeys == null || !OrgKeys.ContainsKey(orgId)) { @@ -190,7 +190,7 @@ namespace Bit.App.Services return OrgKeys[orgId]; } - public void ClearOrgKey(Guid orgId) + public void ClearOrgKey(string orgId) { var localOrgKeys = OrgKeys; if(localOrgKeys == null || !localOrgKeys.ContainsKey(orgId)) @@ -210,7 +210,7 @@ namespace Bit.App.Services PrivateKey = null; } - public CryptoKey AddOrgKey(Guid orgId, CipherString encOrgKey, byte[] privateKey) + public CryptoKey AddOrgKey(string orgId, CipherString encOrgKey, byte[] privateKey) { try { diff --git a/src/App/Services/LoginService.cs b/src/App/Services/LoginService.cs index b9dd18d66..fb0c4d769 100644 --- a/src/App/Services/LoginService.cs +++ b/src/App/Services/LoginService.cs @@ -125,7 +125,7 @@ namespace Bit.App.Services continue; } - var loginUriString = new CipherString(login.Uri).Decrypt(); + var loginUriString = new CipherString(login.Uri).Decrypt(login.OrganizationId); if(string.IsNullOrWhiteSpace(loginUriString)) { continue; diff --git a/src/App/Services/SyncService.cs b/src/App/Services/SyncService.cs index db6fc3c69..202906ce3 100644 --- a/src/App/Services/SyncService.cs +++ b/src/App/Services/SyncService.cs @@ -217,7 +217,7 @@ namespace Bit.App.Services return true; } - var accountRevisionDate = await _accountsApiRepository.GetAccountRevisionDate(); + var accountRevisionDate = await _accountsApiRepository.GetAccountRevisionDateAsync(); if(accountRevisionDate.Succeeded && accountRevisionDate.Result.HasValue && accountRevisionDate.Result.Value > lastSync) { diff --git a/src/iOS.Extension/Models/LoginViewModel.cs b/src/iOS.Extension/Models/LoginViewModel.cs index 8121faa26..6adca6e55 100644 --- a/src/iOS.Extension/Models/LoginViewModel.cs +++ b/src/iOS.Extension/Models/LoginViewModel.cs @@ -7,10 +7,10 @@ namespace Bit.iOS.Extension.Models public LoginViewModel(Login login) { Id = login.Id; - Name = login.Name?.Decrypt(); - Username = login.Username?.Decrypt(); - Password = login.Password?.Decrypt(); - Uri = login.Uri?.Decrypt(); + Name = login.Name?.Decrypt(login.OrganizationId); + Username = login.Username?.Decrypt(login.OrganizationId); + Password = login.Password?.Decrypt(login.OrganizationId); + Uri = login.Uri?.Decrypt(login.OrganizationId); } public string Id { get; set; }