1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-10 21:33:36 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Todd Martin
6dae9f5192 Revert "Enable cipher key encryption for QA"
This reverts commit 1e869bf237.
2023-10-09 10:11:35 -04:00
Todd Martin
1e869bf237 Enable cipher key encryption for QA 2023-10-02 11:05:41 -04:00
github-actions[bot]
bb5125a124 Bumped version to 2023.9.2 (#2797)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
(cherry picked from commit 828043ec97)
2023-09-29 16:31:56 -04:00
Federico Maccaroni
ee1cbae589 Cherry-picked [PM-115] Cipher key encryption update (#2421) 2023-09-29 17:29:04 -03:00
github-actions[bot]
4cc5e13b61 Bumped version to 2023.9.1 (#2784)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
(cherry picked from commit b8cef16711)
2023-09-21 18:17:29 -04:00
Jake Fink
99e64b4e3f [PM-3726] prevent legacy user login (#2769)
* [PM-3726] prevent legacy user login

* [PM-3726] prevent unlock or auto key migration if legacy user

* [PM-3726] add legacy checks to lock page and refactor

* [PM-3726] rethrow exception from pin

* formatting

* [PM-3726] add changes to LockViewController, consolidate logout calls

* formatting

* [PM-3726] pr feedback

* generate resx

* formatting

(cherry picked from commit c4f6ae9077)
2023-09-21 11:55:45 -04:00
github-actions[bot]
0f634ff3f0 Bumped version to 2023.9.0 (#2783)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
(cherry picked from commit 8b9658d2c5)
2023-09-20 12:35:25 -04:00
41 changed files with 7198 additions and 7632 deletions

View File

@@ -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" />

View File

@@ -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();
}
} }
} }

View File

@@ -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();

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);

View File

@@ -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";

View 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.")
{
}
}
}

View File

@@ -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; }
} }
} }

View File

@@ -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;
} }
} }

View File

@@ -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; }
} }
} }

View File

@@ -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()

View File

@@ -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)
{ {

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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;
} }

View File

@@ -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()

View File

@@ -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()

View File

@@ -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));
} }

View File

@@ -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)
{ }
}
} }

View File

@@ -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)
{ {

View File

@@ -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; }
} }
} }

View File

@@ -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; }
} }
} }

View File

@@ -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
{ {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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)
{ {

View File

@@ -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,

View File

@@ -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

View File

@@ -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);
} }

View 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];
}
}
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
{ } { }
} }

View File

@@ -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);
} }