diff --git a/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs b/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs index 3e234e939..2b7345796 100644 --- a/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs +++ b/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs @@ -9,6 +9,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Request; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.Essentials; namespace Bit.App.Pages { @@ -19,6 +20,7 @@ namespace Bit.App.Pages private bool _requestAdminApprovalEnabled; private bool _approveWithMasterPasswordEnabled; private bool _continueEnabled; + private string _email; private readonly IStateService _stateService; private readonly IApiService _apiService; @@ -52,6 +54,8 @@ namespace Bit.App.Pages allowsMultipleExecutions: false); } + public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email); + public bool RememberThisDevice { get => _rememberThisDevice; @@ -82,6 +86,15 @@ namespace Bit.App.Pages set => SetProperty(ref _continueEnabled, value); } + public string Email + { + get => _email; + set => SetProperty(ref _email, value, additionalPropertyNames: + new string[] { + nameof(LoggingInAsText) + }); + } + public async Task InitAsync() { // Appears if the browser is trusted and shared the key with the app diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 87a196f4b..5091bf4a6 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -3631,6 +3631,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Logging in as {0}. + /// + public static string LoggingInAsX { + get { + return ResourceManager.GetString("LoggingInAsX", resourceCulture); + } + } + /// /// Looks up a localized string similar to Logging in as {0} on {1}. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 3c4edb21f..2b9eda0a4 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2661,4 +2661,7 @@ Do you want to switch to this account? Invalid API token + + Logging in as {0} + diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index e393689a4..7b21a5c1e 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; using Bit.Core.Models.Response; @@ -92,9 +93,10 @@ namespace Bit.Core.Abstractions Task PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved); Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest); Task GetKnownDeviceAsync(string email, string deviceIdentifier); + Task GetDeviceByIdentifierAsync(string deviceIdentifier); + Task UpdateTrustedDeviceKeysAsync(string deviceIdentifier, TrustedDeviceKeysRequest deviceRequest); Task GetOrgDomainSsoDetailsAsync(string email); Task GetDevicesExistenceByTypes(DeviceType[] deviceTypes); - Task PutUpdateTrustedDeviceKeys(UpdateTrustedDeviceKeysRequest request); Task GetConfigsAsync(); Task GetFastmailAccountIdAsync(string apiKey); } diff --git a/src/Core/Abstractions/IDeviceTrustCryptoService.cs b/src/Core/Abstractions/IDeviceTrustCryptoService.cs new file mode 100644 index 000000000..d4fc273a0 --- /dev/null +++ b/src/Core/Abstractions/IDeviceTrustCryptoService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IDeviceTrustCryptoService + { + Task GetDeviceKeyAsync(); + Task TrustDeviceAsync(); + } +} diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs index 7a26d49b8..7303e5401 100644 --- a/src/Core/Abstractions/IStateService.cs +++ b/src/Core/Abstractions/IStateService.cs @@ -56,6 +56,8 @@ namespace Bit.Core.Abstractions Task SetOrgKeysEncryptedAsync(Dictionary value, string userId = null); Task GetPrivateKeyEncryptedAsync(string userId = null); Task SetPrivateKeyEncryptedAsync(string value, string userId = null); + Task GetDeviceKeyAsync(string userId = null); + Task SetDeviceKeyAsync(SymmetricCryptoKey value, string userId = null); Task> GetAutofillBlacklistedUrisAsync(string userId = null); Task SetAutofillBlacklistedUrisAsync(List value, string userId = null); Task GetAutofillTileAddedAsync(); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 73863f580..2e4ba55e9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -91,6 +91,7 @@ public static string EncOrgKeysKey(string userId) => $"encOrgKeys_{userId}"; public static string EncPrivateKeyKey(string userId) => $"encPrivateKey_{userId}"; public static string EncKeyKey(string userId) => $"encKey_{userId}"; + public static string DeviceKeyKey(string userId) => $"deviceKey_{userId}"; public static string KeyHashKey(string userId) => $"keyHash_{userId}"; public static string PinProtectedKey(string userId) => $"pinProtectedKey_{userId}"; public static string PassGenOptionsKey(string userId) => $"passwordGenerationOptions_{userId}"; diff --git a/src/Core/Models/Request/TrustedDeviceKeysRequest.cs b/src/Core/Models/Request/TrustedDeviceKeysRequest.cs new file mode 100644 index 000000000..77e52b18c --- /dev/null +++ b/src/Core/Models/Request/TrustedDeviceKeysRequest.cs @@ -0,0 +1,10 @@ + +namespace Bit.Core.Models.Request +{ + public class TrustedDeviceKeysRequest + { + public string EncryptedUserKey { get; set; } + public string EncryptedPublicKey { get; set; } + public string EncryptedPrivateKey { get; set; } + } +} diff --git a/src/Core/Models/Request/UpdateTrustedDeviceKeysRequest.cs b/src/Core/Models/Request/UpdateTrustedDeviceKeysRequest.cs deleted file mode 100644 index 90ae4098e..000000000 --- a/src/Core/Models/Request/UpdateTrustedDeviceKeysRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -namespace Bit.Core.Models.Request -{ - public class UpdateTrustedDeviceKeysRequest - { - public string DeviceIdentifier { get; set; } - public string DevicePublicKeyEncryptedUserKey { get; set; } - public string UserKeyEncryptedDevicePublicKey { get; set; } - public string DeviceKeyEncryptedDevicePrivateKey { get; set; } - } -} - diff --git a/src/Core/Models/Response/DeviceResponse.cs b/src/Core/Models/Response/DeviceResponse.cs index 39c6de52e..331d0562a 100644 --- a/src/Core/Models/Response/DeviceResponse.cs +++ b/src/Core/Models/Response/DeviceResponse.cs @@ -1,17 +1,13 @@ -using System; -using Bit.Core.Enums; +using Bit.Core.Enums; -namespace Bit.Core.Models.Response +public class DeviceResponse { - public class DeviceResponse - { - public string Id { get; set; } - public string UserId { get; set; } - public string Name { get; set; } - public string Identifier { get; set; } - public DeviceType Type { get; set; } - public string CreationDate { get; set; } - public string RevisionDate { get; set; } - } + public string Id { get; set; } + public int Name { get; set; } + public string Identifier { get; set; } + public DeviceType Type { get; set; } + public string CreationDate { get; set; } + public string EncryptedUserKey { get; set; } + public string EncryptedPublicKey { get; set; } + public string EncryptedPrivateKey { get; set; } } - diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index d783a7174..dc2a445a5 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -419,10 +419,14 @@ namespace Bit.Core.Services HttpMethod.Post, "/devices/exist-by-types", deviceTypes, true, true); } - public Task PutUpdateTrustedDeviceKeys(UpdateTrustedDeviceKeysRequest request) + public Task GetDeviceByIdentifierAsync(string deviceIdentifier) { - return SendAsync( - HttpMethod.Put, $"/devices/${request.DeviceIdentifier}/keys", request, true, true); + return SendAsync(HttpMethod.Get, $"/devices/identifier/{deviceIdentifier}", null, true, true); + } + + public Task UpdateTrustedDeviceKeysAsync(string deviceIdentifier, TrustedDeviceKeysRequest trustedDeviceKeysRequest) + { + return SendAsync(HttpMethod.Put, $"/devices/{deviceIdentifier}/keys", trustedDeviceKeysRequest, true, true); } #endregion diff --git a/src/Core/Services/DeviceTrustCryptoService.cs b/src/Core/Services/DeviceTrustCryptoService.cs new file mode 100644 index 000000000..fac44276b --- /dev/null +++ b/src/Core/Services/DeviceTrustCryptoService.cs @@ -0,0 +1,81 @@ + +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; + +namespace Bit.Core.Services +{ + public class DeviceTrustCryptoService : IDeviceTrustCryptoService + { + private readonly IApiService _apiService; + private readonly IAppIdService _appIdService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly ICryptoService _cryptoService; + private readonly IStateService _stateService; + + private const int DEVICE_KEY_SIZE = 64; + + public DeviceTrustCryptoService( + IApiService apiService, + IAppIdService appIdService, + ICryptoFunctionService cryptoFunctionService, + ICryptoService cryptoService, + IStateService stateService) + { + _apiService = apiService; + _appIdService = appIdService; + _cryptoFunctionService = cryptoFunctionService; + _cryptoService = cryptoService; + _stateService = stateService; + } + + public async Task GetDeviceKeyAsync() + { + return await _stateService.GetDeviceKeyAsync(); + } + + private async Task SetDeviceKeyAsync(SymmetricCryptoKey deviceKey) + { + await _stateService.SetDeviceKeyAsync(deviceKey); + } + + public async Task TrustDeviceAsync() + { + // Attempt to get user key + var userKey = await _cryptoService.GetEncKeyAsync(); + if (userKey == null) + { + return null; + } + // Generate deviceKey + var deviceKey = await MakeDeviceKeyAsync(); + + // Generate asymmetric RSA key pair: devicePrivateKey, devicePublicKey + var (devicePublicKey, devicePrivateKey) = await _cryptoFunctionService.RsaGenerateKeyPairAsync(2048); + + // Send encrypted keys to server + var deviceIdentifier = await _appIdService.GetAppIdAsync(); + var deviceRequest = new TrustedDeviceKeysRequest + { + EncryptedUserKey = (await _cryptoService.RsaEncryptAsync(userKey.EncKey, devicePublicKey)).EncryptedString, + EncryptedPublicKey = (await _cryptoService.EncryptAsync(devicePublicKey, userKey)).EncryptedString, + EncryptedPrivateKey = (await _cryptoService.EncryptAsync(devicePrivateKey, deviceKey)).EncryptedString, + }; + + var deviceResponse = await _apiService.UpdateTrustedDeviceKeysAsync(deviceIdentifier, deviceRequest); + + // Store device key if successful + await SetDeviceKeyAsync(deviceKey); + return deviceResponse; + } + + private async Task MakeDeviceKeyAsync() + { + // Create 512-bit device key + var randomBytes = await _cryptoFunctionService.RandomBytesAsync(DEVICE_KEY_SIZE); + return new SymmetricCryptoKey(randomBytes); + } + } +} diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index 82588503b..d446d585c 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -482,6 +482,17 @@ namespace Bit.Core.Services await SetValueAsync(Constants.EncPrivateKeyKey(reconciledOptions.UserId), value, reconciledOptions); } + public async Task GetDeviceKeyAsync(string userId = null) + { + var deviceKeyB64 = await _storageMediatorService.GetAsync(Constants.DeviceKeyKey(userId), true); + return new SymmetricCryptoKey(Convert.FromBase64String(deviceKeyB64)); + } + + public async Task SetDeviceKeyAsync(SymmetricCryptoKey value, string userId = null) + { + await _storageMediatorService.SaveAsync(Constants.DeviceKeyKey(userId), value.KeyB64, true); + } + public async Task> GetAutofillBlacklistedUrisAsync(string userId = null) { var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },