AuthCommands.ps1

#requires -Version 5.1

$expires = @(
    [KeeperSecurity.Authentication.TwoFactorDuration]::EveryLogin,
    [KeeperSecurity.Authentication.TwoFactorDuration]::Every30Days,
    [KeeperSecurity.Authentication.TwoFactorDuration]::Forever)

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'
    }
    return "$([int]$duration)_days"
}


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 {
            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 Connect-Keeper {
    <#
    .Synopsis
    Login to Keeper
 
   .Parameter Username
    User email
 
    .Parameter NewLogin
    Do not use Last Login information
 
    .Parameter SsoPassword
    Use Master Password for SSO account
 
    .Parameter Server
    Change default keeper server
#>

    [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
    )

    Disconnect-Keeper -Resume | Out-Null

    $storage = New-Object KeeperSecurity.Configuration.JsonConfigurationStorage
    if (-not $Server) {
        $Server = $storage.LastServer
        if ($Server) {
            Write-Information -MessageData "`nUsing Keeper Server: $Server`n"
        }
        else {
            Write-Information -MessageData "`nUsing Default Keeper Server: $([KeeperSecurity.Authentication.KeeperEndpoint]::DefaultKeeperServer)`n"
        }
    }


    $endpoint = New-Object KeeperSecurity.Authentication.KeeperEndpoint($Server, $storage.Servers)
    $endpoint.DeviceName = 'PowerShell Commander'
    $endpoint.ClientVersion = 'c16.1.0'
    $authFlow = New-Object KeeperSecurity.Authentication.Sync.AuthSync($storage, $endpoint)

    $authFlow.ResumeSession = $true
    $authFlow.AlternatePassword = $SsoPassword.IsPresent

    if (-not $NewLogin.IsPresent -and -not $SsoProvider.IsPresent) {
        if (-not $Username) {
            $Username = $storage.LastLogin
        }
    }

    $namePrompt = 'Keeper Username'
    if ($SsoProvider.IsPresent) {
        $namePrompt = 'Enterprise Domain'
    }

    if ($Username) {
        Write-Output "$(($namePrompt + ': ').PadLeft(21, ' ')) $Username"
    }
    else {
        while (-not $Username) {
            $Username = Read-Host -Prompt $namePrompt.PadLeft(20, ' ')
        }
    }
    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
    }
    Write-Output ""
    while (-not $authFlow.IsCompleted) {
        if ($lastStep -ne $authFlow.Step.State) {
            printStepHelp $authFlow
            $lastStep = $authFlow.Step.State
        }

        $prompt = getStepPrompt $authFlow

        if ($authFlow.Step -is [KeeperSecurity.Authentication.Sync.PasswordStep]) {
            $securedPassword = Read-Host -Prompt $prompt -AsSecureString
            if ($securedPassword.Length -gt 0) {
                $action = [Net.NetworkCredential]::new('', $securedPassword).Password
            }
            else {
                $action = ''
            }
        }
        else {
            $action = Read-Host -Prompt $prompt
        }

        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-Debug -Message "Connected to Keeper as $Username"

        $vault = New-Object KeeperSecurity.Vault.VaultOnline($auth)
        $task = $vault.SyncDown()
        Write-Information -MessageData 'Syncing ...'
        $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)"
        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
    Sync down with Keeper
#>


    [CmdletBinding()]
    [KeeperSecurity.Vault.VaultOnline]$vault = $Script:Context.Vault
    if ($vault) {
        $task = $vault.SyncDown()
        $task.GetAwaiter().GetResult() | Out-Null
    }
    else {
        Write-Error -Message "Not connected" -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 = [int] [Math]::Truncate($license.BytesUsed * 100 / $license.BytesTotal)
        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

# SIG # Begin signature block
# MIIR1wYJKoZIhvcNAQcCoIIRyDCCEcQCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU5596SoFS3rI1BiU+ehsFWSxa
# C0Gggg4jMIIGsDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0B
# AQwFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVk
# IFJvb3QgRzQwHhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEg
# Q0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5
# WRuxiEL1M4zrPYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJP
# DqFX/IiZwZHMgQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXz
# ENOLsvsI8IrgnQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bq
# HPNlaJGiTUyCEUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTC
# fMjqGzLmysL0p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaD
# G7dqZy3SvUQakhCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urO
# kfW+0/tvk2E0XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7AD
# K5GyNnm+960IHnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4
# R+Z1MI3sMJN2FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlN
# Wdt4z4FKPkBHX8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0I
# U0F8WD1Hs/q27IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYB
# Af8CAQAwHQYDVR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaA
# FOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAK
# BggrBgEFBQcDAzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9v
# Y3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4
# oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJv
# b3RHNC5jcmwwHAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcN
# AQEMBQADggIBADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcT
# Ep6QRJ9L/Z6jfCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WT
# auPrINHVUHmImoqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9
# ntSZz0rdKOtfJqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np37
# 5SFTWsPK6Wrxoj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0
# HKKlS43Nb3Y3LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL
# 6TEa/y4ZXDlx4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+1
# 6oh7cGvmoLr9Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8
# M4+uKIw8y4+ICw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrF
# hsP2JjMMB0ug0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy
# 1lKQ/a+FSCH5Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIHazCC
# BVOgAwIBAgIQAnNTGQOIer82vZ1cJyDJDjANBgkqhkiG9w0BAQsFADBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEg
# Q0ExMB4XDTIyMDIwMjAwMDAwMFoXDTI1MDIwMTIzNTk1OVowcDELMAkGA1UEBhMC
# VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK
# ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5
# IEluYy4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDNgTqmksdjUyKF
# 5zWkDyghf0PLWJWdzG0TX2j8B4J55xwt+B17zd4Xc3n0dvmSVAyPQANeN+mP1chf
# 4LTRn9h4jWb8Jsfn+JzyRhj/gYINYvBnpRpqoM0z7QC9Ebwj5T61Cogm9EKGcrG+
# Ujh+Z7pTqfSUrHD8NMXhDL/UpVn+w0Pb4qg7o7AH2o94n7u/qTlMGZCs+VCAvhNr
# wPABxvFY07YGb9t5/IZlPE8vG3p1vw2SbgREgFWSEQFj6X2CIhSrbiFCW/766/Mq
# EX6qm+RyF71fD4d3yShg39guaE9o+TBl1MqVCje4bK/wGoNxCho0I6Z1fBBKloyp
# vlx3gPpU7tJJ+KpuIiel9R9dGQuscqKzehPtbRc9Abr9ThN/HrLg1sFFVMdn2oMR
# 63QCUdz+B1NuS7Ap8Ti7XvAPJHzEuQDcdMcRbkIfllJVqrb9UXEFwOPzvRU2KrcQ
# 42Jlnn4T+WenPx5Nr3o/o08WLhLTicEK1OacEowyRLBmih4Gxpdk3fUAVCEkdvmq
# TSydQpl1Bk8V88dxCkB1wMZyFYLNcddBL4kUbwjso/z6f2TtfAVYs/iIRWqs7Xqt
# 4F2BBqobOGMymwg6VgVjjzDIgJCZSbjpq2IoVTci5vli6vxgSoZ01fccSaKa4Izm
# B7DbobIkIjLgPqpnCkqlHuJj5hQ9twIDAQABo4ICBjCCAgIwHwYDVR0jBBgwFoAU
# aDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYEFCZd3/KEdT2t5WTIFb3TUaM4
# sTikMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzCBtQYDVR0f
# BIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDBToFGg
# T4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29k
# ZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwPgYDVR0gBDcwNTAzBgZn
# gQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BT
# MIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIx
# Q0ExLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQAGyDM3Cbxq
# Auhr8O2xwOoCSVKmFkXqicwlrugwLW44Y4WX+imvTrGfjj2S99k/4D5H8DgtW/u8
# tOxcCoehTOCIEwP5TLrieHppsqAR4jaJRcdAHOWiJ1bmwQBv/cBU9vaelL0oXxxf
# TwD9oDaQNuyq6p+nIJMqbKv33b8AWGe3zq4JwblaFjRDL5lUDNhPx3g/pm7JhnbX
# 7QTKydAJvpbuP5cqUH1GEeVMjc5vEELtGNy/fy7Ekm4dndX4IZcFXW5L0Lx8cReB
# hIZwA+pzdzTWQYvfxgRMb/j2uY+Tkb6Wz2x9BBS1UXiP2qrs3rhQv8DZRkUSqnko
# YD4uJP8gk8BXcIXIThgEF2YCq2hBiwna5Ijbwkmjn1lWwGv15SznTOTnrVApJqB1
# tB2s2ovUNV4CyKDPVr+9/CS6IQJfEZeHYcYLsIga2q5NZCrqZAasBfCwALVkALos
# DIWhs33vYLfETMSuk5Hd5JC+hLjVM3ZJwslvnc/wec2r0GNAiZ3a1aweC7NYuzRz
# 29Mi/eR/4ylmCltyZqYJ1JcC/g6eY2Q0xkdWc8P0yHfQ/3fe7+AKXXKNjfv858GW
# lg1Ck2lvwPdLqJWqj1FwJPiGRCB+WulPe0csTyWnf+ed45TXx69tZ6BZr0Xr2jXu
# ybBdJtg0NN0a62xxWrmX42CgsrzHzRm7OzGCAx4wggMaAgEBMH0waTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MQIQAnNTGQOIer82vZ1cJyDJDjAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEK
# MAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3
# AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUr6+wO9XX7Mhf9MzB
# n7zQAU9HH7swDQYJKoZIhvcNAQEBBQAEggIAWwTiFJK3ZrBTk3h/sm1MRYiaa6UK
# usYzUnxNXzc+YCnZbB3D8Y7MaqQnzzkx71JwfPei3v4RMGcXEsqQHi7OJu205cEy
# oRc9VQeCBoiIyUMsKT2EXwESO1K2GICKFOH/rfM+xALN+gFVRsxHVbF/CAyxwKMk
# AO6eBTOKuoenicJ8ws266MJaXFLoDZcalc3la6con2siFrXPq1oW7kkuAOXsuRoW
# PR9QxSEEygkACJUmDtyptrlRyoeYyA5wQNui5WZtawXlLMJCerc//Sn5IfdkHwJ8
# T151uQsN8sugzlVA+t1L0h0iAWobaUWObO+WEoQFOoXynxpuj1oixhNX2bSdQAUi
# OXCED4P5tN5NXW1dd5zhPzxwt1naaClUvsxCWlvSbAyxs2QMjMMmnkfZhW15ari/
# Y6Q9ZjYXEPcpprD+OCT6gEtkynOm4/s/MWuC4LtM9327c3fhh+WpF0occwfQEieY
# +FHj46dRvx79oxwIO+gtOFPI4m4bMcoMlL2a7G19L+Kr2OuV9jccAvxy0rhlLrEU
# ms1sQjDsdq8Q03jKVR2HJudenlJa5UGOOl3opiLeg/dGpPEpNmJ5RfQgeUoZ3oFn
# Q4/BLKod+GzL36azSa26o9YPW1fkON3SLtLMWv0uGAqps/1qvbmGpASvmV8hc4CA
# HtMX2gN7UoaYlsc=
# SIG # End signature block