mirror of
https://github.com/bitwarden/mobile
synced 2025-12-10 21:33:36 +00:00
Compare commits
7 Commits
feature/ma
...
v2023.9.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dae9f5192 | ||
|
|
1e869bf237 | ||
|
|
bb5125a124 | ||
|
|
ee1cbae589 | ||
|
|
4cc5e13b61 | ||
|
|
99e64b4e3f | ||
|
|
0f634ff3f0 |
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.8.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.9.2" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Bit.App.Utilities;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -72,11 +73,12 @@ namespace Bit.App.Pages
|
|||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||||
|
|
||||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
AccountSwitchingOverlayViewModel =
|
||||||
{
|
new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||||
AllowAddAccountRow = true,
|
{
|
||||||
AllowActiveAccountSelection = true
|
AllowAddAccountRow = true,
|
||||||
};
|
AllowActiveAccountSelection = true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MasterPassword
|
public string MasterPassword
|
||||||
@@ -155,8 +157,12 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public Command SubmitCommand { get; }
|
public Command SubmitCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
|
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword
|
||||||
|
? AppResources.PasswordIsVisibleTapToHide
|
||||||
|
: AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
|
|
||||||
public Action UnlockedAction { get; set; }
|
public Action UnlockedAction { get; set; }
|
||||||
public event Action<int?> FocusSecretEntry
|
public event Action<int?> FocusSecretEntry
|
||||||
{
|
{
|
||||||
@@ -178,8 +184,9 @@ namespace Bit.App.Pages
|
|||||||
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
|
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
|
||||||
?? await _stateService.GetPinProtectedKeyAsync();
|
?? await _stateService.GetPinProtectedKeyAsync();
|
||||||
PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
|
PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
|
||||||
_pinStatus == PinLockType.Persistent;
|
_pinStatus == PinLockType.Persistent;
|
||||||
BiometricEnabled = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _biometricService.CanUseBiometricsUnlockAsync();
|
|
||||||
|
BiometricEnabled = await IsBiometricsEnabledAsync();
|
||||||
|
|
||||||
// Users without MP and without biometric or pin has no MP to unlock with
|
// Users without MP and without biometric or pin has no MP to unlock with
|
||||||
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
|
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
|
||||||
@@ -214,7 +221,9 @@ namespace Bit.App.Pages
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault;
|
PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault;
|
||||||
LockedVerifyText = _hasMasterPassword ? AppResources.VaultLockedMasterPassword : AppResources.VaultLockedIdentity;
|
LockedVerifyText = _hasMasterPassword
|
||||||
|
? AppResources.VaultLockedMasterPassword
|
||||||
|
: AppResources.VaultLockedIdentity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BiometricEnabled)
|
if (BiometricEnabled)
|
||||||
@@ -233,11 +242,32 @@ namespace Bit.App.Pages
|
|||||||
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
|
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
|
||||||
AppResources.UseFingerprintToUnlock;
|
AppResources.UseFingerprintToUnlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SubmitAsync()
|
public async Task SubmitAsync()
|
||||||
|
{
|
||||||
|
ShowPassword = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||||
|
if (PinEnabled)
|
||||||
|
{
|
||||||
|
await UnlockWithPinAsync(kdfConfig);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UnlockWithMasterPasswordAsync(kdfConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (LegacyUserException)
|
||||||
|
{
|
||||||
|
await HandleLegacyUserAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnlockWithPinAsync(KdfConfig kdfConfig)
|
||||||
{
|
{
|
||||||
if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
|
if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
|
||||||
{
|
{
|
||||||
@@ -246,6 +276,84 @@ namespace Bit.App.Pages
|
|||||||
AppResources.Ok);
|
AppResources.Ok);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var failed = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EncString userKeyPin;
|
||||||
|
EncString oldPinProtected;
|
||||||
|
switch (_pinStatus)
|
||||||
|
{
|
||||||
|
case PinLockType.Persistent:
|
||||||
|
{
|
||||||
|
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
||||||
|
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
||||||
|
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PinLockType.Transient:
|
||||||
|
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
||||||
|
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
||||||
|
break;
|
||||||
|
case PinLockType.Disabled:
|
||||||
|
default:
|
||||||
|
throw new Exception("Pin is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
UserKey userKey;
|
||||||
|
if (oldPinProtected != null)
|
||||||
|
{
|
||||||
|
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
||||||
|
_pinStatus == PinLockType.Transient,
|
||||||
|
Pin,
|
||||||
|
_email,
|
||||||
|
kdfConfig,
|
||||||
|
oldPinProtected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
||||||
|
Pin,
|
||||||
|
_email,
|
||||||
|
kdfConfig,
|
||||||
|
userKeyPin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||||
|
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
||||||
|
failed = decryptedPin != Pin;
|
||||||
|
if (!failed)
|
||||||
|
{
|
||||||
|
Pin = string.Empty;
|
||||||
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
|
await SetUserKeyAndContinueAsync(userKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (LegacyUserException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
if (failed)
|
||||||
|
{
|
||||||
|
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||||
|
if (invalidUnlockAttempts >= 5)
|
||||||
|
{
|
||||||
|
_messagingService.Send("logout");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN,
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnlockWithMasterPasswordAsync(KdfConfig kdfConfig)
|
||||||
|
{
|
||||||
if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
|
if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
|
||||||
{
|
{
|
||||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||||
@@ -254,142 +362,78 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowPassword = false;
|
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
|
||||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
if (await _cryptoService.IsLegacyUserAsync(masterKey))
|
||||||
|
|
||||||
if (PinEnabled)
|
|
||||||
{
|
{
|
||||||
var failed = true;
|
throw new LegacyUserException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
|
||||||
|
var passwordValid = false;
|
||||||
|
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
|
||||||
|
|
||||||
|
if (storedKeyHash != null)
|
||||||
|
{
|
||||||
|
// Offline unlock possible
|
||||||
|
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Online unlock required
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||||
|
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
|
||||||
|
HashPurpose.ServerAuthorization);
|
||||||
|
var request = new PasswordVerificationRequest();
|
||||||
|
request.MasterPasswordHash = keyHash;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
EncString userKeyPin = null;
|
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||||
EncString oldPinProtected = null;
|
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
|
||||||
if (_pinStatus == PinLockType.Persistent)
|
passwordValid = true;
|
||||||
{
|
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
|
||||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
HashPurpose.LocalAuthorization);
|
||||||
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
|
||||||
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
|
||||||
}
|
|
||||||
else if (_pinStatus == PinLockType.Transient)
|
|
||||||
{
|
|
||||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
|
||||||
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserKey userKey;
|
|
||||||
if (oldPinProtected != null)
|
|
||||||
{
|
|
||||||
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
|
||||||
_pinStatus == PinLockType.Transient,
|
|
||||||
Pin,
|
|
||||||
_email,
|
|
||||||
kdfConfig,
|
|
||||||
oldPinProtected
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
|
||||||
Pin,
|
|
||||||
_email,
|
|
||||||
kdfConfig,
|
|
||||||
userKeyPin
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
|
||||||
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
|
||||||
failed = decryptedPin != Pin;
|
|
||||||
if (!failed)
|
|
||||||
{
|
|
||||||
Pin = string.Empty;
|
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
||||||
await SetUserKeyAndContinueAsync(userKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
failed = true;
|
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||||
}
|
}
|
||||||
if (failed)
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordValid)
|
||||||
|
{
|
||||||
|
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
|
||||||
{
|
{
|
||||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
// Save the ForcePasswordResetReason to force a password reset after unlock
|
||||||
if (invalidUnlockAttempts >= 5)
|
await _stateService.SetForcePasswordResetReasonAsync(
|
||||||
{
|
ForcePasswordResetReason.WeakMasterPasswordOnLogin);
|
||||||
_messagingService.Send("logout");
|
}
|
||||||
return;
|
|
||||||
}
|
MasterPassword = string.Empty;
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN,
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
AppResources.AnErrorHasOccurred);
|
|
||||||
|
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||||
|
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||||
|
await SetUserKeyAndContinueAsync(userKey);
|
||||||
|
|
||||||
|
// Re-enable biometrics
|
||||||
|
if (BiometricEnabled & !BiometricIntegrityValid)
|
||||||
|
{
|
||||||
|
await _biometricService.SetupBiometricAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
|
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||||
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
|
if (invalidUnlockAttempts >= 5)
|
||||||
var passwordValid = false;
|
|
||||||
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
|
|
||||||
|
|
||||||
if (storedKeyHash != null)
|
|
||||||
{
|
{
|
||||||
// Offline unlock possible
|
_messagingService.Send("logout");
|
||||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
|
return;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Online unlock required
|
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
|
||||||
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.ServerAuthorization);
|
|
||||||
var request = new PasswordVerificationRequest();
|
|
||||||
request.MasterPasswordHash = keyHash;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
|
|
||||||
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
|
|
||||||
passwordValid = true;
|
|
||||||
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.LocalAuthorization);
|
|
||||||
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
|
||||||
}
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
|
||||||
}
|
|
||||||
if (passwordValid)
|
|
||||||
{
|
|
||||||
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
|
|
||||||
{
|
|
||||||
// Save the ForcePasswordResetReason to force a password reset after unlock
|
|
||||||
await _stateService.SetForcePasswordResetReasonAsync(
|
|
||||||
ForcePasswordResetReason.WeakMasterPasswordOnLogin);
|
|
||||||
}
|
|
||||||
|
|
||||||
MasterPassword = string.Empty;
|
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
||||||
|
|
||||||
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
|
||||||
await _cryptoService.SetMasterKeyAsync(masterKey);
|
|
||||||
await SetUserKeyAndContinueAsync(userKey);
|
|
||||||
|
|
||||||
// Re-enable biometrics
|
|
||||||
if (BiometricEnabled & !BiometricIntegrityValid)
|
|
||||||
{
|
|
||||||
await _biometricService.SetupBiometricAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
|
||||||
if (invalidUnlockAttempts >= 5)
|
|
||||||
{
|
|
||||||
_messagingService.Send("logout");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword,
|
|
||||||
AppResources.AnErrorHasOccurred);
|
|
||||||
}
|
}
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword,
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,25 +496,34 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
ShowPassword = !ShowPassword;
|
ShowPassword = !ShowPassword;
|
||||||
var secret = PinEnabled ? Pin : MasterPassword;
|
var secret = PinEnabled ? Pin : MasterPassword;
|
||||||
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
|
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length,
|
||||||
|
nameof(FocusSecretEntry));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PromptBiometricAsync()
|
public async Task PromptBiometricAsync()
|
||||||
{
|
{
|
||||||
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
try
|
||||||
BiometricButtonVisible = BiometricIntegrityValid;
|
|
||||||
if (!BiometricEnabled || !BiometricIntegrityValid)
|
|
||||||
{
|
{
|
||||||
return;
|
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||||
|
BiometricButtonVisible = BiometricIntegrityValid;
|
||||||
|
if (!BiometricEnabled || !BiometricIntegrityValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||||
|
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||||
|
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
||||||
|
await _stateService.SetBiometricLockedAsync(!success);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
||||||
|
await SetUserKeyAndContinueAsync(userKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
catch (LegacyUserException)
|
||||||
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
|
||||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
|
||||||
await _stateService.SetBiometricLockedAsync(!success);
|
|
||||||
if (success)
|
|
||||||
{
|
{
|
||||||
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
await HandleLegacyUserAsync();
|
||||||
await SetUserKeyAndContinueAsync(userKey);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,5 +546,29 @@ namespace Bit.App.Pages
|
|||||||
_messagingService.Send("unlocked");
|
_messagingService.Send("unlocked");
|
||||||
UnlockedAction?.Invoke();
|
UnlockedAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsBiometricsEnabledAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _vaultTimeoutService.IsBiometricLockSetAsync() &&
|
||||||
|
await _biometricService.CanUseBiometricsUnlockAsync();
|
||||||
|
}
|
||||||
|
catch (LegacyUserException)
|
||||||
|
{
|
||||||
|
await HandleLegacyUserAsync();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleLegacyUserAsync()
|
||||||
|
{
|
||||||
|
// Legacy users must migrate on web vault.
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong,
|
||||||
|
AppResources.AnErrorHasOccurred,
|
||||||
|
AppResources.Ok);
|
||||||
|
await _vaultTimeoutService.LogOutAsync();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,6 +248,14 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
|
||||||
|
if (response.RequiresEncryptionKeyMigration)
|
||||||
|
{
|
||||||
|
// Legacy users must migrate on web vault.
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong, AppResources.AnErrorHasOccurred,
|
||||||
|
AppResources.Ok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.TwoFactor)
|
if (response.TwoFactor)
|
||||||
{
|
{
|
||||||
StartTwoFactorAction?.Invoke();
|
StartTwoFactorAction?.Invoke();
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||||
_cipherDomain = await _cipherService.SaveAttachmentRawWithServerAsync(
|
_cipherDomain = await _cipherService.SaveAttachmentRawWithServerAsync(
|
||||||
_cipherDomain, FileName, FileData);
|
_cipherDomain, Cipher, FileName, FileData);
|
||||||
Cipher = await _cipherDomain.DecryptAsync();
|
Cipher = await _cipherDomain.DecryptAsync();
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
_platformUtilsService.ShowToast("success", null, AppResources.AttachementAdded);
|
_platformUtilsService.ShowToast("success", null, AppResources.AttachementAdded);
|
||||||
|
|||||||
13783
src/App/Resources/AppResources.Designer.cs
generated
13783
src/App/Resources/AppResources.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -954,6 +954,9 @@ Scanning will happen automatically.</value>
|
|||||||
<data name="UpdateKey" xml:space="preserve">
|
<data name="UpdateKey" xml:space="preserve">
|
||||||
<value>You cannot use this feature until you update your encryption key.</value>
|
<value>You cannot use this feature until you update your encryption key.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||||
|
<value>Encryption key migration required. Please login through the web vault to update your encryption key.</value>
|
||||||
|
</data>
|
||||||
<data name="LearnMore" xml:space="preserve">
|
<data name="LearnMore" xml:space="preserve">
|
||||||
<value>Learn more</value>
|
<value>Learn more</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<Cipher> GetAsync(string id);
|
Task<Cipher> GetAsync(string id);
|
||||||
Task<CipherView> GetLastUsedForUrlAsync(string url);
|
Task<CipherView> GetLastUsedForUrlAsync(string url);
|
||||||
Task ReplaceAsync(Dictionary<string, CipherData> ciphers);
|
Task ReplaceAsync(Dictionary<string, CipherData> ciphers);
|
||||||
Task<Cipher> SaveAttachmentRawWithServerAsync(Cipher cipher, string filename, byte[] data);
|
Task<Cipher> SaveAttachmentRawWithServerAsync(Cipher cipher, CipherView cipherView, string filename, byte[] data);
|
||||||
Task SaveCollectionsWithServerAsync(Cipher cipher);
|
Task SaveCollectionsWithServerAsync(Cipher cipher);
|
||||||
Task SaveWithServerAsync(Cipher cipher);
|
Task SaveWithServerAsync(Cipher cipher);
|
||||||
Task<ShareWithServerError> ShareWithServerAsync(CipherView cipher, string organizationId, HashSet<string> collectionIds);
|
Task<ShareWithServerError> ShareWithServerAsync(CipherView cipher, string organizationId, HashSet<string> collectionIds);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Bit.Core.Abstractions
|
|||||||
Task RefreshKeysAsync();
|
Task RefreshKeysAsync();
|
||||||
Task SetUserKeyAsync(UserKey userKey, string userId = null);
|
Task SetUserKeyAsync(UserKey userKey, string userId = null);
|
||||||
Task<UserKey> GetUserKeyAsync(string userId = null);
|
Task<UserKey> GetUserKeyAsync(string userId = null);
|
||||||
|
Task<bool> IsLegacyUserAsync(MasterKey masterKey = null, string userId = null);
|
||||||
Task<UserKey> GetUserKeyWithLegacySupportAsync(string userId = null);
|
Task<UserKey> GetUserKeyWithLegacySupportAsync(string userId = null);
|
||||||
Task<bool> HasUserKeyAsync(string userId = null);
|
Task<bool> HasUserKeyAsync(string userId = null);
|
||||||
Task<bool> HasEncryptedUserKeyAsync(string userId = null);
|
Task<bool> HasEncryptedUserKeyAsync(string userId = null);
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ namespace Bit.Core
|
|||||||
public const int Argon2MemoryInMB = 64;
|
public const int Argon2MemoryInMB = 64;
|
||||||
public const int Argon2Parallelism = 4;
|
public const int Argon2Parallelism = 4;
|
||||||
public const int MasterPasswordMinimumChars = 12;
|
public const int MasterPasswordMinimumChars = 12;
|
||||||
|
public const int CipherKeyRandomBytesLength = 64;
|
||||||
|
public const string CipherKeyEncryptionMinServerVersion = "2023.9.1";
|
||||||
public const string DefaultFido2KeyType = "public-key";
|
public const string DefaultFido2KeyType = "public-key";
|
||||||
public const string DefaultFido2KeyAlgorithm = "ECDSA";
|
public const string DefaultFido2KeyAlgorithm = "ECDSA";
|
||||||
public const string DefaultFido2KeyCurve = "P-256";
|
public const string DefaultFido2KeyCurve = "P-256";
|
||||||
|
|||||||
11
src/Core/Exceptions/LegacyUserException.cs
Normal file
11
src/Core/Exceptions/LegacyUserException.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
namespace Bit.Core.Exceptions
|
||||||
|
{
|
||||||
|
public class LegacyUserException : Exception
|
||||||
|
{
|
||||||
|
public LegacyUserException()
|
||||||
|
: base("Legacy users must migrate on web vault.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ namespace Bit.Core.Models.Data
|
|||||||
Notes = response.Notes;
|
Notes = response.Notes;
|
||||||
CollectionIds = collectionIds?.ToList() ?? response.CollectionIds;
|
CollectionIds = collectionIds?.ToList() ?? response.CollectionIds;
|
||||||
Reprompt = response.Reprompt;
|
Reprompt = response.Reprompt;
|
||||||
|
Key = response.Key;
|
||||||
|
|
||||||
try // Added to address Issue (https://github.com/bitwarden/mobile/issues/1006)
|
try // Added to address Issue (https://github.com/bitwarden/mobile/issues/1006)
|
||||||
{
|
{
|
||||||
@@ -92,5 +93,6 @@ namespace Bit.Core.Models.Data
|
|||||||
public List<PasswordHistoryData> PasswordHistory { get; set; }
|
public List<PasswordHistoryData> PasswordHistory { get; set; }
|
||||||
public List<string> CollectionIds { get; set; }
|
public List<string> CollectionIds { get; set; }
|
||||||
public Enums.CipherRepromptType Reprompt { get; set; }
|
public Enums.CipherRepromptType Reprompt { get; set; }
|
||||||
|
public string Key { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Domain
|
namespace Bit.Core.Models.Domain
|
||||||
@@ -11,11 +13,11 @@ namespace Bit.Core.Models.Domain
|
|||||||
{
|
{
|
||||||
private HashSet<string> _map = new HashSet<string>
|
private HashSet<string> _map = new HashSet<string>
|
||||||
{
|
{
|
||||||
"Id",
|
nameof(Id),
|
||||||
"Url",
|
nameof(Url),
|
||||||
"SizeName",
|
nameof(SizeName),
|
||||||
"FileName",
|
nameof(FileName),
|
||||||
"Key"
|
nameof(Key)
|
||||||
};
|
};
|
||||||
|
|
||||||
public Attachment() { }
|
public Attachment() { }
|
||||||
@@ -23,7 +25,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
public Attachment(AttachmentData obj, bool alreadyEncrypted = false)
|
public Attachment(AttachmentData obj, bool alreadyEncrypted = false)
|
||||||
{
|
{
|
||||||
Size = obj.Size;
|
Size = obj.Size;
|
||||||
BuildDomainModel(this, obj, _map, alreadyEncrypted, new HashSet<string> { "Id", "Url", "SizeName" });
|
BuildDomainModel(this, obj, _map, alreadyEncrypted, new HashSet<string> { nameof(Id), nameof(Url), nameof(SizeName) });
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
@@ -33,25 +35,26 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString Key { get; set; }
|
public EncString Key { get; set; }
|
||||||
public EncString FileName { get; set; }
|
public EncString FileName { get; set; }
|
||||||
|
|
||||||
public async Task<AttachmentView> DecryptAsync(string orgId)
|
public async Task<AttachmentView> DecryptAsync(string orgId, SymmetricCryptoKey key = null)
|
||||||
{
|
{
|
||||||
var view = await DecryptObjAsync(new AttachmentView(this), this, new HashSet<string>
|
var view = await DecryptObjAsync(new AttachmentView(this), this, new HashSet<string>
|
||||||
{
|
{
|
||||||
"FileName"
|
nameof(FileName)
|
||||||
}, orgId);
|
}, orgId, key);
|
||||||
|
|
||||||
if (Key != null)
|
if (Key != null)
|
||||||
{
|
{
|
||||||
var cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var orgKey = await cryptoService.GetOrgKeyAsync(orgId);
|
var cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||||
var decValue = await cryptoService.DecryptToBytesAsync(Key, orgKey);
|
|
||||||
|
var decryptKey = key ?? await cryptoService.GetOrgKeyAsync(orgId);
|
||||||
|
var decValue = await cryptoService.DecryptToBytesAsync(Key, decryptKey);
|
||||||
view.Key = new SymmetricCryptoKey(decValue);
|
view.Key = new SymmetricCryptoKey(decValue);
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// TODO: error?
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return view;
|
return view;
|
||||||
@@ -61,7 +64,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
{
|
{
|
||||||
var a = new AttachmentData();
|
var a = new AttachmentData();
|
||||||
a.Size = Size;
|
a.Size = Size;
|
||||||
BuildDataModel(this, a, _map, new HashSet<string> { "Id", "Url", "SizeName" });
|
BuildDataModel(this, a, _map, new HashSet<string> { nameof(Id), nameof(Url), nameof(SizeName) });
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
[Obsolete("Use AccountDecryptionOptions to determine if the user does not have a MP")]
|
[Obsolete("Use AccountDecryptionOptions to determine if the user does not have a MP")]
|
||||||
public bool ResetMasterPassword { get; set; }
|
public bool ResetMasterPassword { get; set; }
|
||||||
public bool ForcePasswordReset { get; set; }
|
public bool ForcePasswordReset { get; set; }
|
||||||
|
public bool RequiresEncryptionKeyMigration { get; set; }
|
||||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
|
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString ExpYear { get; set; }
|
public EncString ExpYear { get; set; }
|
||||||
public EncString Code { get; set; }
|
public EncString Code { get; set; }
|
||||||
|
|
||||||
public Task<CardView> DecryptAsync(string orgId)
|
public Task<CardView> 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()
|
public CardData ToCardData()
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Domain
|
namespace Bit.Core.Models.Domain
|
||||||
{
|
{
|
||||||
@@ -16,12 +18,12 @@ namespace Bit.Core.Models.Domain
|
|||||||
{
|
{
|
||||||
BuildDomainModel(this, obj, new HashSet<string>
|
BuildDomainModel(this, obj, new HashSet<string>
|
||||||
{
|
{
|
||||||
"Id",
|
nameof(Id),
|
||||||
"OrganizationId",
|
nameof(OrganizationId),
|
||||||
"FolderId",
|
nameof(FolderId),
|
||||||
"Name",
|
nameof(Name),
|
||||||
"Notes"
|
nameof(Notes)
|
||||||
}, alreadyEncrypted, new HashSet<string> { "Id", "OrganizationId", "FolderId" });
|
}, alreadyEncrypted, new HashSet<string> { nameof(Id), nameof(OrganizationId), nameof(FolderId) });
|
||||||
|
|
||||||
Type = obj.Type;
|
Type = obj.Type;
|
||||||
Favorite = obj.Favorite;
|
Favorite = obj.Favorite;
|
||||||
@@ -34,6 +36,11 @@ namespace Bit.Core.Models.Domain
|
|||||||
LocalData = localData;
|
LocalData = localData;
|
||||||
Reprompt = obj.Reprompt;
|
Reprompt = obj.Reprompt;
|
||||||
|
|
||||||
|
if (obj.Key != null)
|
||||||
|
{
|
||||||
|
Key = new EncString(obj.Key);
|
||||||
|
}
|
||||||
|
|
||||||
switch (Type)
|
switch (Type)
|
||||||
{
|
{
|
||||||
case Enums.CipherType.Login:
|
case Enums.CipherType.Login:
|
||||||
@@ -85,29 +92,43 @@ namespace Bit.Core.Models.Domain
|
|||||||
public List<PasswordHistory> PasswordHistory { get; set; }
|
public List<PasswordHistory> PasswordHistory { get; set; }
|
||||||
public HashSet<string> CollectionIds { get; set; }
|
public HashSet<string> CollectionIds { get; set; }
|
||||||
public CipherRepromptType Reprompt { get; set; }
|
public CipherRepromptType Reprompt { get; set; }
|
||||||
|
public EncString Key { get; set; }
|
||||||
|
|
||||||
public async Task<CipherView> DecryptAsync()
|
public async Task<CipherView> DecryptAsync()
|
||||||
{
|
{
|
||||||
var model = new CipherView(this);
|
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<ICryptoService>();
|
||||||
|
|
||||||
|
var orgKey = await cryptoService.GetOrgKeyAsync(OrganizationId);
|
||||||
|
|
||||||
|
var key = await cryptoService.DecryptToBytesAsync(Key, orgKey);
|
||||||
|
model.Key = new CipherKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
await DecryptObjAsync(model, this, new HashSet<string>
|
await DecryptObjAsync(model, this, new HashSet<string>
|
||||||
{
|
{
|
||||||
"Name",
|
nameof(Name),
|
||||||
"Notes"
|
nameof(Notes)
|
||||||
}, OrganizationId);
|
}, OrganizationId, model.Key);
|
||||||
|
|
||||||
switch (Type)
|
switch (Type)
|
||||||
{
|
{
|
||||||
case Enums.CipherType.Login:
|
case Enums.CipherType.Login:
|
||||||
model.Login = await Login.DecryptAsync(OrganizationId);
|
model.Login = await Login.DecryptAsync(OrganizationId, model.Key);
|
||||||
break;
|
break;
|
||||||
case Enums.CipherType.SecureNote:
|
case Enums.CipherType.SecureNote:
|
||||||
model.SecureNote = await SecureNote.DecryptAsync(OrganizationId);
|
model.SecureNote = await SecureNote.DecryptAsync(OrganizationId, model.Key);
|
||||||
break;
|
break;
|
||||||
case Enums.CipherType.Card:
|
case Enums.CipherType.Card:
|
||||||
model.Card = await Card.DecryptAsync(OrganizationId);
|
model.Card = await Card.DecryptAsync(OrganizationId, model.Key);
|
||||||
break;
|
break;
|
||||||
case Enums.CipherType.Identity:
|
case Enums.CipherType.Identity:
|
||||||
model.Identity = await Identity.DecryptAsync(OrganizationId);
|
model.Identity = await Identity.DecryptAsync(OrganizationId, model.Key);
|
||||||
break;
|
break;
|
||||||
case Enums.CipherType.Fido2Key:
|
case Enums.CipherType.Fido2Key:
|
||||||
model.Fido2Key = await Fido2Key.DecryptAsync(OrganizationId);
|
model.Fido2Key = await Fido2Key.DecryptAsync(OrganizationId);
|
||||||
@@ -122,7 +143,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
async Task decryptAndAddAttachmentAsync(Attachment attachment)
|
async Task decryptAndAddAttachmentAsync(Attachment attachment)
|
||||||
{
|
{
|
||||||
var decAttachment = await attachment.DecryptAsync(OrganizationId);
|
var decAttachment = await attachment.DecryptAsync(OrganizationId, model.Key);
|
||||||
model.Attachments.Add(decAttachment);
|
model.Attachments.Add(decAttachment);
|
||||||
}
|
}
|
||||||
foreach (var attachment in Attachments)
|
foreach (var attachment in Attachments)
|
||||||
@@ -137,7 +158,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
async Task decryptAndAddFieldAsync(Field field)
|
async Task decryptAndAddFieldAsync(Field field)
|
||||||
{
|
{
|
||||||
var decField = await field.DecryptAsync(OrganizationId);
|
var decField = await field.DecryptAsync(OrganizationId, model.Key);
|
||||||
model.Fields.Add(decField);
|
model.Fields.Add(decField);
|
||||||
}
|
}
|
||||||
foreach (var field in Fields)
|
foreach (var field in Fields)
|
||||||
@@ -152,7 +173,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
async Task decryptAndAddHistoryAsync(PasswordHistory ph)
|
async Task decryptAndAddHistoryAsync(PasswordHistory ph)
|
||||||
{
|
{
|
||||||
var decPh = await ph.DecryptAsync(OrganizationId);
|
var decPh = await ph.DecryptAsync(OrganizationId, model.Key);
|
||||||
model.PasswordHistory.Add(decPh);
|
model.PasswordHistory.Add(decPh);
|
||||||
}
|
}
|
||||||
foreach (var ph in PasswordHistory)
|
foreach (var ph in PasswordHistory)
|
||||||
@@ -181,11 +202,12 @@ namespace Bit.Core.Models.Domain
|
|||||||
CollectionIds = CollectionIds.ToList(),
|
CollectionIds = CollectionIds.ToList(),
|
||||||
DeletedDate = DeletedDate,
|
DeletedDate = DeletedDate,
|
||||||
Reprompt = Reprompt,
|
Reprompt = Reprompt,
|
||||||
|
Key = Key?.EncryptedString
|
||||||
};
|
};
|
||||||
BuildDataModel(this, c, new HashSet<string>
|
BuildDataModel(this, c, new HashSet<string>
|
||||||
{
|
{
|
||||||
"Name",
|
nameof(Name),
|
||||||
"Notes"
|
nameof(Notes)
|
||||||
});
|
});
|
||||||
switch (c.Type)
|
switch (c.Type)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString UserName { get; set; }
|
public EncString UserName { get; set; }
|
||||||
public EncString Counter { get; set; }
|
public EncString Counter { get; set; }
|
||||||
|
|
||||||
public async Task<Fido2KeyView> DecryptAsync(string orgId)
|
public async Task<Fido2KeyView> DecryptAsync(string orgId, SymmetricCryptoKey key = null)
|
||||||
{
|
{
|
||||||
return await DecryptObjAsync(new Fido2KeyView(), this, EncryptableProperties, orgId);
|
return await DecryptObjAsync(new Fido2KeyView(), this, EncryptableProperties, orgId, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Fido2KeyData ToFido2KeyData()
|
public Fido2KeyData ToFido2KeyData()
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ namespace Bit.Core.Models.Domain
|
|||||||
public FieldType Type { get; set; }
|
public FieldType Type { get; set; }
|
||||||
public LinkedIdType? LinkedId { get; set; }
|
public LinkedIdType? LinkedId { get; set; }
|
||||||
|
|
||||||
public Task<FieldView> DecryptAsync(string orgId)
|
public Task<FieldView> 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()
|
public FieldData ToFieldData()
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString PassportNumber { get; set; }
|
public EncString PassportNumber { get; set; }
|
||||||
public EncString LicenseNumber { get; set; }
|
public EncString LicenseNumber { get; set; }
|
||||||
|
|
||||||
public Task<IdentityView> DecryptAsync(string orgId)
|
public Task<IdentityView> 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()
|
public IdentityData ToIdentityData()
|
||||||
|
|||||||
@@ -31,25 +31,25 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString Totp { get; set; }
|
public EncString Totp { get; set; }
|
||||||
public Fido2Key Fido2Key { get; set; }
|
public Fido2Key Fido2Key { get; set; }
|
||||||
|
|
||||||
public async Task<LoginView> DecryptAsync(string orgId)
|
public async Task<LoginView> DecryptAsync(string orgId, SymmetricCryptoKey key = null)
|
||||||
{
|
{
|
||||||
var view = await DecryptObjAsync(new LoginView(this), this, new HashSet<string>
|
var view = await DecryptObjAsync(new LoginView(this), this, new HashSet<string>
|
||||||
{
|
{
|
||||||
"Username",
|
"Username",
|
||||||
"Password",
|
"Password",
|
||||||
"Totp"
|
"Totp"
|
||||||
}, orgId);
|
}, orgId, key);
|
||||||
if (Uris != null)
|
if (Uris != null)
|
||||||
{
|
{
|
||||||
view.Uris = new List<LoginUriView>();
|
view.Uris = new List<LoginUriView>();
|
||||||
foreach (var uri in Uris)
|
foreach (var uri in Uris)
|
||||||
{
|
{
|
||||||
view.Uris.Add(await uri.DecryptAsync(orgId));
|
view.Uris.Add(await uri.DecryptAsync(orgId, key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Fido2Key != null)
|
if (Fido2Key != null)
|
||||||
{
|
{
|
||||||
view.Fido2Key = await Fido2Key.DecryptAsync(orgId);
|
view.Fido2Key = await Fido2Key.DecryptAsync(orgId, key);
|
||||||
}
|
}
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString Uri { get; set; }
|
public EncString Uri { get; set; }
|
||||||
public UriMatchType? Match { get; set; }
|
public UriMatchType? Match { get; set; }
|
||||||
|
|
||||||
public Task<LoginUriView> DecryptAsync(string orgId)
|
public Task<LoginUriView> 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()
|
public LoginUriData ToLoginUriData()
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString Password { get; set; }
|
public EncString Password { get; set; }
|
||||||
public DateTime LastUsedDate { get; set; }
|
public DateTime LastUsedDate { get; set; }
|
||||||
|
|
||||||
public Task<PasswordHistoryView> DecryptAsync(string orgId)
|
public Task<PasswordHistoryView> 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()
|
public PasswordHistoryData ToPasswordHistoryData()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
|
|
||||||
public SecureNoteType Type { get; set; }
|
public SecureNoteType Type { get; set; }
|
||||||
|
|
||||||
public Task<SecureNoteView> DecryptAsync(string orgId)
|
public Task<SecureNoteView> DecryptAsync(string orgId, SymmetricCryptoKey key = null)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new SecureNoteView(this));
|
return Task.FromResult(new SecureNoteView(this));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Domain
|
namespace Bit.Core.Models.Domain
|
||||||
@@ -102,4 +101,11 @@ namespace Bit.Core.Models.Domain
|
|||||||
: base(key, encType)
|
: base(key, encType)
|
||||||
{ }
|
{ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class CipherKey : SymmetricCryptoKey
|
||||||
|
{
|
||||||
|
public CipherKey(byte[] key, EncryptionType? encType = null)
|
||||||
|
: base(key, encType)
|
||||||
|
{ }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ namespace Bit.Core.Models.Export
|
|||||||
Name = obj.Name?.EncryptedString;
|
Name = obj.Name?.EncryptedString;
|
||||||
Notes = obj.Notes?.EncryptedString;
|
Notes = obj.Notes?.EncryptedString;
|
||||||
Favorite = obj.Favorite;
|
Favorite = obj.Favorite;
|
||||||
|
Key = obj.Key?.EncryptedString;
|
||||||
|
|
||||||
Fields = obj.Fields?.Select(f => new Field(f)).ToList();
|
Fields = obj.Fields?.Select(f => new Field(f)).ToList();
|
||||||
|
|
||||||
@@ -82,6 +84,8 @@ namespace Bit.Core.Models.Export
|
|||||||
public Card Card { get; set; }
|
public Card Card { get; set; }
|
||||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public Identity Identity { get; set; }
|
public Identity Identity { get; set; }
|
||||||
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
public CipherView ToView(Cipher req, CipherView view = null)
|
public CipherView ToView(Cipher req, CipherView view = null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace Bit.Core.Models.Request
|
|||||||
Favorite = cipher.Favorite;
|
Favorite = cipher.Favorite;
|
||||||
LastKnownRevisionDate = cipher.RevisionDate;
|
LastKnownRevisionDate = cipher.RevisionDate;
|
||||||
Reprompt = cipher.Reprompt;
|
Reprompt = cipher.Reprompt;
|
||||||
|
Key = cipher.Key?.EncryptedString;
|
||||||
|
|
||||||
switch (Type)
|
switch (Type)
|
||||||
{
|
{
|
||||||
@@ -129,5 +130,6 @@ namespace Bit.Core.Models.Request
|
|||||||
public Dictionary<string, AttachmentRequest> Attachments2 { get; set; }
|
public Dictionary<string, AttachmentRequest> Attachments2 { get; set; }
|
||||||
public DateTime LastKnownRevisionDate { get; set; }
|
public DateTime LastKnownRevisionDate { get; set; }
|
||||||
public CipherRepromptType Reprompt { get; set; }
|
public CipherRepromptType Reprompt { get; set; }
|
||||||
|
public string Key { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ namespace Bit.Core.Models.Response
|
|||||||
public List<string> CollectionIds { get; set; }
|
public List<string> CollectionIds { get; set; }
|
||||||
public DateTime? DeletedDate { get; set; }
|
public DateTime? DeletedDate { get; set; }
|
||||||
public CipherRepromptType Reprompt { get; set; }
|
public CipherRepromptType Reprompt { get; set; }
|
||||||
|
public string Key { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ namespace Bit.Core.Models.View
|
|||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
public DateTime? DeletedDate { get; set; }
|
public DateTime? DeletedDate { get; set; }
|
||||||
public CipherRepromptType Reprompt { get; set; }
|
public CipherRepromptType Reprompt { get; set; }
|
||||||
|
public CipherKey Key { get; set; }
|
||||||
|
|
||||||
public ItemView Item
|
public ItemView Item
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace Bit.Core.Services
|
|||||||
var device = (int)_platformUtilsService.GetDevice();
|
var device = (int)_platformUtilsService.GetDevice();
|
||||||
_httpClient.DefaultRequestHeaders.Add("Device-Type", device.ToString());
|
_httpClient.DefaultRequestHeaders.Add("Device-Type", device.ToString());
|
||||||
_httpClient.DefaultRequestHeaders.Add("Bitwarden-Client-Name", _platformUtilsService.GetClientType().GetString());
|
_httpClient.DefaultRequestHeaders.Add("Bitwarden-Client-Name", _platformUtilsService.GetClientType().GetString());
|
||||||
_httpClient.DefaultRequestHeaders.Add("Bitwarden-Client-Version", _platformUtilsService.GetApplicationVersion());
|
_httpClient.DefaultRequestHeaders.Add("Bitwarden-Client-Version", VersionHelpers.RemoveSuffix(_platformUtilsService.GetApplicationVersion()));
|
||||||
if (!string.IsNullOrWhiteSpace(customUserAgent))
|
if (!string.IsNullOrWhiteSpace(customUserAgent))
|
||||||
{
|
{
|
||||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(customUserAgent);
|
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(customUserAgent);
|
||||||
|
|||||||
@@ -477,6 +477,17 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tokenResponse = response.TokenResponse;
|
var tokenResponse = response.TokenResponse;
|
||||||
|
if (localHashedPassword != null && tokenResponse.Key == null)
|
||||||
|
{
|
||||||
|
// Only check for legacy if there is no key on token
|
||||||
|
if (await _cryptoService.IsLegacyUserAsync(masterKey))
|
||||||
|
{
|
||||||
|
// Legacy users must migrate on web vault;
|
||||||
|
result.RequiresEncryptionKeyMigration = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
|
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
|
||||||
result.ForcePasswordReset = tokenResponse.ForcePasswordReset;
|
result.ForcePasswordReset = tokenResponse.ForcePasswordReset;
|
||||||
_masterPasswordPolicy = tokenResponse.MasterPasswordPolicy;
|
_masterPasswordPolicy = tokenResponse.MasterPasswordPolicy;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System;
|
//#define ENABLE_NEW_CIPHER_KEY_ENCRYPTION_ON_CREATION
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -30,6 +32,7 @@ namespace Bit.Core.Services
|
|||||||
private readonly IStorageService _storageService;
|
private readonly IStorageService _storageService;
|
||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
private readonly Func<ISearchService> _searchService;
|
private readonly Func<ISearchService> _searchService;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
private readonly string _clearCipherCacheKey;
|
private readonly string _clearCipherCacheKey;
|
||||||
private readonly string[] _allClearCipherCacheKeys;
|
private readonly string[] _allClearCipherCacheKeys;
|
||||||
private Dictionary<string, HashSet<string>> _domainMatchBlacklist = new Dictionary<string, HashSet<string>>
|
private Dictionary<string, HashSet<string>> _domainMatchBlacklist = new Dictionary<string, HashSet<string>>
|
||||||
@@ -48,6 +51,7 @@ namespace Bit.Core.Services
|
|||||||
IStorageService storageService,
|
IStorageService storageService,
|
||||||
II18nService i18nService,
|
II18nService i18nService,
|
||||||
Func<ISearchService> searchService,
|
Func<ISearchService> searchService,
|
||||||
|
IConfigService configService,
|
||||||
string clearCipherCacheKey,
|
string clearCipherCacheKey,
|
||||||
string[] allClearCipherCacheKeys)
|
string[] allClearCipherCacheKeys)
|
||||||
{
|
{
|
||||||
@@ -59,6 +63,7 @@ namespace Bit.Core.Services
|
|||||||
_storageService = storageService;
|
_storageService = storageService;
|
||||||
_i18nService = i18nService;
|
_i18nService = i18nService;
|
||||||
_searchService = searchService;
|
_searchService = searchService;
|
||||||
|
_configService = configService;
|
||||||
_clearCipherCacheKey = clearCipherCacheKey;
|
_clearCipherCacheKey = clearCipherCacheKey;
|
||||||
_allClearCipherCacheKeys = allClearCipherCacheKeys;
|
_allClearCipherCacheKeys = allClearCipherCacheKeys;
|
||||||
}
|
}
|
||||||
@@ -181,6 +186,26 @@ namespace Bit.Core.Services
|
|||||||
Reprompt = model.Reprompt
|
Reprompt = model.Reprompt
|
||||||
};
|
};
|
||||||
|
|
||||||
|
key = await UpdateCipherAndGetCipherKeyAsync(cipher, model, key);
|
||||||
|
|
||||||
|
var tasks = new List<Task>
|
||||||
|
{
|
||||||
|
EncryptObjPropertyAsync(model, cipher, new HashSet<string>
|
||||||
|
{
|
||||||
|
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<SymmetricCryptoKey> UpdateCipherAndGetCipherKeyAsync(Cipher cipher, CipherView cipherView, SymmetricCryptoKey key = null, bool shouldCreateNewCipherKeyIfNeeded = true)
|
||||||
|
{
|
||||||
if (key == null && cipher.OrganizationId != null)
|
if (key == null && cipher.OrganizationId != null)
|
||||||
{
|
{
|
||||||
key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId);
|
key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId);
|
||||||
@@ -190,20 +215,42 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tasks = new List<Task>
|
if (!await ShouldUseCipherKeyEncryptionAsync())
|
||||||
{
|
{
|
||||||
EncryptObjPropertyAsync(model, cipher, new HashSet<string>
|
return key;
|
||||||
{
|
}
|
||||||
"Name",
|
|
||||||
"Notes"
|
if (cipherView.Key != null)
|
||||||
}, key),
|
{
|
||||||
EncryptCipherDataAsync(cipher, model, key),
|
cipher.Key = await _cryptoService.EncryptAsync(cipherView.Key.Key, key);
|
||||||
EncryptFieldsAsync(model.Fields, key, cipher),
|
return cipherView.Key;
|
||||||
EncryptPasswordHistoriesAsync(model.PasswordHistory, key, cipher),
|
}
|
||||||
EncryptAttachmentsAsync(model.Attachments, key, cipher)
|
|
||||||
};
|
if (!shouldCreateNewCipherKeyIfNeeded)
|
||||||
await Task.WhenAll(tasks);
|
{
|
||||||
return cipher;
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENABLE_NEW_CIPHER_KEY_ENCRYPTION_ON_CREATION
|
||||||
|
// 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<ICryptoFunctionService>();
|
||||||
|
var newKey = new SymmetricCryptoKey(await cfs.RandomBytesAsync(Core.Constants.CipherKeyRandomBytesLength));
|
||||||
|
cipher.Key = await _cryptoService.EncryptAsync(newKey.Key, key);
|
||||||
|
|
||||||
|
return newKey;
|
||||||
|
#else
|
||||||
|
return key;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ShouldUseCipherKeyEncryptionAsync()
|
||||||
|
{
|
||||||
|
var config = await _configService.GetAsync();
|
||||||
|
|
||||||
|
return config != null
|
||||||
|
&&
|
||||||
|
VersionHelpers.IsServerVersionGreaterThanOrEqualTo(config.Version, Constants.CipherKeyEncryptionMinServerVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Cipher> GetAsync(string id)
|
public async Task<Cipher> GetAsync(string id)
|
||||||
@@ -511,6 +558,7 @@ namespace Bit.Core.Services
|
|||||||
var request = new CipherRequest(cipher);
|
var request = new CipherRequest(cipher);
|
||||||
response = await _apiService.PutCipherAsync(cipher.Id, request);
|
response = await _apiService.PutCipherAsync(cipher.Id, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userId = await _stateService.GetActiveUserIdAsync();
|
var userId = await _stateService.GetActiveUserIdAsync();
|
||||||
var data = new CipherData(response, userId, cipher.CollectionIds);
|
var data = new CipherData(response, userId, cipher.CollectionIds);
|
||||||
await UpsertAsync(data);
|
await UpsertAsync(data);
|
||||||
@@ -573,9 +621,9 @@ namespace Bit.Core.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Cipher> SaveAttachmentRawWithServerAsync(Cipher cipher, string filename, byte[] data)
|
public async Task<Cipher> SaveAttachmentRawWithServerAsync(Cipher cipher, CipherView cipherView, string filename, byte[] data)
|
||||||
{
|
{
|
||||||
var (attachmentKey, protectedAttachmentKey, encKey) = await MakeAttachmentKeyAsync(cipher.OrganizationId);
|
var (attachmentKey, protectedAttachmentKey, encKey) = await MakeAttachmentKeyAsync(cipher.OrganizationId, cipher, cipherView);
|
||||||
|
|
||||||
var encFileName = await _cryptoService.EncryptAsync(filename, encKey);
|
var encFileName = await _cryptoService.EncryptAsync(filename, encKey);
|
||||||
var encFileData = await _cryptoService.EncryptToBytesAsync(data, attachmentKey);
|
var encFileData = await _cryptoService.EncryptToBytesAsync(data, attachmentKey);
|
||||||
@@ -592,6 +640,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
var uploadDataResponse = await _apiService.PostCipherAttachmentAsync(cipher.Id, request);
|
var uploadDataResponse = await _apiService.PostCipherAttachmentAsync(cipher.Id, request);
|
||||||
response = uploadDataResponse.CipherResponse;
|
response = uploadDataResponse.CipherResponse;
|
||||||
|
|
||||||
await _fileUploadService.UploadCipherAttachmentFileAsync(uploadDataResponse, encFileName, encFileData);
|
await _fileUploadService.UploadCipherAttachmentFileAsync(uploadDataResponse, encFileName, encFileData);
|
||||||
}
|
}
|
||||||
catch (ApiException e) when (e.Error.StatusCode == System.Net.HttpStatusCode.NotFound || e.Error.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed)
|
catch (ApiException e) when (e.Error.StatusCode == System.Net.HttpStatusCode.NotFound || e.Error.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed)
|
||||||
@@ -814,10 +863,18 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
private async Task<Tuple<SymmetricCryptoKey, EncString, SymmetricCryptoKey>> MakeAttachmentKeyAsync(string organizationId)
|
private async Task<Tuple<SymmetricCryptoKey, EncString, SymmetricCryptoKey>> MakeAttachmentKeyAsync(string organizationId, Cipher cipher = null, CipherView cipherView = null)
|
||||||
{
|
{
|
||||||
var encryptionKey = await _cryptoService.GetOrgKeyAsync(organizationId)
|
var orgKey = await _cryptoService.GetOrgKeyAsync(organizationId);
|
||||||
?? (SymmetricCryptoKey)await _cryptoService.GetUserKeyWithLegacySupportAsync();
|
|
||||||
|
SymmetricCryptoKey encryptionKey = orgKey;
|
||||||
|
if (cipher != null && cipherView != null)
|
||||||
|
{
|
||||||
|
encryptionKey = await UpdateCipherAndGetCipherKeyAsync(cipher, cipherView, orgKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionKey ??= await _cryptoService.GetUserKeyWithLegacySupportAsync();
|
||||||
|
|
||||||
var (attachmentKey, protectedAttachmentKey) = await _cryptoService.MakeDataEncKeyAsync(encryptionKey);
|
var (attachmentKey, protectedAttachmentKey) = await _cryptoService.MakeDataEncKeyAsync(encryptionKey);
|
||||||
return new Tuple<SymmetricCryptoKey, EncString, SymmetricCryptoKey>(attachmentKey, protectedAttachmentKey, encryptionKey);
|
return new Tuple<SymmetricCryptoKey, EncString, SymmetricCryptoKey>(attachmentKey, protectedAttachmentKey, encryptionKey);
|
||||||
}
|
}
|
||||||
@@ -1082,7 +1139,7 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
await EncryptObjPropertyAsync(model, attachment, new HashSet<string>
|
await EncryptObjPropertyAsync(model, attachment, new HashSet<string>
|
||||||
{
|
{
|
||||||
"FileName"
|
nameof(AttachmentView.FileName)
|
||||||
}, key);
|
}, key);
|
||||||
if (model.Key != null)
|
if (model.Key != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -62,6 +62,16 @@ namespace Bit.Core.Services
|
|||||||
return _stateService.GetUserKeyAsync(userId);
|
return _stateService.GetUserKeyAsync(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsLegacyUserAsync(MasterKey masterKey = null, string userId = null)
|
||||||
|
{
|
||||||
|
masterKey ??= await GetMasterKeyAsync(userId);
|
||||||
|
if (masterKey == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await ValidateUserKeyAsync(new UserKey(masterKey.Key));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<UserKey> GetUserKeyWithLegacySupportAsync(string userId = null)
|
public async Task<UserKey> GetUserKeyWithLegacySupportAsync(string userId = null)
|
||||||
{
|
{
|
||||||
var userKey = await GetUserKeyAsync(userId);
|
var userKey = await GetUserKeyAsync(userId);
|
||||||
@@ -226,9 +236,9 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(key));
|
throw new ArgumentNullException(nameof(key));
|
||||||
}
|
}
|
||||||
if (!(key is UserKey) && !(key is OrgKey))
|
if (!(key is UserKey) && !(key is OrgKey) && !(key is CipherKey))
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Data encryption keys must be of type UserKey or OrgKey. {key.GetType().FullName} unsupported.");
|
throw new ArgumentException($"Data encryption keys must be of type UserKey or OrgKey or CipherKey. {key.GetType().FullName} unsupported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var newSymKey = await _cryptoFunctionService.RandomBytesAsync(64);
|
var newSymKey = await _cryptoFunctionService.RandomBytesAsync(64);
|
||||||
@@ -997,6 +1007,31 @@ namespace Bit.Core.Services
|
|||||||
return keyCreator(key);
|
return keyCreator(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ValidateUserKeyAsync(UserKey key, string userId = null)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var encPrivateKey = await _stateService.GetPrivateKeyEncryptedAsync(userId);
|
||||||
|
if (encPrivateKey == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateKey = await DecryptToBytesAsync(new EncString(encPrivateKey), key);
|
||||||
|
await _cryptoFunctionService.RsaExtractPublicKeyAsync(privateKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class EncryptedObject
|
private class EncryptedObject
|
||||||
{
|
{
|
||||||
public byte[] Iv { get; set; }
|
public byte[] Iv { get; set; }
|
||||||
@@ -1019,6 +1054,10 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
// Decrypt
|
// Decrypt
|
||||||
var masterKey = new MasterKey(Convert.FromBase64String(oldKey));
|
var masterKey = new MasterKey(Convert.FromBase64String(oldKey));
|
||||||
|
if (await IsLegacyUserAsync(masterKey, userId))
|
||||||
|
{
|
||||||
|
throw new LegacyUserException();
|
||||||
|
}
|
||||||
var encryptedUserKey = await _stateService.GetEncKeyEncryptedAsync(userId);
|
var encryptedUserKey = await _stateService.GetEncKeyEncryptedAsync(userId);
|
||||||
if (encryptedUserKey == null)
|
if (encryptedUserKey == null)
|
||||||
{
|
{
|
||||||
@@ -1058,6 +1097,10 @@ namespace Bit.Core.Services
|
|||||||
kdfConfig,
|
kdfConfig,
|
||||||
oldPinKey
|
oldPinKey
|
||||||
);
|
);
|
||||||
|
if (await IsLegacyUserAsync(masterKey))
|
||||||
|
{
|
||||||
|
throw new LegacyUserException();
|
||||||
|
}
|
||||||
var encUserKey = await _stateService.GetEncKeyEncryptedAsync();
|
var encUserKey = await _stateService.GetEncKeyEncryptedAsync();
|
||||||
var userKey = await DecryptUserKeyWithMasterKeyAsync(
|
var userKey = await DecryptUserKeyWithMasterKeyAsync(
|
||||||
masterKey,
|
masterKey,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@@ -67,14 +69,23 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if (!await _cryptoService.HasUserKeyAsync(userId))
|
if (!await _cryptoService.HasUserKeyAsync(userId))
|
||||||
{
|
{
|
||||||
if (!await _cryptoService.HasAutoUnlockKeyAsync(userId))
|
try
|
||||||
{
|
{
|
||||||
return true;
|
if (!await _cryptoService.HasAutoUnlockKeyAsync(userId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (userId != null && await _stateService.GetActiveUserIdAsync() != userId)
|
||||||
|
{
|
||||||
|
await _cryptoService.SetUserKeyAsync(await _cryptoService.GetAutoUnlockKeyAsync(userId),
|
||||||
|
userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (userId != null && await _stateService.GetActiveUserIdAsync() != userId)
|
catch (LegacyUserException)
|
||||||
{
|
{
|
||||||
await _cryptoService.SetUserKeyAsync(await _cryptoService.GetAutoUnlockKeyAsync(userId), userId);
|
await LogOutAsync(false, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check again to verify auto key was set
|
// Check again to verify auto key was set
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ namespace Bit.Core.Utilities
|
|||||||
var organizationService = new OrganizationService(stateService, apiService);
|
var organizationService = new OrganizationService(stateService, apiService);
|
||||||
var settingsService = new SettingsService(stateService);
|
var settingsService = new SettingsService(stateService);
|
||||||
var fileUploadService = new FileUploadService(apiService);
|
var fileUploadService = new FileUploadService(apiService);
|
||||||
|
var configService = new ConfigService(apiService, stateService, logger);
|
||||||
var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService,
|
var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService,
|
||||||
fileUploadService, storageService, i18nService, () => searchService, clearCipherCacheKey,
|
fileUploadService, storageService, i18nService, () => searchService, configService, clearCipherCacheKey,
|
||||||
allClearCipherCacheKeys);
|
allClearCipherCacheKeys);
|
||||||
var folderService = new FolderService(cryptoService, stateService, apiService, i18nService, cipherService);
|
var folderService = new FolderService(cryptoService, stateService, apiService, i18nService, cipherService);
|
||||||
var collectionService = new CollectionService(cryptoService, stateService, i18nService);
|
var collectionService = new CollectionService(cryptoService, stateService, i18nService);
|
||||||
@@ -89,7 +90,6 @@ namespace Bit.Core.Utilities
|
|||||||
var environmentService = new EnvironmentService(apiService, stateService, conditionedRunner);
|
var environmentService = new EnvironmentService(apiService, stateService, conditionedRunner);
|
||||||
var eventService = new EventService(apiService, stateService, organizationService, cipherService);
|
var eventService = new EventService(apiService, stateService, organizationService, cipherService);
|
||||||
var usernameGenerationService = new UsernameGenerationService(cryptoService, apiService, stateService);
|
var usernameGenerationService = new UsernameGenerationService(cryptoService, apiService, stateService);
|
||||||
var configService = new ConfigService(apiService, stateService, logger);
|
|
||||||
|
|
||||||
Register<IConditionedAwaiterManager>(conditionedRunner);
|
Register<IConditionedAwaiterManager>(conditionedRunner);
|
||||||
Register<ITokenService>("tokenService", tokenService);
|
Register<ITokenService>("tokenService", tokenService);
|
||||||
@@ -97,6 +97,7 @@ namespace Bit.Core.Utilities
|
|||||||
Register<IAppIdService>("appIdService", appIdService);
|
Register<IAppIdService>("appIdService", appIdService);
|
||||||
Register<IOrganizationService>("organizationService", organizationService);
|
Register<IOrganizationService>("organizationService", organizationService);
|
||||||
Register<ISettingsService>("settingsService", settingsService);
|
Register<ISettingsService>("settingsService", settingsService);
|
||||||
|
Register<IConfigService>(configService);
|
||||||
Register<ICipherService>("cipherService", cipherService);
|
Register<ICipherService>("cipherService", cipherService);
|
||||||
Register<IFolderService>("folderService", folderService);
|
Register<IFolderService>("folderService", folderService);
|
||||||
Register<ICollectionService>("collectionService", collectionService);
|
Register<ICollectionService>("collectionService", collectionService);
|
||||||
@@ -115,7 +116,6 @@ namespace Bit.Core.Utilities
|
|||||||
Register<IEnvironmentService>("environmentService", environmentService);
|
Register<IEnvironmentService>("environmentService", environmentService);
|
||||||
Register<IEventService>("eventService", eventService);
|
Register<IEventService>("eventService", eventService);
|
||||||
Register<IUsernameGenerationService>(usernameGenerationService);
|
Register<IUsernameGenerationService>(usernameGenerationService);
|
||||||
Register<IConfigService>(configService);
|
|
||||||
Register<IDeviceTrustCryptoService>(deviceTrustCryptoService);
|
Register<IDeviceTrustCryptoService>(deviceTrustCryptoService);
|
||||||
Register<IPasswordResetEnrollmentService>(passwordResetEnrollmentService);
|
Register<IPasswordResetEnrollmentService>(passwordResetEnrollmentService);
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/Core/Utilities/VersionHelpers.cs
Normal file
34
src/Core/Utilities/VersionHelpers.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities
|
||||||
|
{
|
||||||
|
public static class VersionHelpers
|
||||||
|
{
|
||||||
|
private const char SUFFIX_SEPARATOR = '-';
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares two server versions and gets whether the <paramref name="targetVersion"/>
|
||||||
|
/// is greater than or equal to <paramref name="compareToVersion"/>.
|
||||||
|
/// WARNING: This doesn't take into account hotfix suffix.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetVersion">Version to compare</param>
|
||||||
|
/// <param name="compareToVersion">Version to compare against</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>True</c> if <paramref name="targetVersion"/> is greater than or equal to <paramref name="compareToVersion"/>; <c>False</c> otherwise.
|
||||||
|
/// </returns>
|
||||||
|
public static bool IsServerVersionGreaterThanOrEqualTo(string targetVersion, string compareToVersion)
|
||||||
|
{
|
||||||
|
return new Version(RemoveSuffix(targetVersion)).CompareTo(new Version(RemoveSuffix(compareToVersion))) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RemoveSuffix(string version)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
return version.Split(SUFFIX_SEPARATOR)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.8bit.bitwarden.autofill</string>
|
<string>com.8bit.bitwarden.autofill</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2023.8.1</string>
|
<string>2023.9.2</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Bit.App.Resources;
|
|||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@@ -120,8 +121,7 @@ namespace Bit.iOS.Core.Controllers
|
|||||||
_pinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
|
_pinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
|
||||||
_pinStatus == PinLockType.Persistent;
|
_pinStatus == PinLockType.Persistent;
|
||||||
|
|
||||||
_biometricEnabled = await _vaultTimeoutService.IsBiometricLockSetAsync()
|
_biometricEnabled = await IsBiometricsEnabledAsync();
|
||||||
&& await _biometricService.CanUseBiometricsUnlockAsync();
|
|
||||||
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
|
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
|
||||||
_biometricUnlockOnly = !_hasMasterPassword && _biometricEnabled && !_pinEnabled;
|
_biometricUnlockOnly = !_hasMasterPassword && _biometricEnabled && !_pinEnabled;
|
||||||
|
|
||||||
@@ -260,99 +260,26 @@ namespace Bit.iOS.Core.Controllers
|
|||||||
&&
|
&&
|
||||||
kdfConfig.Memory > Constants.MaximumArgon2IdMemoryBeforeExtensionCrashing
|
kdfConfig.Memory > Constants.MaximumArgon2IdMemoryBeforeExtensionCrashing
|
||||||
&&
|
&&
|
||||||
!await _platformUtilsService.ShowDialogAsync(AppResources.UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve, AppResources.Warning, AppResources.Continue, AppResources.Cancel))
|
!await _platformUtilsService.ShowDialogAsync(
|
||||||
|
AppResources.UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve,
|
||||||
|
AppResources.Warning, AppResources.Continue, AppResources.Cancel))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_pinEnabled)
|
if (_pinEnabled)
|
||||||
{
|
{
|
||||||
var failed = true;
|
await UnlockWithPinAsync(inputtedValue, email, kdfConfig);
|
||||||
try
|
|
||||||
{
|
|
||||||
EncString userKeyPin = null;
|
|
||||||
EncString oldPinProtected = null;
|
|
||||||
if (_pinStatus == PinLockType.Persistent)
|
|
||||||
{
|
|
||||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
|
||||||
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
|
||||||
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
|
||||||
}
|
|
||||||
else if (_pinStatus == PinLockType.Transient)
|
|
||||||
{
|
|
||||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
|
||||||
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
UserKey userKey;
|
|
||||||
if (oldPinProtected != null)
|
|
||||||
{
|
|
||||||
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
|
||||||
_pinStatus == PinLockType.Transient,
|
|
||||||
inputtedValue,
|
|
||||||
email,
|
|
||||||
kdfConfig,
|
|
||||||
oldPinProtected
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
|
||||||
inputtedValue,
|
|
||||||
email,
|
|
||||||
kdfConfig,
|
|
||||||
userKeyPin
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
|
||||||
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
|
||||||
failed = decryptedPin != inputtedValue;
|
|
||||||
if (!failed)
|
|
||||||
{
|
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
||||||
await SetKeyAndContinueAsync(userKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
if (failed)
|
|
||||||
{
|
|
||||||
await HandleFailedCredentialsAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(inputtedValue, email, kdfConfig);
|
await UnlockWithMasterPasswordAsync(inputtedValue, email, kdfConfig);
|
||||||
|
|
||||||
var storedPasswordHash = await _cryptoService.GetMasterKeyHashAsync();
|
|
||||||
if (storedPasswordHash == null)
|
|
||||||
{
|
|
||||||
var oldKey = await _secureStorageService.GetAsync<string>("oldKey");
|
|
||||||
if (masterKey.KeyB64 == oldKey)
|
|
||||||
{
|
|
||||||
var localPasswordHash = await _cryptoService.HashMasterKeyAsync(inputtedValue, masterKey, HashPurpose.LocalAuthorization);
|
|
||||||
await _secureStorageService.RemoveAsync("oldKey");
|
|
||||||
await _cryptoService.SetMasterKeyHashAsync(localPasswordHash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(inputtedValue, masterKey);
|
|
||||||
if (passwordValid)
|
|
||||||
{
|
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
||||||
|
|
||||||
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
|
||||||
await _cryptoService.SetMasterKeyAsync(masterKey);
|
|
||||||
await SetKeyAndContinueAsync(userKey, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await HandleFailedCredentialsAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (LegacyUserException)
|
||||||
|
{
|
||||||
|
await HandleLegacyUserAsync();
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_checkingPassword = false;
|
_checkingPassword = false;
|
||||||
@@ -370,20 +297,125 @@ namespace Bit.iOS.Core.Controllers
|
|||||||
InvalidValue();
|
InvalidValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UnlockWithPinAsync(string inputPin, string email, KdfConfig kdfConfig)
|
||||||
|
{
|
||||||
|
var failed = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EncString userKeyPin = null;
|
||||||
|
EncString oldPinProtected = null;
|
||||||
|
if (_pinStatus == PinLockType.Persistent)
|
||||||
|
{
|
||||||
|
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
||||||
|
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
||||||
|
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
||||||
|
}
|
||||||
|
else if (_pinStatus == PinLockType.Transient)
|
||||||
|
{
|
||||||
|
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
||||||
|
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserKey userKey;
|
||||||
|
if (oldPinProtected != null)
|
||||||
|
{
|
||||||
|
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
||||||
|
_pinStatus == PinLockType.Transient,
|
||||||
|
inputPin,
|
||||||
|
email,
|
||||||
|
kdfConfig,
|
||||||
|
oldPinProtected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
||||||
|
inputPin,
|
||||||
|
email,
|
||||||
|
kdfConfig,
|
||||||
|
userKeyPin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||||
|
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
||||||
|
failed = decryptedPin != inputPin;
|
||||||
|
if (!failed)
|
||||||
|
{
|
||||||
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
|
await SetKeyAndContinueAsync(userKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed)
|
||||||
|
{
|
||||||
|
await HandleFailedCredentialsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnlockWithMasterPasswordAsync(string inputPassword, string email, KdfConfig kdfConfig)
|
||||||
|
{
|
||||||
|
var masterKey = await _cryptoService.MakeMasterKeyAsync(inputPassword, email, kdfConfig);
|
||||||
|
if (await _cryptoService.IsLegacyUserAsync(masterKey))
|
||||||
|
{
|
||||||
|
throw new LegacyUserException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedPasswordHash = await _cryptoService.GetMasterKeyHashAsync();
|
||||||
|
if (storedPasswordHash == null)
|
||||||
|
{
|
||||||
|
var oldKey = await _secureStorageService.GetAsync<string>("oldKey");
|
||||||
|
if (masterKey.KeyB64 == oldKey)
|
||||||
|
{
|
||||||
|
var localPasswordHash =
|
||||||
|
await _cryptoService.HashMasterKeyAsync(inputPassword, masterKey,
|
||||||
|
HashPurpose.LocalAuthorization);
|
||||||
|
await _secureStorageService.RemoveAsync("oldKey");
|
||||||
|
await _cryptoService.SetMasterKeyHashAsync(localPasswordHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(inputPassword, masterKey);
|
||||||
|
if (passwordValid)
|
||||||
|
{
|
||||||
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
|
|
||||||
|
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||||
|
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||||
|
await SetKeyAndContinueAsync(userKey, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await HandleFailedCredentialsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task PromptBiometricAsync()
|
public async Task PromptBiometricAsync()
|
||||||
{
|
{
|
||||||
if (!_biometricEnabled || !_biometricIntegrityValid)
|
try
|
||||||
{
|
{
|
||||||
return;
|
if (!_biometricEnabled || !_biometricIntegrityValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||||
|
_pinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||||
|
() => MasterPasswordCell.TextField.BecomeFirstResponder());
|
||||||
|
await _stateService.SetBiometricLockedAsync(!success);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
||||||
|
await SetKeyAndContinueAsync(userKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
catch (LegacyUserException)
|
||||||
_pinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
|
||||||
() => MasterPasswordCell.TextField.BecomeFirstResponder());
|
|
||||||
await _stateService.SetBiometricLockedAsync(!success);
|
|
||||||
if (success)
|
|
||||||
{
|
{
|
||||||
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
await HandleLegacyUserAsync();
|
||||||
await SetKeyAndContinueAsync(userKey);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,6 +468,29 @@ namespace Bit.iOS.Core.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsBiometricsEnabledAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _vaultTimeoutService.IsBiometricLockSetAsync() &&
|
||||||
|
await _biometricService.CanUseBiometricsUnlockAsync();
|
||||||
|
}
|
||||||
|
catch (LegacyUserException)
|
||||||
|
{
|
||||||
|
await HandleLegacyUserAsync();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleLegacyUserAsync()
|
||||||
|
{
|
||||||
|
// Legacy users must migrate on web vault.
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong,
|
||||||
|
AppResources.AnErrorHasOccurred,
|
||||||
|
AppResources.Ok);
|
||||||
|
await _vaultTimeoutService.LogOutAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private void InvalidValue()
|
private void InvalidValue()
|
||||||
{
|
{
|
||||||
var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred,
|
var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.8bit.bitwarden.find-login-action-extension</string>
|
<string>com.8bit.bitwarden.find-login-action-extension</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2023.8.1</string>
|
<string>2023.9.2</string>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2023.8.1</string>
|
<string>2023.9.2</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.8bit.bitwarden</string>
|
<string>com.8bit.bitwarden</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2023.8.1</string>
|
<string>2023.9.2</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleIconName</key>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
|
||||||
@@ -26,16 +28,36 @@ namespace Bit.Core.Test.AutoFixture
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class UserCipherView : ICustomization
|
||||||
|
{
|
||||||
|
public void Customize(IFixture fixture)
|
||||||
|
{
|
||||||
|
byte[] getRandomBytes(int size)
|
||||||
|
{
|
||||||
|
Random random = new Random();
|
||||||
|
|
||||||
|
byte[] bytes = new byte[size];
|
||||||
|
random.NextBytes(bytes);
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
fixture.Customize<CipherView>(composer => composer
|
||||||
|
.Without(c => c.OrganizationId)
|
||||||
|
.Without(c => c.Attachments)
|
||||||
|
.With(c => c.Key, new SymmetricCryptoKey(getRandomBytes(32), Enums.EncryptionType.AesCbc128_HmacSha256_B64)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal class UserCipherAutoDataAttribute : CustomAutoDataAttribute
|
internal class UserCipherAutoDataAttribute : CustomAutoDataAttribute
|
||||||
{
|
{
|
||||||
public UserCipherAutoDataAttribute() : base(new SutProviderCustomization(),
|
public UserCipherAutoDataAttribute() : base(new SutProviderCustomization(),
|
||||||
new UserCipher())
|
new UserCipher(), new UserCipherView())
|
||||||
{ }
|
{ }
|
||||||
}
|
}
|
||||||
internal class InlineUserCipherAutoDataAttribute : InlineCustomAutoDataAttribute
|
internal class InlineUserCipherAutoDataAttribute : InlineCustomAutoDataAttribute
|
||||||
{
|
{
|
||||||
public InlineUserCipherAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
|
public InlineUserCipherAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
|
||||||
typeof(UserCipher) }, values)
|
typeof(UserCipher), typeof(UserCipherView) }, values)
|
||||||
{ }
|
{ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace Bit.Core.Test.Services
|
|||||||
{
|
{
|
||||||
[Theory, UserCipherAutoData]
|
[Theory, UserCipherAutoData]
|
||||||
public async Task SaveWithServerAsync_PrefersFileUploadService(SutProvider<CipherService> sutProvider,
|
public async Task SaveWithServerAsync_PrefersFileUploadService(SutProvider<CipherService> sutProvider,
|
||||||
Cipher cipher, string fileName, EncByteArray data, AttachmentUploadDataResponse uploadDataResponse, EncString encKey)
|
Cipher cipher, CipherView cipherView, string fileName, EncByteArray data, AttachmentUploadDataResponse uploadDataResponse, EncString encKey)
|
||||||
{
|
{
|
||||||
var encFileName = new EncString(fileName);
|
var encFileName = new EncString(fileName);
|
||||||
sutProvider.GetDependency<ICryptoService>().EncryptAsync(fileName, Arg.Any<SymmetricCryptoKey>())
|
sutProvider.GetDependency<ICryptoService>().EncryptAsync(fileName, Arg.Any<SymmetricCryptoKey>())
|
||||||
@@ -33,7 +33,7 @@ namespace Bit.Core.Test.Services
|
|||||||
sutProvider.GetDependency<IApiService>().PostCipherAttachmentAsync(cipher.Id, Arg.Any<AttachmentRequest>())
|
sutProvider.GetDependency<IApiService>().PostCipherAttachmentAsync(cipher.Id, Arg.Any<AttachmentRequest>())
|
||||||
.Returns(uploadDataResponse);
|
.Returns(uploadDataResponse);
|
||||||
|
|
||||||
await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, fileName, data.Buffer);
|
await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, cipherView, fileName, data.Buffer);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IFileUploadService>().Received(1)
|
await sutProvider.GetDependency<IFileUploadService>().Received(1)
|
||||||
.UploadCipherAttachmentFileAsync(uploadDataResponse, encFileName, data);
|
.UploadCipherAttachmentFileAsync(uploadDataResponse, encFileName, data);
|
||||||
@@ -43,7 +43,7 @@ namespace Bit.Core.Test.Services
|
|||||||
[InlineUserCipherAutoData(HttpStatusCode.NotFound)]
|
[InlineUserCipherAutoData(HttpStatusCode.NotFound)]
|
||||||
[InlineUserCipherAutoData(HttpStatusCode.MethodNotAllowed)]
|
[InlineUserCipherAutoData(HttpStatusCode.MethodNotAllowed)]
|
||||||
public async Task SaveWithServerAsync_FallsBackToLegacyFormData(HttpStatusCode statusCode,
|
public async Task SaveWithServerAsync_FallsBackToLegacyFormData(HttpStatusCode statusCode,
|
||||||
SutProvider<CipherService> sutProvider, Cipher cipher, string fileName, EncByteArray data,
|
SutProvider<CipherService> sutProvider, Cipher cipher, CipherView cipherView, string fileName, EncByteArray data,
|
||||||
CipherResponse response, EncString encKey)
|
CipherResponse response, EncString encKey)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICryptoService>().EncryptAsync(fileName, Arg.Any<SymmetricCryptoKey>())
|
sutProvider.GetDependency<ICryptoService>().EncryptAsync(fileName, Arg.Any<SymmetricCryptoKey>())
|
||||||
@@ -56,7 +56,7 @@ namespace Bit.Core.Test.Services
|
|||||||
sutProvider.GetDependency<IApiService>().PostCipherAttachmentLegacyAsync(cipher.Id, Arg.Any<MultipartFormDataContent>())
|
sutProvider.GetDependency<IApiService>().PostCipherAttachmentLegacyAsync(cipher.Id, Arg.Any<MultipartFormDataContent>())
|
||||||
.Returns(response);
|
.Returns(response);
|
||||||
|
|
||||||
await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, fileName, data.Buffer);
|
await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, cipherView, fileName, data.Buffer);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IApiService>().Received(1)
|
await sutProvider.GetDependency<IApiService>().Received(1)
|
||||||
.PostCipherAttachmentLegacyAsync(cipher.Id, Arg.Any<MultipartFormDataContent>());
|
.PostCipherAttachmentLegacyAsync(cipher.Id, Arg.Any<MultipartFormDataContent>());
|
||||||
@@ -64,7 +64,7 @@ namespace Bit.Core.Test.Services
|
|||||||
|
|
||||||
[Theory, UserCipherAutoData]
|
[Theory, UserCipherAutoData]
|
||||||
public async Task SaveWithServerAsync_ThrowsOnBadRequestApiException(SutProvider<CipherService> sutProvider,
|
public async Task SaveWithServerAsync_ThrowsOnBadRequestApiException(SutProvider<CipherService> sutProvider,
|
||||||
Cipher cipher, string fileName, EncByteArray data, EncString encKey)
|
Cipher cipher, CipherView cipherView, string fileName, EncByteArray data, EncString encKey)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICryptoService>().EncryptAsync(fileName, Arg.Any<SymmetricCryptoKey>())
|
sutProvider.GetDependency<ICryptoService>().EncryptAsync(fileName, Arg.Any<SymmetricCryptoKey>())
|
||||||
.Returns(new EncString(fileName));
|
.Returns(new EncString(fileName));
|
||||||
@@ -77,7 +77,7 @@ namespace Bit.Core.Test.Services
|
|||||||
.Throws(expectedException);
|
.Throws(expectedException);
|
||||||
|
|
||||||
var actualException = await Assert.ThrowsAsync<ApiException>(async () =>
|
var actualException = await Assert.ThrowsAsync<ApiException>(async () =>
|
||||||
await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, fileName, data.Buffer));
|
await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, cipherView, fileName, data.Buffer));
|
||||||
|
|
||||||
Assert.Equal(expectedException.Error.StatusCode, actualException.Error.StatusCode);
|
Assert.Equal(expectedException.Error.StatusCode, actualException.Error.StatusCode);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user