mirror of
https://github.com/bitwarden/mobile
synced 2025-12-15 07:43:37 +00:00
* [PM-1208] Add Device approval options screen. View model waiting for additional logic to be added.
* [PM-1208] Add device related api endpoint. Add AccoundDecryptOptions model and property to user Account.
* [PM-1208] Add continue button and not you option
* [PM-1379] add DeviceTrustCryptoService with establish trust logic (#2535)
* [PM-1379] add DeviceCryptoService with establish trust logic
* PM-1379 update api location and other minor refactors
* pm-1379 fix encoding
* update trusted device keys api call to Put
* [PM-1379] rename DeviceCryptoService to DeviceTrustCryptoService
- refactors to prevent side effects
* [PM-1379] rearrange methods in DeviceTrustCryptoService
* [PM-1379] rearrange methods in abstraction
* [PM-1379] deconstruct tuples
* [PM-1379] remove extra tasks
* [PM-2583] Answer auth request with mp field as null if doesn't have it. (#2609)
* [PM-2287][PM-2289][PM-2293] Approval Options (#2608)
* [PM-2293] Add AuthRequestType to PasswordlessLoginPage.
* [PM-2293] Add Actions to ApproveWithDevicePage
* [PM-2293] Change screen text based on AuthRequestType
* [PM-2293] Refactor AuthRequestType enum. Add label. Remove unnecessary actions.
* [PM-2293] Change boolean variable expression.
* [PM-2293] Trust device after admin request login.
* code format
* [PM-2287] Add trust device to master password unlock. Change trust device method. Remove email from SSO login page.
* [PM-2293] Fix state variable get set.
* [PM-2287][PM-2289][PM-2293] Rename method
* [PM-1201] Change timeout actions available based on hasMasterPassword (#2610)
* [PM-1201] Change timeout actions available based on hasMasterPassword
* [PM-2731] add user key and master key types
* [PM-2713] add new state for new keys and obsolete old ones
- UserKey
- MasterKey
- UserKeyMasterKey (enc UserKey from User Table)
* [PM-271] add UserKey and MasterKey support to crypto service
* [PM-2713] rename key hash to password hash & begin add methods to crypto service
* [PM-2713] continue organizing crypto service
* [PM-2713] more updates to crypto service
* [PM-2713] add new pin methods to state service
* [PM-2713] fix signature of GetUserKeyPin
* [PM-2713] add make user key method to crypto service
* [PM-2713] refresh pin key when setting user key
* [PM-2713] use new MakeMasterKey method
* [PM-2713] add toggle method to crypto service for keys
* [PM-2713] converting calls to new crypto service api
* [PM-2713] add migration for pin on lock screens
* [PM-2713] more conversions to new crypto service api
* [PM-2713] convert cipher service and others to crypto service api
* [PM-2713] More conversions to crypto api
* [PM-2713] use new crypto service api in auth service
* [PM-2713] remove unused cached values in crypto service
* [PM-2713] set decrypt and set user key in login helper
* fix bad merge
* Update crypto service api call to fix build
* [PM-1208] Fix app resource file
* [PM-1208] Fix merge
* [PM-1208] Fix merge
* [PM-2713] optimize async code in crypto service
* [PM-2713] rename password hash to master key hash
* [PM-2713] fix casting issues and pin
* [PM-2713] remove extra comment
* [PM-2713] remove broken casting
* [PM-2297] Login with trusted device (Flow 2) (#2623)
* [PM-2297] Add DecryptUserKeyWithDeviceKey method
* [PM-2297] Add methods to DeviceTrustCryptoService update decryption options model
* [PM-2297] Update account decryption options model
* [PM-2297] Fix TrustedDeviceOption and DeviceResponse model. Change StateService device key get set to have default user id
* [PM-2297] Update navigation to decryption options
* [PM-2297] Add missing action navigations to iOS extensions
* [PM-2297] Fix trust device bug/typo
* [PM-2297] Fix model bug
* [PM-2297] Fix state var crash
* [PM-2297] Add trust device login logic to auth service
* [PM-2297] Refactor auth service key connector code
* [PM-2297] Remove reconciledOptions for deviceKey in state service
* [PM-2297] Remove unnecessary user id params
* [PM-2289] [PM-2293] TDE Login with device Admin Request (#2642)
* [PM-2713] deconstruct new key pair
* [PM-2713] rename PrivateKey methods to UserPrivateKey on crypto service
* [PM-2713] rename PinLockEnum to PinLockType
* [PM-2713] don't pass user key as param when encrypting
* [PM-2713] rename toggle method, don't reset enc user key
* [PM-2713] pr feedback
* [PM-2713] PR feedback
* [PM-2713] rename get pin lock type method
* [PM-2713] revert feedback for build
* [PM-2713] rename state methods
* [PM-2713] combine makeDataEncKey methods
* [PM-2713] consolidate attachment key creation
- also fix ios files missed during symbol rename
* [PM-2713] replace generic with inherited class
* rename account keys to be more descriptive
* [PM-2713] add auto unlock key to mobile
* [PM-1208] Add TDE flows for new users (#2655)
* [PM-1208] Create new user on SSO. Logout if not password is setup or has pending admin auth request.
* [PM-1208] Fix new user UserKey decryption.
* [PM-1208] Add new user continue to vault logic. Auto enrol user on continue.
* [PM-1208] Trust device only if needed
* [PM-1208] Add logic for New User SSO.
* [PM-1208] Add logic for New User SSO (missing file).
* [PM-2713] set user key on set password page
* [PM-2713] set enc user key during kc onboarding
* fix formatting
* [PM-2713] make method async again
- returning null from a task thats not async throws
* [PM-2713] clear service cache when adding new account
* Fix build after merge
* [PM-3313] Fix Android SSO Login (#2663)
* [PM-3313] Catch exception on AuthPendingRequest
* [PM-3313] Fix lock timeout action if user doesn't have a master password.
* code format
* [PM-3313] Null email in Approval Options screen (#2664)
* [PM-3313] Fix null email in approval options screen
* [PM-3320][PM-3321] Fix labels and UI tweaks (#2666)
* [PM-3320] Fix UI copy and remember me default ON.
* [PM-3321] Fix UI on Log in with device screen.
* [PM-3337] Fix admin request deny error (#2669)
* [PM-3342] Not you button logs user out. (#2672)
* [PM-3319] Check for admin request in Lock page (#2668)
* [PM-3319] Ignore admin auth request when choosing mp as decryption option.
* [PM-2289] Change header title based on auth request type (#2670)
* [PM-2289] Change header title based on auth request type
* [PM-3333] Check for purged admin auth requests (#2671)
* [PM-3333] Check for purged admin auth requests
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
---------
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
* [PM-3341] Vault Timeout Action not persisted correctly (#2673)
* [PM-3341] Fix timeout action change when navigating
* [PM-3357] Fix copy for Login Initiated (#2674)
* [PM-3362] Fix auth request approval (#2675)
* [PM-3362] Fix auth request approval
* [PM-3362] Add new exception type
* [PM-3102] Update Master password reprompt to be based on MP instead of Key Connector (#2653)
* PM-3102 Added check to see if a user has master password set replacing previous usage of key connector.
* PM-3102 Fix formatting
* [PM-2713] Final merge from Key Migration branch to TDE Feature branch (#2667)
* [PM-2713] add async to key connector service methods
* [PM-2713] rename ephemeral pin key
* add state for biometric key and accept UserKey instead of string for auto key
* Get UserKey from bio state on unlock
* PM-2713 Fix auto-migrating EncKeyEncrypted into MasterKey encrypted UserKey when requesting DecryptUserKeyWithMasterKeyAsync is called
* renaming bio key and fix build
* PM-3194 Fix biometrics button to be shown on upgrade when no UserKey is present yet
* revert removal of key connector service from auth service
* PM-2713 set user key when using KC
* clear enc user key after migration
* use is true for nullable bool
* PR feedback, refactor kc service
---------
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
* Fix app fresh install user login with master password. (#2676)
* [PM-3303] Fix biometric login after key migration (#2679)
* [PM-3303] Add condition to biometric unlock
* [PM-3381] Fix TDE login 2FA flow (#2678)
* [PM-3381] Check for vault lock on 2FA screen
* [PM-3381] Move logic to ViewModel
* [PM-3381] Fix null vm error
* [PM-3379] Fix key rotation on trusted device. (#2680)
* [PM-3381] Update login flows (#2683)
* [PM-3381] Update login flows
* [PM-3381] Remove _authingWithSso parameter
* PM-3385 Fix MP reprompt item level when no MP hash is stored like logging in with TDE. Also refactor code to be more maintainable (#2687)
* PM-3386 Fix MP reprompt / OTP decision to be also based on the master key hash. (#2688)
* PM-3450 Fix has master password with mp key hash check (#2689)
* [PM-3394] Fix login with device for passwordless approvals (#2686)
* set activeUserId to null when logging in a new account
- Also stop the user key from being set in inactive accounts
* get token for login with device if approving device doesn't have master key
* add comment
* simplify logic
* check for route instead of using isAuthenticated
- we don't clear the user id when logging in new account
- this means we can't trust the state service, so we have to base our logic off the route in login with device
* use authenticated auth request for tde login with device
* [PM-3394] Add authingWithSso parameter to LoginPasswordlessRequestPage.
* pr feedback
* [PM-3394] Refactor condition
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
---------
Co-authored-by: André Bispo <abispo@bitwarden.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
* [PM-3462] Handle force password reset on mobile with TDE (#2694)
* [PM-3462] Handle force password reset on mobile with TDE
* [PM-3462] update references to refactored crypto method
- fix kc bug, we were sending private key instead of user key to server
- rename kc service method to be correct
* [PM-3462] Update TwoFactorPage login logic
* [PM-3462] Added pending admin request check to TwoFactorPage
* [PM-3462] Added new exception types for null keys
---------
Co-authored-by: André Bispo <abispo@bitwarden.com>
* [PM-1029] Fix Async suffix in ApiService. Add UserKeyNullExceptions.
* [PM 3513] Fix passwordless 2fa login with device on mobile (#2700)
* [PM-3513] Fix 2FA for normal login with device with users without mp
* move _userKey
---------
Co-authored-by: André Bispo <abispo@bitwarden.com>
* clear encrypted pin on logout (#2699)
---------
Co-authored-by: André Bispo <abispo@bitwarden.com>
Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
(cherry picked from commit bfcfd367dd)
360 lines
13 KiB
C#
360 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Input;
|
|
using Bit.App.Abstractions;
|
|
using Bit.App.Resources;
|
|
using Bit.App.Utilities;
|
|
using Bit.Core;
|
|
using Bit.Core.Abstractions;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.Models.Domain;
|
|
using Bit.Core.Models.Response;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Utilities;
|
|
using Xamarin.CommunityToolkit.ObjectModel;
|
|
using Xamarin.Essentials;
|
|
using Xamarin.Forms;
|
|
|
|
namespace Bit.App.Pages
|
|
{
|
|
public class LoginPasswordlessRequestViewModel : CaptchaProtectedViewModel
|
|
{
|
|
private const int REQUEST_TIME_UPDATE_PERIOD_IN_SECONDS = 4;
|
|
|
|
private IDeviceActionService _deviceActionService;
|
|
private IAuthService _authService;
|
|
private ISyncService _syncService;
|
|
private II18nService _i18nService;
|
|
private IStateService _stateService;
|
|
private IPlatformUtilsService _platformUtilsService;
|
|
private IEnvironmentService _environmentService;
|
|
private ILogger _logger;
|
|
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
|
private readonly ICryptoFunctionService _cryptoFunctionService;
|
|
private readonly ICryptoService _cryptoService;
|
|
|
|
protected override II18nService i18nService => _i18nService;
|
|
protected override IEnvironmentService environmentService => _environmentService;
|
|
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
|
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
|
|
|
|
private CancellationTokenSource _checkLoginRequestStatusCts;
|
|
private Task _checkLoginRequestStatusTask;
|
|
private string _fingerprintPhrase;
|
|
private string _email;
|
|
private string _requestId;
|
|
private string _requestAccessCode;
|
|
private AuthRequestType _authRequestType;
|
|
// Item1 publicKey, Item2 privateKey
|
|
private Tuple<byte[], byte[]> _requestKeyPair;
|
|
|
|
public LoginPasswordlessRequestViewModel()
|
|
{
|
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
|
_authService = ServiceContainer.Resolve<IAuthService>();
|
|
_syncService = ServiceContainer.Resolve<ISyncService>();
|
|
_i18nService = ServiceContainer.Resolve<II18nService>();
|
|
_stateService = ServiceContainer.Resolve<IStateService>();
|
|
_logger = ServiceContainer.Resolve<ILogger>();
|
|
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
|
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>();
|
|
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
|
|
|
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
|
|
onException: ex => HandleException(ex),
|
|
allowsMultipleExecutions: false);
|
|
|
|
CloseCommand = new AsyncCommand(() => Device.InvokeOnMainThreadAsync(CloseAction),
|
|
onException: _logger.Exception,
|
|
allowsMultipleExecutions: false);
|
|
}
|
|
|
|
public Action StartTwoFactorAction { get; set; }
|
|
public Action LogInSuccessAction { get; set; }
|
|
public Action UpdateTempPasswordAction { get; set; }
|
|
public Action CloseAction { get; set; }
|
|
public bool AuthingWithSso { get; set; }
|
|
|
|
public ICommand CreatePasswordlessLoginCommand { get; }
|
|
public ICommand CloseCommand { get; }
|
|
|
|
public string HeaderTitle
|
|
{
|
|
get
|
|
{
|
|
switch (_authRequestType)
|
|
{
|
|
case AuthRequestType.AuthenticateAndUnlock:
|
|
return AppResources.LogInWithDevice;
|
|
case AuthRequestType.AdminApproval:
|
|
return AppResources.LogInInitiated;
|
|
default:
|
|
return string.Empty;
|
|
};
|
|
}
|
|
}
|
|
|
|
public string Title
|
|
{
|
|
get
|
|
{
|
|
switch (_authRequestType)
|
|
{
|
|
case AuthRequestType.AuthenticateAndUnlock:
|
|
return AppResources.LogInInitiated;
|
|
case AuthRequestType.AdminApproval:
|
|
return AppResources.AdminApprovalRequested;
|
|
default:
|
|
return string.Empty;
|
|
};
|
|
}
|
|
}
|
|
|
|
public string SubTitle
|
|
{
|
|
get
|
|
{
|
|
switch (_authRequestType)
|
|
{
|
|
case AuthRequestType.AuthenticateAndUnlock:
|
|
return AppResources.ANotificationHasBeenSentToYourDevice;
|
|
case AuthRequestType.AdminApproval:
|
|
return AppResources.YourRequestHasBeenSentToYourAdmin;
|
|
default:
|
|
return string.Empty;
|
|
};
|
|
}
|
|
}
|
|
|
|
public string Description
|
|
{
|
|
get
|
|
{
|
|
switch (_authRequestType)
|
|
{
|
|
case AuthRequestType.AuthenticateAndUnlock:
|
|
return AppResources.PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice;
|
|
case AuthRequestType.AdminApproval:
|
|
return AppResources.YouWillBeNotifiedOnceApproved;
|
|
default:
|
|
return string.Empty;
|
|
};
|
|
}
|
|
}
|
|
|
|
public string OtherOptions
|
|
{
|
|
get
|
|
{
|
|
switch (_authRequestType)
|
|
{
|
|
case AuthRequestType.AuthenticateAndUnlock:
|
|
return AppResources.LogInWithDeviceMustBeSetUpInTheSettingsOfTheBitwardenAppNeedAnotherOption;
|
|
case AuthRequestType.AdminApproval:
|
|
return AppResources.TroubleLoggingIn;
|
|
default:
|
|
return string.Empty;
|
|
};
|
|
}
|
|
}
|
|
|
|
public string FingerprintPhrase
|
|
{
|
|
get => _fingerprintPhrase;
|
|
set => SetProperty(ref _fingerprintPhrase, value);
|
|
}
|
|
|
|
public string Email
|
|
{
|
|
get => _email;
|
|
set => SetProperty(ref _email, value);
|
|
}
|
|
|
|
public AuthRequestType AuthRequestType
|
|
{
|
|
get => _authRequestType;
|
|
set
|
|
{
|
|
SetProperty(ref _authRequestType, value, additionalPropertyNames: new string[]
|
|
{
|
|
nameof(Title),
|
|
nameof(SubTitle),
|
|
nameof(Description),
|
|
nameof(OtherOptions),
|
|
nameof(ResendNotificationVisible)
|
|
});
|
|
PageTitle = HeaderTitle;
|
|
}
|
|
}
|
|
|
|
public bool ResendNotificationVisible => AuthRequestType == AuthRequestType.AuthenticateAndUnlock;
|
|
|
|
public void StartCheckLoginRequestStatus()
|
|
{
|
|
try
|
|
{
|
|
_checkLoginRequestStatusCts?.Cancel();
|
|
_checkLoginRequestStatusCts = new CancellationTokenSource();
|
|
_checkLoginRequestStatusTask = new TimerTask(_logger, CheckLoginRequestStatus, _checkLoginRequestStatusCts).RunPeriodic(TimeSpan.FromSeconds(REQUEST_TIME_UPDATE_PERIOD_IN_SECONDS));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Exception(ex);
|
|
}
|
|
}
|
|
|
|
public void StopCheckLoginRequestStatus()
|
|
{
|
|
try
|
|
{
|
|
_checkLoginRequestStatusCts?.Cancel();
|
|
_checkLoginRequestStatusCts?.Dispose();
|
|
_checkLoginRequestStatusCts = null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Exception(ex);
|
|
}
|
|
}
|
|
|
|
private async Task CheckLoginRequestStatus()
|
|
{
|
|
if (string.IsNullOrEmpty(_requestId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
PasswordlessLoginResponse response = null;
|
|
if (AuthingWithSso)
|
|
{
|
|
response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId);
|
|
}
|
|
else
|
|
{
|
|
response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode);
|
|
}
|
|
|
|
if (response?.RequestApproved != true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StopCheckLoginRequestStatus();
|
|
|
|
var authResult = await _authService.LogInPasswordlessAsync(AuthingWithSso, Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
|
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
|
|
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
|
{
|
|
await HandleLoginCompleteAsync();
|
|
return;
|
|
}
|
|
|
|
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (authResult.TwoFactor)
|
|
{
|
|
StartTwoFactorAction?.Invoke();
|
|
}
|
|
else if (authResult.ForcePasswordReset)
|
|
{
|
|
UpdateTempPasswordAction?.Invoke();
|
|
}
|
|
else
|
|
{
|
|
await HandleLoginCompleteAsync();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StartCheckLoginRequestStatus();
|
|
HandleException(ex);
|
|
}
|
|
}
|
|
|
|
private async Task HandleLoginCompleteAsync()
|
|
{
|
|
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
|
_syncService.FullSyncAsync(true).FireAndForget();
|
|
LogInSuccessAction?.Invoke();
|
|
}
|
|
|
|
private async Task CreatePasswordlessLoginAsync()
|
|
{
|
|
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
|
|
|
|
PasswordlessLoginResponse response = null;
|
|
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
|
if (pendingRequest != null && _authRequestType == AuthRequestType.AdminApproval)
|
|
{
|
|
response = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
|
if (response == null || (response.IsAnswered && !response.RequestApproved.Value))
|
|
{
|
|
// handle pending auth request not valid remove it from state
|
|
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
|
pendingRequest = null;
|
|
response = null;
|
|
}
|
|
else
|
|
{
|
|
// Derive pubKey from privKey in state to avoid MITM attacks
|
|
// Also generate FingerprintPhrase locally for the same reason
|
|
var derivedPublicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(pendingRequest.PrivateKey);
|
|
response.FingerprintPhrase = string.Join("-", await _cryptoService.GetFingerprintAsync(Email, derivedPublicKey));
|
|
response.RequestKeyPair = new Tuple<byte[], byte[]>(derivedPublicKey, pendingRequest.PrivateKey);
|
|
}
|
|
}
|
|
|
|
if (response == null)
|
|
{
|
|
response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType);
|
|
}
|
|
|
|
await HandlePasswordlessLoginAsync(response, pendingRequest == null && _authRequestType == AuthRequestType.AdminApproval);
|
|
await _deviceActionService.HideLoadingAsync();
|
|
}
|
|
|
|
private async Task HandlePasswordlessLoginAsync(PasswordlessLoginResponse response, bool createPendingAdminRequest)
|
|
{
|
|
if (response == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(response));
|
|
}
|
|
|
|
if (createPendingAdminRequest)
|
|
{
|
|
var pendingAuthRequest = new PendingAdminAuthRequest { Id = response.Id, PrivateKey = response.RequestKeyPair.Item2 };
|
|
await _stateService.SetPendingAdminAuthRequestAsync(pendingAuthRequest);
|
|
}
|
|
|
|
FingerprintPhrase = response.FingerprintPhrase;
|
|
_requestId = response.Id;
|
|
_requestAccessCode = response.RequestAccessCode;
|
|
_requestKeyPair = response.RequestKeyPair;
|
|
}
|
|
|
|
private void HandleException(Exception ex)
|
|
{
|
|
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
|
|
{
|
|
await _deviceActionService.HideLoadingAsync();
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
|
|
}).FireAndForget();
|
|
_logger.Exception(ex);
|
|
}
|
|
}
|
|
}
|
|
|