AuthCommands.ps1
|
#requires -Version 5.1 function New-SqliteIdbConnectionFunc { param([Parameter(Mandatory)][string] $ConnectionString) $connType = [Microsoft.Data.Sqlite.SqliteConnection] $dm = [System.Reflection.Emit.DynamicMethod]::new( 'PcOpenSqliteConnection', [System.Data.IDbConnection], [Type[]]@(), [KeeperSecurity.Vault.SqlKeeperStorage]) $il = $dm.GetILGenerator() $il.Emit([System.Reflection.Emit.OpCodes]::Ldstr, $ConnectionString) $il.Emit([System.Reflection.Emit.OpCodes]::Newobj, $connType.GetConstructor(@([string]))) $il.Emit([System.Reflection.Emit.OpCodes]::Dup) $openMi = [System.Data.Common.DbConnection].GetMethod('Open', [Type[]]@()) $il.Emit([System.Reflection.Emit.OpCodes]::Callvirt, $openMi) $il.Emit([System.Reflection.Emit.OpCodes]::Ret) $dm.CreateDelegate([Func[System.Data.IDbConnection]]) } function Initialize-SqliteStorageDependencies { param( [Parameter(Mandatory = $true)][string] $ErrorContext ) $moduleRoot = $PSScriptRoot if ($MyInvocation.MyCommand.Module) { $moduleRoot = $MyInvocation.MyCommand.Module.ModuleBase } $storageUtilsRoot = Join-Path $moduleRoot 'StorageUtils' $requiredStorageDlls = @( 'Microsoft.Data.Sqlite.dll', 'SQLitePCLRaw.batteries_v2.dll', 'SQLitePCLRaw.core.dll', 'SQLitePCLRaw.provider.dynamic_cdecl.dll', 'e_sqlite3.dll' ) $missingFiles = [System.Collections.Generic.List[string]]::new() foreach ($fileName in $requiredStorageDlls) { $filePath = Join-Path $storageUtilsRoot $fileName if (-not (Test-Path -LiteralPath $filePath -PathType Leaf)) { $missingFiles.Add($fileName) } } $nativeSqlitePath = Join-Path $storageUtilsRoot 'e_sqlite3.dll' if (-not (Test-Path -LiteralPath $nativeSqlitePath -PathType Leaf)) { $missingFiles.Add('e_sqlite3.dll') } if ($missingFiles.Count -gt 0) { $missingList = $missingFiles -join ', ' throw "Offline storage dependencies were not found in '$storageUtilsRoot'. Missing: $missingList. When using -UseOfflineStorage, copy the SQLite assemblies from a Commander net8.0 build into the 'StorageUtils' folder under the PowerCommander module directory." } if (-not $script:StorageUtilsAssemblyResolveRegistered) { $script:StorageUtilsAssemblyResolveRegistered = $true $sur = $storageUtilsRoot $mr = $moduleRoot $handler = [System.ResolveEventHandler] { param($AssemblyResolveSource, $AssemblyResolveEventArgs) $simpleName = ($AssemblyResolveEventArgs.Name -split ',')[0] foreach ($root in @($sur, $mr)) { $candidate = [System.IO.Path]::Combine($root, "$simpleName.dll") if ([System.IO.File]::Exists($candidate)) { return [System.Reflection.Assembly]::LoadFrom($candidate) } } return $null } [System.AppDomain]::CurrentDomain.add_AssemblyResolve($handler) } if (-not $script:PcSqlitePclInitialized) { $batteriesPath = Join-Path $storageUtilsRoot 'SQLitePCLRaw.batteries_v2.dll' $batteriesAsm = [System.Reflection.Assembly]::LoadFrom($batteriesPath) $batteriesType = $batteriesAsm.GetType('SQLitePCL.Batteries_V2') if (-not $batteriesType) { throw "Could not load type SQLitePCL.Batteries_V2 from $batteriesPath" } $initMethod = $batteriesType.GetMethod('Init', [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static) [void]$initMethod.Invoke($null, @()) $script:PcSqlitePclInitialized = $true } [void][System.Reflection.Assembly]::LoadFrom((Join-Path $storageUtilsRoot 'Microsoft.Data.Sqlite.dll')) } function Get-SqliteVaultStorageFromHelper { param([Parameter(Mandatory = $true)][string] $ConnectionString, [Parameter(Mandatory = $true)][string] $OwnerUid) Initialize-SqliteStorageDependencies -ErrorContext 'Offline storage' $getConnection = New-SqliteIdbConnectionFunc -ConnectionString $ConnectionString $dialect = [KeeperSecurity.Storage.SqliteDialect]::Instance $vaultStorage = New-Object KeeperSecurity.Vault.SqlKeeperStorage($getConnection, $dialect, $OwnerUid) $verifyConn = New-Object Microsoft.Data.Sqlite.SqliteConnection($ConnectionString) $verifyConn.Open() try { $schemas = @($vaultStorage.GetStorages() | ForEach-Object { $_.Schema }) $failed = [KeeperSecurity.Storage.DatabaseUtils]::VerifyDatabase($verifyConn, $dialect, $schemas) if ($failed -and $failed.Count -gt 0) { [System.Diagnostics.Trace]::TraceError(($failed -join "`n")) } } finally { $verifyConn.Dispose() } return $vaultStorage } function Get-SqliteComplianceStorageFromHelper { param([Parameter(Mandatory = $true)][string] $ConnectionString) Initialize-SqliteStorageDependencies -ErrorContext 'Compliance storage' $getConnection = New-SqliteIdbConnectionFunc -ConnectionString $ConnectionString $dialect = [KeeperSecurity.Storage.SqliteDialect]::Instance $complianceStorage = New-Object KeeperSecurity.Compliance.SqlComplianceStorage($getConnection, $dialect) $verifyConn = New-Object Microsoft.Data.Sqlite.SqliteConnection($ConnectionString) $verifyConn.Open() try { [KeeperSecurity.Compliance.SqlComplianceStorage]::VerifyDatabase($verifyConn, $dialect) } finally { $verifyConn.Dispose() } return $complianceStorage } $expires = @( [KeeperSecurity.Authentication.TwoFactorDuration]::EveryLogin, [KeeperSecurity.Authentication.TwoFactorDuration]::Every30Days, [KeeperSecurity.Authentication.TwoFactorDuration]::Forever) function Test-InteractiveSession { if ($psISE) { return $true } if ($PSIsInteractive) { return $true } if ($PSPrivateMetadata.JobId) { return $false } return $Host.UI.SupportsVirtualTerminal } function twoFactorChannelToText ([KeeperSecurity.Authentication.TwoFactorChannel] $channel) { if ($channel -eq [KeeperSecurity.Authentication.TwoFactorChannel]::Authenticator) { return 'authenticator' } if ($channel -eq [KeeperSecurity.Authentication.TwoFactorChannel]::TextMessage) { return 'sms' } if ($channel -eq [KeeperSecurity.Authentication.TwoFactorChannel]::DuoSecurity) { return 'duo' } if ($channel -eq [KeeperSecurity.Authentication.TwoFactorChannel]::RSASecurID) { return 'rsa' } if ($channel -eq [KeeperSecurity.Authentication.TwoFactorChannel]::KeeperDNA) { return 'dna' } return '' } function deviceApprovalChannelToText ([KeeperSecurity.Authentication.DeviceApprovalChannel]$channel) { if ($channel -eq [KeeperSecurity.Authentication.DeviceApprovalChannel]::Email) { return 'email' } if ($channel -eq [KeeperSecurity.Authentication.DeviceApprovalChannel]::KeeperPush) { return 'keeper' } if ($channel -eq [KeeperSecurity.Authentication.DeviceApprovalChannel]::TwoFactorAuth) { return '2fa' } return '' } function twoFactorDurationToExpire ([KeeperSecurity.Authentication.TwoFactorDuration] $duration) { if ($duration -eq [KeeperSecurity.Authentication.TwoFactorDuration]::EveryLogin) { return 'now' } if ($duration -eq [KeeperSecurity.Authentication.TwoFactorDuration]::Forever) { return 'never' } if ($duration -eq [KeeperSecurity.Authentication.TwoFactorDuration]::Every30Days) { return '30_days' } return "---" } function getStepPrompt ([KeeperSecurity.Authentication.IAuthentication] $auth) { $prompt = "`nUnsupported ($($auth.step.State.ToString()))" if ($auth.step -is [KeeperSecurity.Authentication.Sync.DeviceApprovalStep]) { $prompt = "`nDevice Approval ($(deviceApprovalChannelToText $auth.step.DefaultChannel))" } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.TwoFactorStep]) { $channelText = twoFactorChannelToText $auth.step.DefaultChannel $prompt = "`n2FA channel($($channelText)) expire[$(twoFactorDurationToExpire $auth.step.Duration)]" } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.PasswordStep]) { $prompt = "`nMaster Password" } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.SsoTokenStep]) { $prompt = "`nSSO Token" } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.SsoDataKeyStep]) { $prompt = "`nSSO Login Approval" } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.ReadyToLoginStep]) { $prompt = "`nLogin" } return $prompt } function printStepHelp ([KeeperSecurity.Authentication.IAuthentication] $auth) { $commands = @() if ($auth.step -is [KeeperSecurity.Authentication.Sync.DeviceApprovalStep]) { $channels = @() foreach ($ch in $auth.step.Channels) { $channels += deviceApprovalChannelToText $ch } if ($channels) { $commands += "channel=<$($channels -join ' | ')> to change channel." } $commands += "`"push`" to send a push to the channel" $commands += '<code> to send a code to the channel' } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.TwoFactorStep]) { $channels = @() foreach ($ch in $auth.step.Channels) { $channelText = twoFactorChannelToText $ch if ($channelText) { $channels += $channelText } } if ($channels) { $commands += "channel=<$($channels -join ' | ')> to change channel." } $channels = @() foreach ($ch in $auth.step.Channels) { $pushes = $auth.step.GetChannelPushActions($ch) if ($null -ne $pushes) { foreach ($push in $pushes) { $channels += [KeeperSecurity.Authentication.AuthUIExtensions]::GetPushActionText($push) } } } if ($channels) { $commands += "`"$($channels -join ' | ')`" to send a push/code" } $channels = @() foreach ($exp in $expires) { $channels += twoFactorDurationToExpire $exp } $commands += "expire=<$($channels -join ' | ')> to set 2fa expiration." $commands += '<code> to send a 2fa code.' } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.PasswordStep]) { $commands += '<password> to send a master password.' } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.SsoTokenStep]) { $commands += $auth.step.SsoLoginUrl $commands += '' if (-not $auth.step.LoginAsProvider) { $commands += '"password" to login using master password.' } $commands += '<sso token> paste SSO login token.' } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.SsoDataKeyStep]) { $channels = @() foreach ($ch in $auth.step.Channels) { $channels += [KeeperSecurity.Authentication.AuthUIExtensions]::SsoDataKeyShareChannelText($ch) } if ($channels) { $commands += "`"$($channels -join ' | ')`" to request login approval" } } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.ReadyToLoginStep]) { $commands += '"login <Keeper Email>" login to Keeper as user' $commands += '"login_sso <Enterprise Domain>" login to Enterprise Domain' } if ($commands) { Write-Output "`nAvailable Commands`n" foreach ($command in $commands) { Write-Output $command } Write-Output '<Enter> to resume' } } function executeStepAction ([KeeperSecurity.Authentication.IAuthentication] $auth, [string] $action) { function tryExpireToTwoFactorDuration ([string] $expire, [ref] [KeeperSecurity.Authentication.TwoFactorDuration] $duration) { $result = $true if ($expire -eq 'now') { $duration.Value = [KeeperSecurity.Authentication.TwoFactorDuration]::EveryLogin } elseif ($expire -eq 'never') { $duration.Value = [KeeperSecurity.Authentication.TwoFactorDuration]::Forever } elseif ($expire -eq '30_days') { $duration.Value = [KeeperSecurity.Authentication.TwoFactorDuration]::Every30Days } else { $duration.Value = [KeeperSecurity.Authentication.TwoFactorDuration]::EveryLogin } return $result } function tryTextToDeviceApprovalChannel ([string] $text, [ref] [KeeperSecurity.Authentication.DeviceApprovalChannel] $channel) { $result = $true if ($text -eq 'email') { $channel.Value = [KeeperSecurity.Authentication.DeviceApprovalChannel]::Email } elseif ($text -eq 'keeper') { $channel.Value = [KeeperSecurity.Authentication.DeviceApprovalChannel]::KeeperPush } elseif ($text -eq '2fa') { $channel.Value = [KeeperSecurity.Authentication.DeviceApprovalChannel]::TwoFactorAuth } else { Write-Output 'Unsupported device approval channel:', $text $result = $false } return $result } function tryTextToTwoFactorChannel ([string] $text, [ref] [KeeperSecurity.Authentication.TwoFactorChannel] $channel) { $result = $true if ($text -eq 'authenticator') { $channel.Value = [KeeperSecurity.Authentication.TwoFactorChannel]::Authenticator } elseif ($text -eq 'sms') { $channel.Value = [KeeperSecurity.Authentication.TwoFactorChannel]::TextMessage } elseif ($text -eq 'duo') { $channel.Value = [KeeperSecurity.Authentication.TwoFactorChannel]::DuoSecurity } elseif ($text -eq 'rsa') { $channel.Value = [KeeperSecurity.Authentication.TwoFactorChannel]::RSASecurID } elseif ($text -eq 'dna') { $channel.Value = [KeeperSecurity.Authentication.TwoFactorChannel]::KeeperDNA } else { Write-Output 'Unsupported 2FA channel:', $text $result = $false } return $result } if ($auth.step -is [KeeperSecurity.Authentication.Sync.DeviceApprovalStep]) { if ($action -eq 'push') { $auth.step.SendPush($auth.step.DefaultChannel).GetAwaiter().GetResult() | Out-Null } elseif ($action -match 'channel\s*=\s*(.*)') { $ch = $Matches.1 [KeeperSecurity.Authentication.DeviceApprovalChannel]$cha = $auth.step.DefaultChannel if (tryTextToDeviceApprovalChannel ($ch) ([ref]$cha)) { $auth.step.DefaultChannel = $cha } } else { Try { $auth.step.SendCode($auth.step.DefaultChannel, $action).GetAwaiter().GetResult() | Out-Null } Catch [KeeperSecurity.Authentication.KeeperApiException] { Write-Warning $_ } Catch { Write-Error $_ } } } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.TwoFactorStep]) { if ($action -match 'channel\s*=\s*(.*)') { $ch = $Matches.1 [KeeperSecurity.Authentication.TwoFactorChannel]$cha = $auth.step.DefaultChannel if (tryTextToTwoFactorChannel($ch) ([ref]$cha)) { $auth.step.DefaultChannel = $cha } } elseif ($action -match 'expire\s*=\s*(.*)') { $exp = $Matches.1 [KeeperSecurity.Authentication.TwoFactorDuration]$dur = $auth.step.Duration if (tryExpireToTwoFactorDuration($exp) ([ref]$dur)) { $auth.step.Duration = $dur } } else { foreach ($cha in $auth.step.Channels) { $pushes = $auth.step.GetChannelPushActions($cha) if ($null -ne $pushes) { foreach ($push in $pushes) { if ($action -eq [KeeperSecurity.Authentication.AuthUIExtensions]::GetPushActionText($push)) { $auth.step.SendPush($push).GetAwaiter().GetResult() | Out-Null return } } } Try { $auth.step.SendCode($auth.step.DefaultChannel, $action).GetAwaiter().GetResult() | Out-Null } Catch { Write-Error $_ } } } } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.PasswordStep]) { Try { $auth.step.VerifyPassword($action).GetAwaiter().GetResult() | Out-Null } Catch [KeeperSecurity.Authentication.KeeperAuthFailed] { Write-Warning 'Invalid password' } Catch [KeeperSecurity.Authentication.KeeperApiException] { if ($_.Exception.Message -match 'Invalid|Credential|password|authentication failed' -or $_.Exception.Code -match 'invalid') { Write-Warning "Invalid credentials: $($_.Exception.Message)" } else { Write-Error $_ } } Catch { Write-Error $_ } } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.SsoTokenStep]) { if ($action -eq 'password') { $auth.step.LoginWithPassword().GetAwaiter().GetResult() | Out-Null } else { $auth.step.SetSsoToken($action).GetAwaiter().GetResult() | Out-Null } } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.SsoDataKeyStep]) { [KeeperSecurity.Authentication.DataKeyShareChannel]$channel = [KeeperSecurity.Authentication.DataKeyShareChannel]::KeeperPush if ([KeeperSecurity.Authentication.AuthUIExtensions]::TryParseDataKeyShareChannel($action, [ref]$channel)) { $auth.step.RequestDataKey($channel).GetAwaiter().GetResult() | Out-Null } } elseif ($auth.step -is [KeeperSecurity.Authentication.Sync.ReadyToLoginStep]) { if ($action -match '^login\s+(.*)$') { $username = $Matches.1 $auth.Login($username).GetAwaiter().GetResult() | Out-Null } elseif ($action -match '^login_sso\s+(.*)$') { $providerName = $Matches.1 $auth.LoginSso($providerName).GetAwaiter().GetResult() | Out-Null } } } function getConfigurationForDevice { param( [Parameter(Mandatory=$true)] [string] $Device, [Parameter()] [string] $Username, [Parameter()] [string] $Server ) $deviceToken = $Device.Trim() if (-not $deviceToken) { Write-Error "Device parameter requires a device_token value." -ErrorAction Stop } $configuration = New-Object KeeperSecurity.Configuration.KeeperConfiguration $configuration.LastLogin = $Username $configuration.LastServer = $Server $deviceConf = New-Object KeeperSecurity.Configuration.DeviceConfiguration $deviceToken if ($Server) { $deviceConf.ServerInfo.Put((New-Object KeeperSecurity.Configuration.DeviceServerConfiguration $Server)) } $configuration.Devices.Put($deviceConf) if ($Username) { $userConf = New-Object KeeperSecurity.Configuration.UserConfiguration $Username $userConf.Server = $Server $userConf.LastDevice = New-Object KeeperSecurity.Configuration.UserDeviceConfiguration $deviceToken $configuration.Users.Put($userConf) } if ($Server) { $configuration.Servers.Put((New-Object KeeperSecurity.Configuration.ServerConfiguration $Server)) } return New-Object KeeperSecurity.Configuration.InMemoryConfigurationStorage $configuration } function Connect-Keeper { <# .Synopsis Login to Keeper .Parameter Username User email .Parameter Password User password .Parameter NewLogin Do not resume the stored session (full login). When omitted, resume is also skipped if -Username differs from the stored LastLogin (switching accounts). .Parameter SsoPassword Use Master Password for SSO account .Parameter SsoProvider Login using SSO provider .Parameter Server Change default keeper server .Parameter Device Device token. When provided, skips loading configuration from file. .Parameter Config Config file name .Parameter UseOfflineStorage Use SQLite file for vault cache (persists between sessions). .Parameter VaultDatabasePath Path to the SQLite database file for vault storage. Default: keeper_db.sqlite in the same directory as the config file (or current directory). .Parameter SkipSync After a successful login, do not call SyncDown. The authenticated session and VaultOnline instance are available. The local vault stays empty until you run Sync-Keeper. AutoSync is disabled until then. .Parameter KeepAlive Enable automatic session keep-alive during login. .Parameter PushNotifications Enable push notifications during login. #> [CmdletBinding(DefaultParameterSetName = 'regular')] Param( [Parameter(Position = 0)][string] $Username, [Parameter()] [SecureString]$Password, [Parameter()][switch] $NewLogin, [Parameter(ParameterSetName = 'sso_password')][switch] $SsoPassword, [Parameter(ParameterSetName = 'sso_provider')][switch] $SsoProvider, [Parameter()][string] $Server, [Parameter()][string] $Device, [Parameter()][string] $Config, [Parameter()][switch] $UseOfflineStorage, [Parameter()][string] $VaultDatabasePath, [Parameter()][switch] $SkipSync, [Parameter()][switch] $KeepAlive, [Parameter()][switch] $PushNotifications ) Disconnect-Keeper -Resume | Out-Null $deviceTokenOnly = $false if ($Device) { if ($Config) { Write-Error "The Device and Config parameters cannot be used together." -ErrorAction Stop } $storage = getConfigurationForDevice -Device $Device -Username $Username -Server $Server $deviceTokenOnly = $true } elseif ($Config) { $storage = New-Object KeeperSecurity.Configuration.JsonConfigurationStorage $Config } else { $storage = New-Object KeeperSecurity.Configuration.JsonConfigurationStorage } $endpoint = New-Object KeeperSecurity.Authentication.KeeperEndpoint($storage, $Server) $endpoint.DeviceName = 'PowerShell Commander' $endpoint.ClientVersion = 'c18.0.0' Write-Information -MessageData "`nUsing Keeper Server: $($endpoint.Server)`n" -InformationAction Continue $authFlow = New-Object KeeperSecurity.Authentication.Sync.AuthSync($storage, $endpoint) $authFlow.AlternatePassword = $SsoPassword.IsPresent $authFlow.NoNewDevice = $deviceTokenOnly $authFlow.AutoKeepAlive = $KeepAlive.IsPresent $authFlow.UsePushNotifications = $PushNotifications.IsPresent if (-not $NewLogin.IsPresent -and -not $SsoProvider.IsPresent) { if (-not $Username) { $conf = $storage.Get() if ($conf.LastServer -eq $endpoint.Server) { $Username = $conf.LastLogin } } } $namePrompt = 'Keeper Username' if ($SsoProvider.IsPresent) { $namePrompt = 'Enterprise Domain' } if ($Username) { Write-Output "$(($namePrompt + ': ').PadLeft(21, ' ')) $Username" } elseif (Test-InteractiveSession) { while (-not $Username) { $Username = Read-Host -Prompt $namePrompt.PadLeft(20, ' ') } } else { Write-Error "Non-interactive session detected" -ErrorAction Stop } $canResume = -not ($NewLogin.IsPresent -or $Password) if ($canResume -and $PSBoundParameters.ContainsKey('Username')) { $cfgForResume = $storage.Get() if ($cfgForResume.LastLogin -and $Username -and [string]::Compare($Username, $cfgForResume.LastLogin, $true) -ne 0) { $canResume = $false Write-Verbose "Username differs from stored LastLogin; starting a new session (no resume)." } } $authFlow.ResumeSession = $canResume if ($deviceTokenOnly) { $authFlow.ResumeSession = $false } Write-Verbose "Resume Session: $($authFlow.ResumeSession)" $biometricPresent = $false try { $windowsHelloAvailable = Test-WindowsHelloCapabilities if ($windowsHelloAvailable) { $biometricPresent = Test-WindowsHelloBiometricPreviouslyUsed -Username $Username if (-not $biometricPresent) { Write-Debug "Windows Hello biometric authentication not available for this user" } } else { Write-Debug "Windows Hello not available on this system" } } catch { Write-Debug "Failed to check Windows Hello capabilities or biometric credential status: $($_.Exception.Message)" } if ($SsoProvider.IsPresent) { $authFlow.LoginSso($Username).GetAwaiter().GetResult() | Out-Null } else { $passwords = @() if ($Password) { if ($Password -is [SecureString]) { $passwords += [Net.NetworkCredential]::new('', $Password).Password } elseif ($Password -is [String]) { $passwords += $Password } } $authFlow.Login($Username, $passwords).GetAwaiter().GetResult() | Out-Null } while (-not $authFlow.IsCompleted) { if ($lastStep -ne $authFlow.Step.State) { if (-not $biometricPresent) { printStepHelp $authFlow } $lastStep = $authFlow.Step.State } if ($biometricPresent) { try { Write-Host "Attempting Keeper biometric authentication..." $biometricResult = Assert-KeeperBiometricCredential -AuthSyncObject $authFlow -Username $Username -PassThru if ($biometricResult.Success -and $biometricResult.IsValid) { $authFlow.ResumeLoginWithToken($biometricResult.EncryptedLoginToken).GetAwaiter().GetResult() | Out-Null if ($authFlow.IsCompleted) { Write-Debug "Authentication completed successfully!" break } Write-Debug "Biometric authentication succeeded, but additional authentication steps required" $biometricPresent = $false } else { $isCancelled = $biometricResult.ErrorMessage -match "cancelled|cancel" -or $biometricResult.ErrorType -eq "OperationCanceledException" if ($isCancelled) { Write-Host "Windows Hello authentication was cancelled. Falling back to default login method." -ForegroundColor Yellow } else { Write-Host "Biometric authentication failed, falling back to default login method. Device might not be registered" Write-Host "Please try running 'Set-KeeperDeviceSettings -Register' to register the device" } $biometricPresent = $false } } catch { Write-Host "Error logging in with biometric authentication, did you register the device using 'Set-KeeperDeviceSettings -Register'?" Write-Host "Biometric authentication error: $($_.Exception.Message), falling back to default input" $biometricPresent = $false } } $prompt = getStepPrompt $authFlow if ($authFlow.Step -is [KeeperSecurity.Authentication.Sync.PasswordStep]) { if (Test-InteractiveSession) { $securedPassword = Read-Host -Prompt $prompt -AsSecureString if ($securedPassword.Length -gt 0) { $action = [Net.NetworkCredential]::new('', $securedPassword).Password } else { $action = '' } } else { Write-Error "Non-interactive session detected" -ErrorAction Stop } } else { if (Test-InteractiveSession) { $action = Read-Host -Prompt $prompt } else { Write-Error "Non-interactive session detected" -ErrorAction Stop } } if ($action) { if ($action -eq '?') { } else { executeStepAction $authFlow $action } } } if ($authFlow.Step.State -ne [KeeperSecurity.Authentication.Sync.AuthState]::Connected) { if ($authFlow.Step -is [KeeperSecurity.Authentication.Sync.ErrorStep]) { Write-Warning $authFlow.Step.Message } return } $auth = $authFlow if ([KeeperSecurity.Authentication.AuthExtensions]::IsAuthenticated($auth)) { Write-Information -MessageData "Connected to Keeper as $($auth.Username)" -InformationAction Continue $vaultStorage = $null if ($UseOfflineStorage) { $ownerUid = [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($auth.AuthContext.AccountUid) if ($VaultDatabasePath) { $dbPath = $VaultDatabasePath } elseif ($Config) { $resolved = $null try { $resolved = Resolve-Path -LiteralPath $Config -ErrorAction Stop } catch { } $configDir = if ($resolved) { [System.IO.Path]::GetDirectoryName($resolved.Path) } else { [System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($Config)) } $dbPath = Join-Path $configDir 'keeper_db.sqlite' } else { $dbPath = Join-Path (Get-Location).Path 'keeper_db.sqlite' } $dbPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($dbPath) $connectionString = "Data Source=$dbPath;Pooling=True;" Write-Information -MessageData "Using vault database: $dbPath" $vaultStorage = Get-SqliteVaultStorageFromHelper -ConnectionString $connectionString -OwnerUid $ownerUid } $vault = New-Object KeeperSecurity.Vault.VaultOnline($auth, $vaultStorage) if ($SkipSync.IsPresent) { $vault.AutoSync = $false Write-Information -MessageData 'SkipSync: vault SyncDown skipped. Local folder tree and records are empty until you run Sync-Keeper.' -InformationAction Continue } else { $task = $vault.SyncDown() Write-Information -MessageData 'Syncing ...' -InformationAction Continue $task.GetAwaiter().GetResult() | Out-Null $vault.AutoSync = $true } $Script:Context.Auth = $auth $Script:Context.Vault = $vault [KeeperSecurity.Vault.VaultData]$vaultData = $vault Write-Information -MessageData "Decrypted $($vaultData.RecordCount) record(s)" -InformationAction Continue Set-KeeperLocation -Path '\' | Out-Null } } $Keeper_ConfigServerCompleter = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $prefixes = @('', 'dev.', 'qa.') $suffixes = $('.com', '.eu') $prefixes | ForEach-Object { $p = $_; $suffixes | ForEach-Object { $s = $_; "${p}keepersecurity${s}" } } | Where-Object { $_.StartsWith($wordToComplete) } } Register-ArgumentCompleter -Command Connect-Keeper -ParameterName Server -ScriptBlock $Keeper_ConfigServerCompleter New-Alias -Name kc -Value Connect-Keeper function Disconnect-Keeper { <# .Synopsis Logout from Keeper #> [CmdletBinding()] Param( [Parameter()][switch] $Resume ) $Script:Context.AvailableTeams = $null $Script:Context.AvailableUsers = $null $Script:Context.ManagedCompanyId = 0 $Script:Context.Enterprise = $null $vault = $Script:Context.Vault if ($vault) { $vault.Dispose() | Out-Null } $Script:Context.Vault = $null [KeeperSecurity.Authentication.IAuthentication] $auth = $Script:Context.Auth if ($auth) { if (-not $Resume.IsPresent) { $auth.Logout().GetAwaiter().GetResult() | Out-Null } $auth.Dispose() | Out-Null } $Script:Context.Auth = $null } New-Alias -Name kq -Value Disconnect-Keeper function Sync-Keeper { <# .SYNOPSIS Synchronizes local Keeper vault state with the server. .DESCRIPTION Downloads updates from the Keeper and refreshes the local vault cache. Optionally clears and re-syncs record types if the -SyncRecordTypes switch is used. .PARAMETER SyncRecordTypes If specified, clears locally cached record types and forces a fresh sync from the server. .EXAMPLE Sync-Keeper Performs a standard vault sync from the Keeper server. .EXAMPLE Sync-Keeper -SyncRecordTypes Clears local record type definitions and syncs them anew from the Keeper server. .NOTES Requires a valid Keeper session. Run `Connect-Keeper` or equivalent before using this command. #> [CmdletBinding()] param ( [Parameter()] [switch] $SyncRecordTypes ) $vault = $Script:Context.Vault if ($vault -is [KeeperSecurity.Vault.VaultOnline]) { if ($SyncRecordTypes) { $vault.Storage.Clear() Write-Host "Cleared local record type cache for re-sync." } Write-Host "Syncing vault with Keeper server..." $task = $vault.SyncDown() $task.GetAwaiter().GetResult() | Out-Null $vault.AutoSync = $true Write-Host "Vault sync completed." } else { Write-Error -Message "Not connected to a Keeper vault. Please authenticate first." -ErrorAction Stop } } New-Alias -Name ks -Value Sync-Keeper function Get-KeeperInformation { <# .Synopsis Prints account license information #> $vault = getVault [KeeperSecurity.Authentication.IAuthentication]$auth = $vault.Auth [KeeperSecurity.Authentication.AccountLicense]$license = $auth.AuthContext.License switch ($license.AccountType) { 0 { $accountType = $license.ProductTypeName } 1 { $accountType = 'Family Plan'} 2 { $accountType = 'Enterprise' } Default { $accountType = $license.ProductTypeName } } $accountType = 'Enterprise' [PSCustomObject]@{ PSTypeName = "KeeperSecurity.License.Info" User = $auth.Username Server = $auth.Endpoint.Server Admin = $auth.AuthContext.IsEnterpriseAdmin AccountType = $accountType RenewalDate = $license.ExpirationDate StorageCapacity = [int] [Math]::Truncate($license.BytesTotal / (1024 * 1024 * 1024)) StorageUsage = $license.StorageUsagePercent StorageExpires = $license.StorageExpirationDate } if ($license.AccountType -eq 2) { $enterprise = getEnterprise if ($enterprise) { $enterpriseLicense = $enterprise.enterpriseData.EnterpriseLicense $productTypeId = $enterpriseLicense.ProductTypeId if ($productTypeId -in @(2, 5)) { $tier = $enterpriseLicense.Tier if ($tier -eq 1) { $plan = 'Enterprise' } else { $plan = 'Business' } } elseif ($productTypeId -in @(9, 10)) { $distributor = $enterpriseLicense.Distributor if ($distributor -eq $true) { $plan = 'Distributor' } else { $plan = 'Managed MSP' } } elseif ($productTypeId -in @(11, 12)) { $plan = 'Keeper MSP' } elseif ($productTypeId -eq 8) { $tier = $enterpriseLicense.Tier if ($tier -eq 1) { $plan = 'Enterprise' } else { $plan = 'Business' } $plan = "MC $plan" } else { $plan = 'Unknown' } if ($productTypeId -in @(5, 10, 12)) { $plan = "$plan Trial" } $enterpriseInfo = [PSCustomObject]@{ PSTypeName = "KeeperSecurity.License.EnterpriseInfo" LicenseType = 'Enterprise' EnterpriseName = $enterprise.loader.EnterpriseName BasePlan = $plan } if ($enterpriseLicense.Paid) { $expiration = $enterpriseLicense.Expiration if ($expiration -gt 0) { $exp = [KeeperSecurity.Utils.DateTimeOffsetExtensions]::FromUnixTimeMilliseconds($expiration) $expDate = $exp.ToString('d') Add-Member -InputObject $enterpriseInfo -MemberType NoteProperty -Name 'Expires' -Value $expDate } switch ($enterpriseLicense.filePlanTypeId) { -1 { $filePlan = 'No Storage' } 0 { $filePlan = 'Trial' } 1 { $filePlan = '1GB' } 2 { $filePlan = '10GB' } 3 { $filePlan = '50GB' } 4 { $filePlan = '100GB' } 5 { $filePlan = '250GB' } 6 { $filePlan = '500GB' } 7 { $filePlan = '1TB' } 8 { $filePlan = '10TB' } Default { $filePlan = '???' } } Add-Member -InputObject $enterpriseInfo -MemberType NoteProperty -Name 'StorageCapacity' -Value $filePlan $numberOfSeats = $enterpriseLicense.NumberOfSeats if ($numberOfSeats -gt 0) { Add-Member -InputObject $enterpriseInfo -MemberType NoteProperty -Name 'TotalUsers' -Value $numberOfSeats } $seatsAllocated = $enterpriseLicense.SeatsAllocated if ($seatsAllocated -gt 0) { Add-Member -InputObject $enterpriseInfo -MemberType NoteProperty -Name 'ActiveUsers' -Value $seatsAllocated } $seatsPending = $enterpriseLicense.SeatsPending if ($seatsAllocated -gt 0) { Add-Member -InputObject $enterpriseInfo -MemberType NoteProperty -Name 'InvitedUsers' -Value $SeatsPending } } $enterpriseInfo } } } New-Alias -Name kwhoami -Value Get-KeeperInformation function compareArrays { param ($array1, $array2) if ($array1.Length -eq $array2.Length) { foreach ($i in 0..($array1.Length-1)) { if ($array1[$i] -ne $array2[$i]) { return $false } } return $true } return $false } function formatTimeout { param ($timeout) if ($timeout -gt 0) { $dayMillis = [TimeSpan]::FromDays(1).TotalMilliseconds if ($logoutTimer -gt $dayMillis) { return "$([Math]::Round($logoutTimer / $dayMillis)) day(s)" } $hourMillis = [TimeSpan]::FromHours(1).TotalMilliseconds if ($logoutTimer -gt $hourMillis) { return "$([Math]::Round($logoutTimer / $hourMillis)) hour(s)" } $minuteMillis = [TimeSpan]::FromMinutes(1).TotalMilliseconds return "$([Math]::Round($logoutTimer / $minuteMillis)) minute(s)" } } function Get-KeeperDeviceSettings { <# .SYNOPSIS Display settings of the current device .PARAMETER Auth Optional authentication object. If not provided, uses the current vault's auth. #> [CmdletBinding()] Param ( [Parameter()][object] $Auth ) if (-not $Auth) { $vault = getVault $auth = $vault.Auth } else { $auth = $Auth } $accountSummary = [KeeperSecurity.Authentication.AuthExtensions]::LoadAccountSummary($auth).GetAwaiter().GetResult() $device = $accountSummary.Devices | Where-Object { compareArrays $_.EncryptedDeviceToken $auth.DeviceToken } | Select-Object -First 1 if (-not $device) { Write-Error -Message "The current device could not be found" -ErrorAction Stop } $logoutTimer = $accountSummary.Settings.LogoutTimer if ($logoutTimer -gt 0) { $logoutTimerText = formatTimeout $logoutTimer } else { $logoutTimerText = '1 hour(s)' } $persistentLoginRestricted = $false if ($accountSummary.Enforcements.Booleans) { $plp = $accountSummary.Enforcements.Booleans | Where-Object { $_.Key -eq 'restrict_persistent_login' } | Select-Object -First 1 if ($plp) { $persistentLoginRestricted = $plp.Value } } $persistentLoginEnabled = $false if (-not $persistentLoginRestricted) { $persistentLoginEnabled = $accountSummary.Settings.PersistentLogin } $settings = [PSCustomObject]@{ PSTypeName = "KeeperSecurity.Authentication.DeviceInfo" DeviceName = $device.DeviceName PersistentLogin = $persistentLoginEnabled DataKeyPresent = $device.EncryptedDataKeyPresent IpAutoApprove = -not $accountSummary.Settings.IpDisableAutoApprove IsSsoUser = $accountSummary.Settings.SsoUser DeviceLogoutTimeout = $logoutTimerText } if ($accountSummary.Enforcements.Longs) { $enf = $accountSummary.Enforcements.Longs | Where-Object { $_.Key -eq 'logout_timer_desktop' } | Select-Object -First 1 if ($enf.Length -eq 1) { $entLogoutTimer = $enf.Value if ($entLogoutTimer -gt 0) { $entLogoutTimerText = formatTimeout $entLogoutTimer Add-Member -InputObject $settings -MemberType NoteProperty -Name 'EnterpriseLogoutTimeout' -Value $entLogoutTimerText } } } $settings } function Set-KeeperDeviceSettings { <# .SYNOPSIS Modifies the current device settings .PARAMETER NewName Modifies device name .PARAMETER Timeout Sets inactivity timeout. Format: NUMBER[h|d] default - minutes, h - hours, d - days .PARAMETER Register Register current device for Persistent Login .PARAMETER PersistentLogin Enables or disables Persistent login for account ON | OFF .PARAMETER IpAutoApprove Enables or disables Automatic Approval by IP address for account ON | OFF .EXAMPLE C:\PS> Set-KeeperDeviceSettings -NewName 'Azure' -Timeout 30d -PersistentLogin ON -Register #> [CmdletBinding()] Param ( [Parameter()][String] $NewName, [Parameter(HelpMessage='NUMBER[h|d]')][String] $Timeout, [Parameter()][Switch] $Register, [Parameter()][ValidateSet('ON', 'OFF')][String] $PersistentLogin, [Parameter()][ValidateSet('ON', 'OFF')][String] $IpAutoApprove ) $vault = getVault $auth = $vault.Auth $accountSummary = [KeeperSecurity.Authentication.AuthExtensions]::LoadAccountSummary($auth).GetAwaiter().GetResult() $device = $accountSummary.Devices | Where-Object { compareArrays $_.EncryptedDeviceToken $auth.DeviceToken } | Select-Object -First 1 if (-not $device) { Write-Error -Message "The current device could not be found" -ErrorAction Stop } $changed = $false if ($NewName) { $request = New-Object Authentication.DeviceUpdateRequest $request.ClientVersion = $auth.Endpoint.ClientVersion $request.DeviceStatus = [Authentication.DeviceStatus]::DeviceOk $request.DeviceName = $NewName $request.EncryptedDeviceToken = $device.EncryptedDeviceToken $auth.ExecuteAuthRest("authentication/update_device", $request, $null, 0).GetAwaiter().GetResult() | Out-Null Write-Information "Device name was changed to `"$NewName`"" $changed = $true } $persistentLoginRestricted = $false if ($accountSummary.Enforcements.Booleans) { $plp = $accountSummary.Enforcements.Booleans | Where-Object { $_.Key -eq 'restrict_persistent_login' } | Select-Object -First 1 if ($plp) { $persistentLoginRestricted = $plp.Value } } if ($Register.IsPresent) { $registered = [KeeperSecurity.Authentication.AuthExtensions]::RegisterDataKeyForDevice($auth, $device).GetAwaiter().GetResult() if ($registered) { Write-Information "Device is registered for Persistent Login" } $changed = $true } if ($PersistentLogin) { if ($persistentLoginRestricted -eq $true) { Write-Error "Persistent Login feature is restricted by Enterprise Administrator" -ErrorAction Stop } $value = '0' if ($PersistentLogin -eq 'ON') { $value = '1' } [KeeperSecurity.Authentication.AuthExtensions]::SetSessionParameter($auth, 'persistent_login', $value).GetAwaiter().GetResult() | Out-Null $changed = $true } if ($IpAutoApprove) { $value = '1' if ($IpAutoApprove -eq 'ON') { $value = '0' } [KeeperSecurity.Authentication.AuthExtensions]::SetSessionParameter($auth, 'ip_disable_auto_approve', $value).GetAwaiter().GetResult() | Out-Null $changed = $true } if ($Timeout) { $lastLetter = $Timeout[-1] if ($lastLetter -eq 'd') { $timeoutInt = $Timeout.Substring(0, $Timeout.Length - 1) } elseif ($lastLetter -eq 'h') { $timeoutInt = $Timeout.Substring(0, $Timeout.Length - 1) } else { $lastLetter = '' $timeoutInt = $Timeout } $minutes = $null $b = [int]::TryParse($timeoutInt, [ref]$minutes) if (-not $b) { Write-Error "Invalid timeout value `"$Timeout`". Format NUMBER[h|d]. d-days, h-hours. default minutes " -ErrorAction Stop } if ($lastLetter -eq 'h') { $minutes = $minutes * 60 } elseif ($lastLetter -eq 'd') { $minutes = $minutes * (60 * 24) } [KeeperSecurity.Authentication.AuthExtensions]::SetSessionInactivityTimeout($auth, $minutes).GetAwaiter().GetResult() | Out-Null $changed = $true } if (-not $changed) { Get-KeeperDeviceSettings } } New-Alias -Name this-device -Value Set-KeeperDeviceSettings # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC11d1fIpkMlRMy # oZ3Cy3OA4dEGr31MPbqLKmTBxiAFJaCCITswggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4 # RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg # MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit # eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS # 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM # swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC # Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3 # /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j # q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5 # OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo # 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU # tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm # KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP # TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq # hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK # r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda # qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+ # lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a # brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS # y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK # iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb # KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q # xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm # zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn # HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w # gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1 # c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo # dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi # 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg # xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF # cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ # m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS # GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1 # ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9 # MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7 # Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG # RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6 # X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd # BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx # XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF # BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln # aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j # b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo # dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy # bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL # BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj # aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0 # hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0 # F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT # mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf # ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE # wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh # OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX # gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO # LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG # WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg # AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG # EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0 # IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex # MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx # FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy # NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI # hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3 # zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch # TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj # FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo # yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP # KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS # uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w # JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW # doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg # rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K # 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf # gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy # Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL # TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG # AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j # b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy # dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j # cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB # CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ # D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/ # ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu # +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o # bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h # ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn # M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol # /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY # xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc # CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB # ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx # oAMCAQICEAHdzU+FVN9jCMv0HhHagNUwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE # BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy # dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB # MTAeFw0yNjA2MDUwMDAwMDBaFw0yNzA2MDQyMzU5NTlaMIHRMRMwEQYLKwYBBAGC # NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ # cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC # VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK # ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5 # IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCb4DRTV0sNQsa1 # 0YRh+bliabmLOVYr6S0+BSVvRJAN3SHP6x52i1Dkpki5xVDIH06ZnnsToVrgvTv+ # QxGwsn9SAPHEZ/PIJRFxbMR4ShDaptYyL4f0u4k/3HwRzIleWE4mTUonYH8BdgLw # /F53B7wa7VTDHtxXltYTibEOwJxYCOi4Zr2FYQhjw14/CHcqS3FSMs6YYU2T56+g # w819hQM3K0YlwTNOFoIm1v7/ZZZiJGH8uGDsvy1makh1Xyyo/wN8EbQ1nbslmePT # roPm9w7WqiP/yiq+CZHiuTk9JK5bEgkWG3ns+v25cI251WidJx3SU7IZnX0OTd6/ # ZdKhprD5Gcfy5GBbJdcYw2WycQRW0PT5BEt55xRE0heufkpDaTUN6RdOuJdXbkl0 # hV91IZIuhueEMCk3h5mDTlU5gImxqj0R/TbAxjSSGTKCeuYFkQIRqytSabdrZZ48 # kW5hOIZMVDY1f4kpPJa8UeEvDZXT3vrtj36aSJrwez2uh4FMNlkCAwEAAaOCAgIw # ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBT1 # SmCYU/7Yrz1fX66Ur5nSzlSYOzA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p # bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT # QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA # A4ICAQBcavcUHNFEg872HDRq2+hRlnvaghCXv7X/6h9HSzjAQP3rt95BZty3ASqi # 2MYyGQLGdDl4DToe/WhajtEOBOYa83agW6tBvrfcKRrDrwJOMPTbwNYvn+GuiL4T # CKzXaytWiJJbrc5odc7Ecat2ZvJylpPmNainr4Q0LzzH23Gea/Mm/hIJTN4IGgrH # hrXiTIIW/ZUzrY6g8b3RZB4BA497n43wNdSqP+C3ntFw6NiGB4Z25SW4YntIxYPv # Kf37OVhF0xqxLC1sK/XxgK0EGQ6iaj8Ncpr2C5vSNZqfW2MndxOA1W67pgDpg83k # UWG+/YJeGhqOTF82/0kIzQXeI/lIqbnL/IJAJqSm/ROSpsGUKVbzk03cpTD55ZQX # WjM0fLirypBqY05T8gnh1L0fSwxr/SwJZ8OddivgyK1YOMn02nnsEG5kxBt9cMX4 # JCYABhypmAVDRvyYifEVdoFWv2gAXXW+PPRvlNa6E4aMCZrVcoKHiyeMAXOi1IC9 # mHvC2+foTSMFueq3AdnYfeKnZnAiKXKRhXcdHbQYcR2A7AIzIcqahPYr4FNEgb/E # /y/kypAkf0rMHlYl1kNqLs2Nv1UnMEHYT5YmDVLO63+1Trcw4zTZ70zuqIqeID/d # nbOlgtyG6DSRCL7f0E7kP18f4RoX5i1PkfeO4VJHsAuCeNG1qjGCBdkwggXVAgEB # MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD # VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI # QTM4NCAyMDIxIENBMQIQAd3NT4VU32MIy/QeEdqA1TANBglghkgBZQMEAgEFAKCB # hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE # AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ # BDEiBCCP2ujh3I0W5oQGgbfFtR154Fzw7GZn3Zwa3Dn4DHMWZDANBgkqhkiG9w0B # AQEFAASCAYAlPlrHgdZXCiOa0Ylz4QTpxWJmOMBjoB+fHPDOhRe/KK65iqTNHFJ8 # nSCK564XkHoRi02r47nSZeq565b1Rsp3S1B8vtQdL7jr4BbQLn2gaGYli35ykcKC # ku6KU36d7C0IkLoZ1xUnhnnlbK4yX5FCmahKj4wrQ1wb0t/lZ0IdAbpDyphEDFwN # 1RMC5I3OVFZURbN/zvvTsjLoYVSHAyrkDCQGrzcSAVEpfDfyBh7baXVzPibX1cjE # u4oCkpNbLDNwoKKZLS8F7wt1Y3mQzxUtkJBrWZrjV3L8Cd5Sujo2Ot1SDrRYzkiB # nLqcvhkE2TsiFyb2wabyVN0c+oJwFpSrRMX2robZIPficWlSU7zwJIagb3zLR7N7 # lCplvU09lKeHK2VBXcHzon+n9WHEto+hoCHuEsmr409iCqakQV2AnyLY0r8C0B2B # 6rXZEpR23xm4N0dmuimjEp34cJ2YFRAkAbvQXxdP1FxZ2ZV5qyM40Dp2RbUYJAmU # GqQ3dDtJIzmhggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNjEzMDA1NjI3WjAv # BgkqhkiG9w0BCQQxIgQgD9vMEQQRgXBOtJHVX9oqIfroEu6nGE/otvU4Xwx48UUw # DQYJKoZIhvcNAQEBBQAEggIAeh5qBuZSNOtPgecyX4nuRGpkh36vKYO+Py6lyX9W # mHqWNp103bffe18ZZOAQSDjZlOoJWXwsF1HKKDQf6umV02RuUOtHp5rkS5Wsy+1u # soQnjmcR3gcudWN9YuvLCeuYL0YKNVhqobh7xNLN/6qFiY4JM1lhNjpqGL1qchls # n98bjgvUP7FJjK6SNaah2z/iHW+7nDubIeTW56vPcgF3jecFU0Vx+J9ZoTVJQABf # PIVsrRCDjFn02o4r891OxZQ31XkCpp8Buk9orNu/xSTqBYUccK4+L/+qquJGDQyb # GarL4Xjrg4YHKY0j8jVrAN4Ex4ItzwJMLXD5YB+/avWpJObSbQtfdURgHsG0dBhA # t8jCNLdpXnj1gCX/qWYEeVn/iG7/C16GyLlB8yQBl/QywvN1wIiqPqRkQSeSpRu3 # kdlBKqpBt/LW0PWIgdpa052DZmc8fCJTBzJ1xMkBXIGLcA4O39IXsED0AECv4Mgi # 8cheDUy9zmUtpi4S+HEvLACOt40knbk/QjWIYd0cBSe/OE/ktW7ELe1tj1bBCIWZ # vTTSST9lurDmde3lmo2i1OgVISrJEApvDcI0SURWqSPBD1rC/9rAhzfN/YUW5pJf # Fg0UQ7wBHkDnUO4P3YHjx1Q3jMXM56Xb6vzX6ui44ntx1VCsdNNy9r+00YXUIyFl # ZL0= # SIG # End signature block |