EnterpriseDevices.ps1
|
function Convert-DeviceTokenToString { <# .Synopsis Internal helper function to convert device token byte array to string #> Param ( [Parameter(Mandatory = $true)] [byte[]] $Token ) $sb = New-Object System.Text.StringBuilder $maxLength = 50 foreach ($b in $Token) { if ($sb.Length -ge $maxLength) { break } [void]$sb.AppendFormat("{0:x2}", $b) } return $sb.ToString() } function Get-PendingKeeperDeviceApproval { <# .Synopsis List pending device approval requests .Description Displays a list of all pending device approval requests with details including user email, device ID, device name, client version, and IP address. .Parameter Reload Reload the list of pending device approvals from the server .Parameter Format Output format: table, csv, or json .Parameter Output File path to write output to (required for csv and json formats) .Example Get-PendingKeeperDeviceApproval Lists all pending device approvals in table format .Example Get-PendingKeeperDeviceApproval -Format csv -Output devices.csv Exports pending device approvals to CSV file #> [CmdletBinding()] Param ( [Parameter()][switch] $Reload, [Parameter()][ValidateSet('table', 'csv', 'json')][string] $Format = 'table', [Parameter()][string] $Output ) [Enterprise]$enterprise = getEnterprise if ($Reload) { $enterprise.loader.Load().GetAwaiter().GetResult() | Out-Null } $approvals = @($enterprise.deviceApproval.DeviceApprovalRequests) if ($approvals.Count -eq 0) { Write-Output "There are no pending devices" return } $deviceList = New-Object System.Collections.ArrayList foreach ($device in $approvals) { $user = $null if ($enterprise.enterpriseData.TryGetUserById($device.EnterpriseUserId, [ref]$user)) { $deviceTokenBytes = $device.EncryptedDeviceToken.ToByteArray() $deviceId = Convert-DeviceTokenToString -Token $deviceTokenBytes [void]$deviceList.Add([PSCustomObject]@{ Email = $user.Email DeviceId = $deviceId DeviceName = $device.DeviceName ClientVersion = $device.ClientVersion IpAddress = $device.IpAddress DeviceType = $device.DeviceType }) } else { Write-Warning "Skipping device for user ID $($device.EnterpriseUserId) - user not found" } } $deviceList = $deviceList.ToArray() $deviceList = $deviceList | Sort-Object DeviceId if ($Format -eq 'json') { $json = $deviceList | ConvertTo-Json if ($Output) { $json | Out-File -FilePath $Output -Encoding UTF8 Write-Output "Output written to $Output" } else { Write-Output $json } } elseif ($Format -eq 'csv') { if (-not $Output) { Write-Error "Output file path is required for CSV format" -ErrorAction Stop } $deviceList | Export-Csv -Path $Output -NoTypeInformation Write-Output "Output written to $Output" } else { $deviceList | Format-Table -AutoSize Email, DeviceId, DeviceName, ClientVersion, IpAddress } } function Approve-KeeperDevice { <# .Synopsis Approve pending device requests .Description Approves pending device approval requests. You can specify devices by device ID (partial match supported) or user email. .Parameter Match Device ID (partial match supported) or user email to approve. If not specified, all pending devices will be approved. .Parameter Reload Reload the list of pending device approvals before processing .Parameter TrustedIp Approve devices from a trusted IP address .Example Approve-KeeperDevice -Match "user@example.com" Approves all pending devices for user@example.com .Example Approve-KeeperDevice -Match "a1b2c3" Approves devices with device ID starting with "a1b2c3" .Example Approve-KeeperDevice Approves all pending devices #> [CmdletBinding(SupportsShouldProcess)] Param ( [Parameter(Position = 0)] [string] $Match, [Parameter()] [switch] $Reload, [Parameter()] [switch] $TrustedIp ) [Enterprise]$enterprise = getEnterprise if ($Reload) { try { $enterprise.loader.Load().GetAwaiter().GetResult() | Out-Null } catch { Write-Error "Failed to reload enterprise data: $($_.Exception.Message)" -ErrorAction Stop } } $approvals = @($enterprise.deviceApproval.DeviceApprovalRequests) if ($approvals.Count -eq 0) { Write-Output "There are no pending devices" return } if (-not [string]::IsNullOrWhiteSpace($Match)) { $Match = $Match.Trim() } $devices = Get-MatchingDevices -Approvals $approvals -Enterprise $enterprise -Match $Match if ($devices.Count -eq 0) { $matchText = if ([string]::IsNullOrWhiteSpace($Match)) { "all pending devices" } else { "matching '$Match'" } Write-Output "No device found $matchText" return } $deviceDetails = $devices | ForEach-Object { $deviceTokenBytes = $_.EncryptedDeviceToken.ToByteArray() $deviceId = Convert-DeviceTokenToString -Token $deviceTokenBytes $user = $null if ($enterprise.enterpriseData.TryGetUserById($_.EnterpriseUserId, [ref]$user)) { "$($user.Email) ($deviceId)" } else { "User ID $($_.EnterpriseUserId) ($deviceId)" } } $deviceListText = $deviceDetails -join ", " if ($PSCmdlet.ShouldProcess("$($devices.Count) device(s)", "Approve", "This will approve the following devices: $deviceListText")) { try { $enterprisePrivateKeyBytes = $enterprise.loader.EcPrivateKey if (-not $enterprisePrivateKeyBytes) { Write-Error "Enterprise private key not available. Cannot approve devices without the enterprise private key." return } $enterpriseEcKey = [KeeperSecurity.Utils.CryptoUtils]::LoadEcPrivateKey($enterprisePrivateKeyBytes) $dataKeys = New-Object 'System.Collections.Generic.Dictionary[long,byte[]]' $userIdsToLoad = New-Object 'System.Collections.Generic.List[long]' foreach ($device in $devices) { if (-not $dataKeys.ContainsKey($device.EnterpriseUserId)) { $userIdsToLoad.Add($device.EnterpriseUserId) } } if ($userIdsToLoad.Count -gt 0) { $dataKeyRq = New-Object Authentication.UserDataKeyRequest foreach ($userId in $userIdsToLoad) { $dataKeyRq.EnterpriseUserId.Add($userId) | Out-Null } $dataKeyRs = $enterprise.loader.Auth.ExecuteAuthRest("enterprise/get_enterprise_user_data_key", $dataKeyRq, [Enterprise.EnterpriseUserDataKeys]).GetAwaiter().GetResult() foreach ($key in $dataKeyRs.Keys) { if ($key.UserEncryptedDataKey.IsEmpty) { continue } try { $userDataKey = [KeeperSecurity.Utils.CryptoUtils]::DecryptEc($key.UserEncryptedDataKey.ToByteArray(), $enterpriseEcKey) $dataKeys[$key.EnterpriseUserId] = $userDataKey } catch { Write-Warning "Failed to decrypt data key for user $($key.EnterpriseUserId): $($_.Exception.Message)" } } } $rq = New-Object Enterprise.ApproveUserDevicesRequest foreach ($device in $devices) { if (-not $dataKeys.ContainsKey($device.EnterpriseUserId)) { continue } if ($device.DevicePublicKey.IsEmpty) { continue } try { $devicePublicKey = [KeeperSecurity.Utils.CryptoUtils]::LoadEcPublicKey($device.DevicePublicKey.ToByteArray()) $userDataKey = $dataKeys[$device.EnterpriseUserId] $encryptedDataKey = [KeeperSecurity.Utils.CryptoUtils]::EncryptEc($userDataKey, $devicePublicKey) $deviceRq = New-Object Enterprise.ApproveUserDeviceRequest $deviceRq.EnterpriseUserId = $device.EnterpriseUserId $deviceRq.EncryptedDeviceToken = [Google.Protobuf.ByteString]::CopyFrom($device.EncryptedDeviceToken.ToByteArray()) $deviceRq.EncryptedDeviceDataKey = [Google.Protobuf.ByteString]::CopyFrom($encryptedDataKey) $rq.DeviceRequests.Add($deviceRq) | Out-Null } catch { Write-Warning "Failed to prepare approval for device: $($_.Exception.Message)" } } if ($rq.DeviceRequests.Count -eq 0) { Write-Output "No device to approve" return } $rs = $enterprise.loader.Auth.ExecuteAuthRest("enterprise/approve_user_devices", $rq, [Enterprise.ApproveUserDevicesResponse]).GetAwaiter().GetResult() if ($rs.DeviceResponses -and $rs.DeviceResponses.Count -gt 0) { foreach ($approveRs in $rs.DeviceResponses) { if ($approveRs.Failed) { $user = $null if ($enterprise.enterpriseData.TryGetUserById($approveRs.EnterpriseUserId, [ref]$user)) { Write-Warning "Failed to approve device for $($user.Email): $($approveRs.Message)" } else { Write-Warning "Failed to approve device for user ID $($approveRs.EnterpriseUserId): $($approveRs.Message)" } } } } try { $enterprise.loader.Load().GetAwaiter().GetResult() | Out-Null } catch { Write-Warning "Failed to reload enterprise data after approval: $($_.Exception.Message)" } Write-Output "Approved $($rq.DeviceRequests.Count) device(s)" } catch { Write-Error "Failed to approve devices: $($_.Exception.Message)" -ErrorAction Stop } } } function Deny-KeeperDevice { <# .Synopsis Deny pending device requests .Description Denies pending device approval requests. You can specify devices by device ID (partial match supported) or user email. .Parameter Match Device ID (partial match supported) or user email to deny. If not specified, all pending devices will be denied. .Parameter Reload Reload the list of pending device approvals before processing .Example Deny-KeeperDevice -Match "user@example.com" Denies all pending devices for user@example.com .Example Deny-KeeperDevice -Match "a1b2c3" Denies devices with device ID starting with "a1b2c3" .Example Deny-KeeperDevice Denies all pending devices #> [CmdletBinding(SupportsShouldProcess)] Param ( [Parameter(Position = 0)] [string] $Match, [Parameter()][switch] $Reload ) [Enterprise]$enterprise = getEnterprise if ($Reload) { try { $enterprise.loader.Load().GetAwaiter().GetResult() | Out-Null } catch { Write-Error "Failed to reload enterprise data: $($_.Exception.Message)" -ErrorAction Stop } } $approvals = @($enterprise.deviceApproval.DeviceApprovalRequests) if ($approvals.Count -eq 0) { Write-Output "There are no pending devices" return } if (-not [string]::IsNullOrWhiteSpace($Match)) { $Match = $Match.Trim() } $devices = Get-MatchingDevices -Approvals $approvals -Enterprise $enterprise -Match $Match if ($devices.Count -eq 0) { $matchText = if ([string]::IsNullOrWhiteSpace($Match)) { "all pending devices" } else { "matching '$Match'" } Write-Output "No device found $matchText" return } $deviceDetails = $devices | ForEach-Object { $deviceTokenBytes = $_.EncryptedDeviceToken.ToByteArray() $deviceId = Convert-DeviceTokenToString -Token $deviceTokenBytes $user = $null if ($enterprise.enterpriseData.TryGetUserById($_.EnterpriseUserId, [ref]$user)) { "$($user.Email) ($deviceId)" } else { "User ID $($_.EnterpriseUserId) ($deviceId)" } } $deviceListText = $deviceDetails -join ", " if ($PSCmdlet.ShouldProcess("$($devices.Count) device(s)", "Deny", "This will deny the following devices: $deviceListText")) { try { $rq = New-Object Enterprise.ApproveUserDevicesRequest foreach ($device in $devices) { $deviceRq = New-Object Enterprise.ApproveUserDeviceRequest $deviceRq.EnterpriseUserId = $device.EnterpriseUserId $deviceRq.EncryptedDeviceToken = [Google.Protobuf.ByteString]::CopyFrom($device.EncryptedDeviceToken.ToByteArray()) $deviceRq.DenyApproval = $true $rq.DeviceRequests.Add($deviceRq) | Out-Null } if ($rq.DeviceRequests.Count -eq 0) { Write-Output "No device to deny" return } $rs = $enterprise.loader.Auth.ExecuteAuthRest("enterprise/approve_user_devices", $rq, [Enterprise.ApproveUserDevicesResponse]).GetAwaiter().GetResult() if ($rs.DeviceResponses -and $rs.DeviceResponses.Count -gt 0) { foreach ($approveRs in $rs.DeviceResponses) { if ($approveRs.Failed) { $user = $null if ($enterprise.enterpriseData.TryGetUserById($approveRs.EnterpriseUserId, [ref]$user)) { Write-Warning "Failed to deny device for $($user.Email): $($approveRs.Message)" } else { Write-Warning "Failed to deny device for user ID $($approveRs.EnterpriseUserId): $($approveRs.Message)" } } } } try { $enterprise.loader.Load().GetAwaiter().GetResult() | Out-Null } catch { Write-Warning "Failed to reload enterprise data after denial: $($_.Exception.Message)" } Write-Output "Denied $($rq.DeviceRequests.Count) device(s)" } catch { Write-Error "Failed to deny devices: $($_.Exception.Message)" -ErrorAction Stop } } } function Get-MatchingDevices { param( [Parameter(Mandatory=$true)] $Approvals, [Parameter(Mandatory=$true)] $Enterprise, [string]$Match ) if ([string]::IsNullOrWhiteSpace($Match)) { return @($Approvals) } $devices = New-Object System.Collections.ArrayList foreach ($device in $Approvals) { $deviceTokenBytes = $device.EncryptedDeviceToken.ToByteArray() $deviceId = Convert-DeviceTokenToString -Token $deviceTokenBytes if ($deviceId.StartsWith($Match, [System.StringComparison]::OrdinalIgnoreCase)) { [void]$devices.Add($device) continue } $user = $null if ($Enterprise.enterpriseData.TryGetUserById($device.EnterpriseUserId, [ref]$user)) { if ($user.Email -ieq $Match) { [void]$devices.Add($device) } } } return $devices.ToArray() } function Get-TrustedIpDevices { param( [Parameter(Mandatory=$true)] $Devices, [Parameter(Mandatory=$true)] $Enterprise ) try { $userIds = New-Object System.Collections.Generic.HashSet[long] $userEmails = New-Object System.Collections.Generic.Dictionary[long,string] foreach ($device in $Devices) { if (-not $userIds.Contains($device.EnterpriseUserId)) { $userIds.Add($device.EnterpriseUserId) | Out-Null $user = $null if ($Enterprise.enterpriseData.TryGetUserById($device.EnterpriseUserId, [ref]$user)) { $userEmails[$device.EnterpriseUserId] = $user.Email } } } if ($userEmails.Count -eq 0) { return @() } $lastYear = (Get-Date).AddDays(-365) $fromTimestamp = [DateTimeOffset]::new($lastYear).ToUnixTimeSeconds() $toTimestamp = [DateTimeOffset]::new((Get-Date)).ToUnixTimeSeconds() $rq = New-Object KeeperSecurity.Enterprise.AuditLogCommands+GetAuditEventReportsCommand $rq.ReportType = "span" $rq.Scope = "enterprise" $rq.Columns = @("ip_address", "username") $rq.Limit = 1000 $filter = New-Object KeeperSecurity.Enterprise.AuditLogCommands+ReportFilter $filter.EventTypes = @("login") $filter.Username = $userEmails.Values.ToArray() $filter.Created = New-Object KeeperSecurity.Enterprise.AuditLogCommands+CreatedFilter $filter.Created.Min = $fromTimestamp $filter.Created.Max = $toTimestamp $rq.Filter = $filter $auditResult = $Enterprise.loader.Auth.ExecuteAuthCommand( [KeeperSecurity.Enterprise.AuditLogCommands+GetAuditEventReportsCommand], [KeeperSecurity.Enterprise.AuditLogCommands+GetAuditEventReportsResponse], $rq ).GetAwaiter().GetResult() $auditEvents = $auditResult.Events $trustedIps = New-Object 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.HashSet[string]]' foreach ($auditEvent in $auditEvents) { if ($auditEvent.ContainsKey('username') -and $auditEvent.ContainsKey('ip_address')) { $username = $auditEvent['username'].ToString().ToLowerInvariant() $ipAddress = $auditEvent['ip_address'].ToString() if (-not $trustedIps.ContainsKey($username)) { $trustedIps[$username] = New-Object System.Collections.Generic.HashSet[string] } [void]$trustedIps[$username].Add($ipAddress) } } $trustedDevices = New-Object System.Collections.ArrayList foreach ($device in $Devices) { $user = $null if ($Enterprise.enterpriseData.TryGetUserById($device.EnterpriseUserId, [ref]$user)) { $username = $user.Email.ToLowerInvariant() $deviceIp = $device.IpAddress if ($trustedIps.ContainsKey($username) -and $trustedIps[$username].Contains($deviceIp)) { [void]$trustedDevices.Add($device) } else { Write-Warning "The user $($user.Email) attempted to login from an untrusted IP ($deviceIp). To force the approval, run the same command without the -TrustedIp argument" } } } return $trustedDevices.ToArray() } catch { Write-Warning "Failed to filter devices by trusted IP: $($_.Exception.Message). Approving all matching devices." return $Devices } } # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC7ZGZx3U8B/V3V # zcdr5RgHre7NlscktE4RmGzKgiBTvKCCITswggWNMIIEdaADAgECAhAOmxiO+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 # oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE # BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy # dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB # MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC # NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ # cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC # VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK # ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5 # IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a # vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ # aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc # vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC # h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt # 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk # Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s # FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm # 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw # ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG # 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p # bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT # QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA # A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6 # z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2 # IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL # iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu # FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc # H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA # 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB # +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b # WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A # g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH # 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB # MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD # VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI # QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB # hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE # AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ # BDEiBCARLJrtAYr7CdVC79wc4ZUxad/zWAlWT/z5GD3s/jXVSjANBgkqhkiG9w0B # AQEFAASCAYAuypZHnQfrqZ5ISrp3k/z5HyVbpWNm2zSETs2ChxX5fk0A0H/L64Ug # xzF+5SlNami5kVNfy591i1IgCtKe0fr2BpyutmsPz93JdBEhnL23Bw2NUnBWc7Gy # DMsBi4uPFWH3moiuoPYw8JzhmDT91vAZW82ySO5JaET/lO+3ORyGqRyGTV9mU95g # aW3DZDOr4uwHV1lZGUwitejlOcr6w5AlK1EcEZ1a6hVWgNRmKWliV3L7xKeD3g6S # XSG1denG9OnF8tTMlFr1jXiCAFuYHYuYQRuM6/kDsLayf09XYwsuO1TggXqXSA7Q # XFTbEigt/Pu9AUIi3kDbc60P2+4Gt8PSqZBlo+3JiFuqFl+mh/DAhidjxX3dY4zc # 4FSDxedBZ7A8H5jl2jB/sC2eat5bvyf+zze080g87T/6EsR6z0HluAYbMWS6oahp # hZFi18zaziRGNNWxWlV8/Tqs9o29IYsC8SXf7jI+jC1/YbGL0OjO4AhATvnGZ+No # dBho0Uonui6hggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwMTIzMjMzOTIwWjAv # BgkqhkiG9w0BCQQxIgQg+YZntuGKkfGtvum1HS4m8dbKB/EWRTyMgNTcYFo7BgEw # DQYJKoZIhvcNAQEBBQAEggIAsrY6F3Ad4X5ynnp7/r2kF7KbF+B46w4j8QcCcFo2 # 6qD834/1tABRU/6pHgCNM6bZty7flF2ceF0vMDnJghVGYfHNfXzUo9a2RVUUqqvF # B/TGm9YhVrBOtXuYiGzY3YHsnZo6Pqf/5x7XGlf7fYN+Y+gjHJFHN76exbyY3A4s # t2V1bp1+A02nP+LBxs1ELmFud+BSAF05xZnqnIkP4wC3ebglJrlvCTQhp6zuU4pY # yq/ma4E2Em1TW7K0ae10hdWiMTpu9uF79EWz6MGltL9fun3s4266mWHQiD+qaWPw # J20KmZ+lwZbKS+tiubGsRwt9SiPdQLYgjh45FEEimJikZp6qmuJWMwwBQ7ITPRFv # al69/Ck1Ap5gunvlkiyjSZLGcrVy7ZxjSwg2nc3R5vjVQja5QtRREGcYZ5W27Eas # f34VaqYRvJtWb+ojyE7kXvIYFrAxdlkOt9oAwZs8d76IhCZP9ZXZiAxe++UtDPAW # DAZQR/0f/qMCfPuhxMnvc1x+6rMD8M2e4rSsMsjcsKmf1qlCMro+rZSfw/fKBAqo # hqnGPqO6w2z3dtMBXoYlEa+pm3AC9ugHMNu6nut+DY4Uc7k2SJeRKmezl6aWB9t+ # 5bVK/HwGcE3WL61QsKyqYLeDi9ByZYxBJ/8Jx38P66wgifDjfx5/KRGyYNuU+3jo # lv0= # SIG # End signature block |