BreachWatch.ps1

#requires -Version 5.1

function Get-KeeperBreachWatchList {
    <#
    .SYNOPSIS
    Lists Keeper records flagged by BreachWatch that require attention (excludes ignored and good records).

    .DESCRIPTION
    Retrieves Keeper records in your vault that have been flagged by BreachWatch and require attention, including weak, breached, and changed passwords.
    This excludes records marked as "Good" or "Ignore" status.
    You can filter to only show records you own, include all records, and display results with numbering.

    .PARAMETER OwnedOnly
    Show only records owned by you.

    .PARAMETER All
    Show all BreachWatch-flagged records, even if there are more than 32 (default output is limited for readability).

    .PARAMETER Numbered
    Display a serial number column in the output.

   .EXAMPLE
    Get-KeeperBreachWatchList
    Lists up to 32 records in your vault that require BreachWatch attention (weak, breached, or changed passwords).

    .EXAMPLE
    Get-KeeperBreachWatchList -OwnedOnly
    Lists only the records you own that require BreachWatch attention.

    .EXAMPLE
    Get-KeeperBreachWatchList -All -Numbered
    Lists all BreachWatch-flagged records with a serial number column.

    .NOTES
    This function helps you quickly identify and review records in your Keeper vault that require attention due to password issues (weak, breached, or changed passwords).
    Records with "Good" or "Ignore" status are excluded from the results.
    #>

    [CmdletBinding()]
    param(
        [Parameter()][Switch]$OwnedOnly,
        [Parameter()][Switch]$All,
        [Parameter()][Switch]$Numbered,
        [Parameter()][string]$VaultContextVar = "Global:KeeperVaultContext"
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault

    $recordUids = $vault.BreachWatchRecords() |
        Where-Object { $_.Status -ne "Ignore" -and $_.Status -ne "Good" } |
        Select-Object -ExpandProperty RecordUid

    $records = $vault.KeeperRecords |
        Where-Object {
            $recordUids -contains $_.Uid -and
            (-not $OwnedOnly -or $_.Owner)
        }

    if ($records.Count -gt 0) {
        $table = New-Object System.Collections.Generic.List[object]
        $index = 1

        foreach ($r in $records | Sort-Object Title) {
            $row = if ($Numbered) {
                [PSCustomObject]@{
                    "S.No"       = $index++
                    "Record UID" = $r.Uid
                    "Title"      = $r.Title
                    "Description"= [KeeperSecurity.Utils.RecordTypesUtils]::KeeperRecordPublicInformation($r)
                }
            } else {
                [PSCustomObject]@{
                    "Record UID" = $r.Uid
                    "Title"      = $r.Title
                    "Description"= [KeeperSecurity.Utils.RecordTypesUtils]::KeeperRecordPublicInformation($r)
                }
            }
            $table.Add($row)
        }

        $total = $table.Count
        if (-not $All.IsPresent -and $total -gt 32) {
            $table = $table[0..29]
        }

        $table | Format-Table -AutoSize

        if ($table.Count -lt $total) {
            Write-Host ""
            Write-Host "$($total - $table.Count) records skipped."
        }
    } else {
        Write-Host "No BreachWatch issues detected (excluding ignored records)"
    }

    $scannedUids = $vault.BreachWatchRecords() | Select-Object -ExpandProperty RecordUid
    $notScanned = $vault.KeeperRecords |
        Where-Object { $_.Owner -and ($scannedUids -notcontains $_.Uid) -and $_.GetType() -ne [KeeperSecurity.Vault.ApplicationRecord] }

    $hasPasswordsToScan = $false
    foreach ($record in $notScanned) {
        $loadedRecord = $null
        if ($vault.TryLoadKeeperRecord($record.Uid, [ref]$loadedRecord)) {
            $pw = Get-KeeperRecordPassword -Record $loadedRecord -Silent
            if ($pw) {
                $hasPasswordsToScan = $true
                break
            }
        }
    }

    if ($hasPasswordsToScan) {
        Write-Host "`nSome passwords in your vault have not been scanned.`nUse `"breachwatch scan`" to check against the Dark Web database."
    }
}

function Test-PasswordAgainstBreachWatch {
    <#
    .SYNOPSIS
    Checks passwords against the BreachWatch database for breaches.

    .DESCRIPTION
    Tests one or more passwords against Keeper's BreachWatch database to determine if they have been compromised
    in known data breaches. Passwords can be provided as parameters or entered securely via prompt.

    .PARAMETER Passwords
    One or more passwords (as SecureString) to check. If not provided, you will be prompted to enter a password securely.

    .PARAMETER ShowPassword
    Display the actual password in the results instead of masking it with asterisks.

    .EXAMPLE
    Test-PasswordAgainstBreachWatch
    Prompts for a password securely and checks it against the BreachWatch database.

    .EXAMPLE
    $pwd1 = ConvertTo-SecureString "password123" -AsPlainText -Force
    Test-PasswordAgainstBreachWatch -Passwords $pwd1
    Checks the specified password against the BreachWatch database.

    .EXAMPLE
    $pwd = Read-Host "Enter password" -AsSecureString
    Test-PasswordAgainstBreachWatch -Passwords $pwd -ShowPassword
    Prompts for a password securely and displays the actual password in results.

    .NOTES
    This function requires an active Keeper vault session and a BreachWatch-enabled Enterprise account.
    Passwords are processed securely and are not stored or logged.
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [SecureString[]]$Passwords,
        
        [Parameter()]
        [Switch]$ShowPassword,
        
        [Parameter()]
        [string]$VaultContextVar = "Global:KeeperVaultContext"
    )

    begin {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
        $passwordList = New-Object System.Collections.Generic.List[string]
    }

    process {
        if ($Passwords) {
            foreach ($secPwd in $Passwords) {
                if ($null -ne $secPwd -and $secPwd.Length -gt 0) {
                    $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd)
                    try {
                        $password = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
                        if (-not [string]::IsNullOrEmpty($password)) {
                            $passwordList.Add($password)
                        }
                    }
                    finally {
                        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
                    }
                }
            }
        }
    }

    end {
        if ($vault.Auth.AuthContext.License.AccountType -ne 2) {
            Write-Host "BreachWatch is not available for this account type."
            Write-Host "BreachWatch requires an Enterprise license."
            return
        }

        if ($passwordList.Count -eq 0) {
            $securePassword = Read-Host "Password to Check" -AsSecureString
            if ($null -eq $securePassword -or $securePassword.Length -eq 0) {
                return
            }
            
            $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword)
            try {
                $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
                if (-not [string]::IsNullOrEmpty($plainPassword)) {
                    $passwordList.Add($plainPassword)
                }
            }
            finally {
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
            }
        }

        try {
            $initTask = [KeeperSecurity.BreachWatch.BreachWatch]::InitializeBreachWatch($vault.Auth)
            $initTask.Wait()

            if ([KeeperSecurity.BreachWatch.BreachWatch]::PasswordToken.Length -eq 0) {
                return
            }

            Write-Host "Scanning $($passwordList.Count) password(s)..."

            $passwordEntries = [System.Collections.Generic.List[ValueTuple[string, byte[]]]]::new()
            
            foreach ($password in $passwordList) {
                $tuple = [ValueTuple[string, byte[]]]::new($password, $null)
                $passwordEntries.Add($tuple)
            }
            
            $cancellationToken = [System.Threading.CancellationToken]::None
            $tasks = [KeeperSecurity.BreachWatch.BreachWatch]::ScanPasswordsAsync($passwordEntries, $cancellationToken)
            $tasks.Wait()
            $results = $tasks.Result
            $euids = New-Object System.Collections.Generic.List[byte[]]

            Write-Host "Processing $($results.Count) result(s)..."

            foreach ($result in $results) {
                $password = $result.Item1
                $status = $result.Item2

                if ($null -ne $status.Euid -and -not $status.Euid.IsEmpty) {
                    $euids.Add($status.Euid.ToByteArray())
                }

                $displayPassword = if ($ShowPassword) { $password } else { "*" * $password.Length }
                $statusText = if ($status.BreachDetected) { "WEAK" } else { "GOOD" }
                $score = [KeeperSecurity.BreachWatch.PasswordUtils]::PasswordScore($password)
                
                $strengthText = switch ($score) {
                    { $_ -lt 40 } { "Very Weak" }
                    { $_ -lt 60 } { "Weak" }
                    { $_ -lt 80 } { "Fair" }
                    { $_ -lt 90 } { "Good" }
                    { $_ -ge 90 } { "Strong" }
                }
                
                Write-Host ("{0,16}: {1} | Strength: {2} (Score: {3})" -f $displayPassword, $statusText, $strengthText, $score)
            }

            if ($euids.Count -gt 0) {
                $deleteTask = [KeeperSecurity.BreachWatch.BreachWatch]::DeleteEuids($euids)
                $deleteTask.Wait()
            }
        }
        catch [KeeperSecurity.BreachWatch.BreachWatchException] {
            $ex = $_.Exception
            if ($ex.Message.Contains("Invalid payload")) {
                Write-Host "BreachWatch Invalid Payload Error: $($ex.Message)"
                Write-Host ""
                Write-Host "Attempting to re-initialize BreachWatch tokens..."
                
                try {
                    $reinitTask = [KeeperSecurity.BreachWatch.BreachWatch]::ReInitializeBreachWatch($vault.Auth)
                    $reinitTask.Wait()
                    Write-Host "BreachWatch tokens re-initialized. Please try the command again."
                }
                catch {
                    Write-Host "Failed to re-initialize BreachWatch tokens: $($_.Exception.Message)"
                    Write-Host "This may indicate an account permissions issue or temporary server problem."
                }
            }
            else {
                Write-Host "BreachWatch error: $($ex.Message)"
            }
        }
        catch {
            Write-Host "Error scanning passwords: $($_.Exception.Message)"
            Write-Host "Exception type: $($_.Exception.GetType().FullName)"
            if ($_.Exception.InnerException) {
                Write-Host "Inner error: $($_.Exception.InnerException.Message)"
                Write-Host "Inner exception type: $($_.Exception.InnerException.GetType().FullName)"
            }
        }
    }
}

function Set-KeeperBreachWatchRecordIgnore {
    <#
    .SYNOPSIS
    Ignores BreachWatch records.

    .DESCRIPTION
    Sets one or more records' BreachWatch status to "Ignore", which excludes them from BreachWatch scanning and reporting.

    .PARAMETER RecordUids
    One or more record UIDs to ignore.

    .EXAMPLE
    Set-KeeperBreachWatchRecordIgnore -RecordUids "abc123def456"
    Ignores the record with the specified UID from breachwatch scan.

    .EXAMPLE
    Set-KeeperBreachWatchRecordIgnore -RecordUids "abc123def456", "xyz789ghi012"
    Ignores multiple records with the specified UIDs from breachwatch scan.

    .NOTES
    Usage: Set-KeeperBreachWatchRecordIgnore -RecordUids <record_uid1> [<record_uid2> ...]
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$RecordUids,

        [Parameter()]
        [string]$VaultContextVar = "Global:KeeperVaultContext"
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault

    if ($vault.Auth.AuthContext.License.AccountType -ne 2) {
        Write-Host "BreachWatch is not available for this account type."
        Write-Host "BreachWatch requires an Enterprise license."
        return
    }

    if ($RecordUids.Count -eq 0) {
        Write-Host "Record UID is required for 'ignore' command."
        Write-Host "Usage: Set-KeeperBreachWatchRecordIgnore -RecordUids <record_uid>"
        return
    }

    try {
        foreach ($recordUid in $RecordUids) {
            $record = $null
            if ($vault.TryLoadKeeperRecord($recordUid, [ref]$record)) {
                [KeeperSecurity.BreachWatch.BreachWatchIgnore]::IgnoreRecord($vault, $recordUid).GetAwaiter().GetResult()
                Write-Host "Record '$($record.Title)' (UID: $recordUid) has been ignored."
            }
            else {
                Write-Host "Record with UID '$recordUid' has not been found."                
            }
        }
    }
    catch {
        Write-Host "Error ignoring record(s): $($_.Exception.Message)"
    }
}

function Get-KeeperIgnoredBreachWatchRecords {
    <#
    .SYNOPSIS
    Lists all BreachWatch records that are currently ignored.

    .DESCRIPTION
    Retrieves all records in your vault that have been marked as ignored in BreachWatch.
    This helps you review which records you've previously chosen to exclude from BreachWatch monitoring.

    .PARAMETER OwnedOnly
    Show only records owned by you.

    .PARAMETER Numbered
    Display a serial number column in the output.

    .EXAMPLE
    Get-KeeperIgnoredBreachWatchRecords
    Lists all ignored BreachWatch records.

    .EXAMPLE
    Get-KeeperIgnoredBreachWatchRecords -OwnedOnly -Numbered
    Lists only your ignored records with numbering.

    .NOTES
    This function helps you audit which records have been excluded from BreachWatch monitoring.
    #>

    [CmdletBinding()]
    param(
        [Parameter()][Switch]$OwnedOnly,
        [Parameter()][Switch]$Numbered,
        [Parameter()][string]$VaultContextVar = "Global:KeeperVaultContext"
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault

    if ($vault.Auth.AuthContext.License.AccountType -ne 2) {
        Write-Host "BreachWatch is not available for this account type."
        Write-Host "BreachWatch requires an Enterprise license."
        return
    }

    $ignoredRecordUids = $vault.BreachWatchRecords() |
        Where-Object { $_.Status -eq "Ignore" } |
        Select-Object -ExpandProperty RecordUid

    if ($ignoredRecordUids.Count -eq 0) {
        Write-Host "No ignored BreachWatch records found."
        return
    }

    $records = $vault.KeeperRecords |
        Where-Object {
            $ignoredRecordUids -contains $_.Uid -and
            (-not $OwnedOnly -or $_.Owner)
        }

    if ($records.Count -gt 0) {
        $table = New-Object System.Collections.Generic.List[object]
        $index = 1

        foreach ($r in $records | Sort-Object Title) {
            $row = if ($Numbered) {
                [PSCustomObject]@{
                    "S.No"       = $index++
                    "Record UID" = $r.Uid
                    "Title"      = $r.Title
                    "Description"= [KeeperSecurity.Utils.RecordTypesUtils]::KeeperRecordPublicInformation($r)
                    "Status"     = "Ignored"
                }
            } else {
                [PSCustomObject]@{
                    "Record UID" = $r.Uid
                    "Title"      = $r.Title
                    "Description"= [KeeperSecurity.Utils.RecordTypesUtils]::KeeperRecordPublicInformation($r)
                    "Status"     = "Ignored"
                }
            }
            $table.Add($row)
        }

        $table | Format-Table -AutoSize
        Write-Host "Total ignored records: $($table.Count)"
    } else {
        Write-Host "No ignored BreachWatch records found for the specified criteria."
    }
}

Set-Alias -Name kbw -Value Get-KeeperBreachWatchList
Set-Alias -Name kbwp -Value Test-PasswordAgainstBreachWatch
Set-Alias -Name kbwi -Value Set-KeeperBreachWatchRecordIgnore
Set-Alias -Name kbwig -Value Get-KeeperIgnoredBreachWatchRecords

# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA8NAII+VaLxcbC
# uNkz1gsRV/lzSkaeFC/xqyGjKxpUQaCCITswggWNMIIEdaADAgECAhAOmxiO+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
# oAMCAQICEAWjoxq4NU+fKJYdPQIHYbgwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MTAeFw0yNDEyMzEwMDAwMDBaFw0yNTEyMzAyMzU5NTlaMIHRMRMwEQYLKwYBBAGC
# NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ
# cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC
# VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK
# ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5
# IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDO/6wXrwKVD/ly
# Y5UsXcgEgAJyxUNa+DRVz6P0tEWk79TgmoQKRHkjeqMvtDsGyzZcGkHqKDmpzHGY
# YeuP/nxfEswjBw3Pz7/GgbHdQdlyMP1U/jWsH49DcQF9c6Sj6f7T6mr5urJkQzDg
# HjqO6i8cyLuvG0thJ6r0k9d+u7TKcYYIK1ZdxmCj4XQj3jQ0bJMIXgksSNnM0fKH
# u5oLqNp1WmUeVRKuMWJDGcE7k+0TfBEeK+XKzkmyRcmm6e/UoGD+zcipg3GGelyd
# ugH2bBA2a3uFYc2qjtK5fIuRwZlZgysFND2iKAFHcnrpbSGkuaHtBY3E4xra5AZf
# ge0wrbzdSURPU5st7KZ6i8r5UYa1SXQyghqL0CiiZPOwQIyKIjUphF7NB9D2wdWW
# DgOpuHHu/wTplGn5YmFgaq1h5h4saov4WuWSsSpWMRpYpA+KdVpTJgxAYYN0T6HV
# tWeXhqvdmXsTBjlrHzkKd2IUw3TDgO7Y1j9l8wGnmxsQnjhDRKUCAwEAAaOCAgIw
# ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBTn
# KL7CoOsQM8480ZplkEaWu6eOzDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB
# BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC
# B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p
# bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
# QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT
# QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA
# A4ICAQCyKgKBI5CmZbjIjn38FPlfsIipJg25SiGcpsrMxGaLfp4geoO7UV61pEKO
# oJ/DhtqhuiDvC/qNFb8FtPldPKeoFam9I3kfZApu5A2ZNkZs3r4WQ60W2Y9EOym9
# 6SHg/ShLnEXxBdFBqoKdKu43qxVz8yrUJOZCaT+ubEkVe24ISDFraubHR5/4hlDI
# /vBXfQBMauJEfnck8P+qdWRYmO6SL+GAI4w645MlDEqILeno/WBsEwvMlVybI2qy
# 6thfrL8J/DZVPk9N1P2Y13P2cQ3OYN4qq5eRpvUAmi4RtmiyKxkVzefT3KJKeyRT
# Wup5JFkkoIeMNNbc/sUUsoqilgFzbNpR0w9hfXHhpc/VMP0bz4T3JemyC6iXEekV
# JM2bayhzSo/OTwZvyprq8dHKOcUcV7F7Bd7Zqdq/k7ddb3gPY3vqcs7qhP5EUD5+
# YRfHyEbnmoeyVTGmhwUzG0Wg3FRYqBpghh1Mdu9MoS2qQNHmF6WSb9J+tkCDKF/T
# T3FthHMIS1hgFdSFvkyKP8XKawFwbVwjv5obHICJ2zM3rHYNLlrG6IsuB33kxMwn
# vZXeeG4lCkyULuMKm85DeUhnsDik3f/nOJOcOss7lFrJN2GaY1qrIKU5QN81Ofdb
# rzBbKtBWHQNz8vGLFWmy8E8c/Xe5KQQB0z0WMA3LCjSxjD3PujGCBdkwggXVAgEB
# MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD
# VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI
# QTM4NCAyMDIxIENBMQIQBaOjGrg1T58olh09AgdhuDANBglghkgBZQMEAgEFAKCB
# hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ
# BDEiBCDNWAV8i5A5HBXc0gldOoRO2MK/k6+Y+Z/mQ+9QPYCOtDANBgkqhkiG9w0B
# AQEFAASCAYDA9ZYF6/ee5qxwNVB7nzXxtFdDjVAfNrq1WOkHTrxLopDsSIXNuRHg
# R/ilpFSeUBidUPupbb5qhU/RoeRpFxQUpc2fOoa5xDTzaGDOpSamYNc/8G0Fsk3w
# Dr94rhlnXUdKiEHXvD8dGhaFNI4yhoZdfS0acKj3hziSrWCqeiAWp9KCYBJKv+ds
# hnW38zejdKacRPeWhtdWgSZMA491PLkaLx5DpSCUDZt/FK40MLeX2j6usUpt/tF6
# ljMcXltPhrNqA7/yT5ZU0SMVuWnGSMm4AMBWbFoB3j+gMhRwYuXbVNYKVt4kXB3w
# SWFcpllqTVrwbkGYSYaimR8i6s4Hl0X3HSi73bDCIbKMbhofRwJm07TVLNu09j0G
# SS22MLmqj+b40zjBJK1sF5YNXm9VRcvpFLZLHzf6EXoMbrrtFzKbx1rGQlU3eD1c
# /U95JWRvo/zbx7yBtChX4GX2mPqPCM6vRBZbWXxDlG9m4V0QrOlGWaqpTzPiw5We
# nKB0JVZY6XqhggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjUwOTMwMDQzODA2WjAv
# BgkqhkiG9w0BCQQxIgQgOzzz/r5RFtKXKYiUQnNv8/+qKAXKhsRfTlYh9fzELmEw
# DQYJKoZIhvcNAQEBBQAEggIANu/Kf9cumB17Y7St5x4wt+mcL/Gy3ObjIrQDuso7
# 84yt4uQ31O+EIrDxJZcNk2dmFF5wHHZ5iPI5aMX+EwZ14Em27yjvPflh0032j3/G
# f0Cyb3xrqrjslAO7sOvC9WypdIwcX59iwjwBwv9z1ssaD+au0fNG8+Wp/K7Of8qs
# 3vschrouC6XXzsnQEyJATAx3LGLKzEFuwq9fm1nDq0HM99C53RTqr8GUFjp4QqFh
# C7HLzcRx7ZjR7yKYko/syyQps1tfl6VSFiM/v5zlcdmC2FvAxhx18gYjMfh7JKk3
# jbL5oKE6hci7AGh7xmHfFwJLYGOF9ZrhAwHIZm6oORpoiJsFtXHBHNK48OfloKEh
# 50/fgsfK5coFtJdxzbx5tUhr7KN7klrZM2vUO6BMidHmWd9cC5v+iIHEXKCPU6K7
# /QX0gkumDIekZKu8FbNHD6T5A4Mka0fW+D6AHz1X3JvGvnDsMGopWo3R7IbqRRm7
# syFU/2i8G4yuG1py65x2wGoCl5TY9qX+5bMDPdTzlT3cmcAUT/KUq1vPguqwTE4H
# ki83rNVfLCfm3tk74rxbV/2E9hnv8TSC5lN1smsYFuN0503wRKjEVVv907zbnR9Q
# DfiAVIhDeC1CG1nlCXzo26HecBigVBbmg64A/ohz9zF+ndxfM8rs2U+wxiail5HF
# OY4=
# SIG # End signature block