VaultCommands.ps1
|
#requires -Version 5.1 . "$PSScriptRoot/ImportSampleData.ps1" $Script:PathDelimiter = [System.IO.Path]::DirectorySeparatorChar function getVault { if (-not $Script:Context.Auth) { Write-Error -Message "Not Connected" -ErrorAction Stop } if (-not $Script:Context.Vault) { Write-Error -Message "Not Connected to Keeper vault. Please login first." -ErrorAction Stop } $Script:Context.Vault } function resolveKeeperNSFFolder { Param( [Parameter(Mandatory = $true)][string] $Identifier, [Parameter(Mandatory = $true)][KeeperSecurity.Vault.VaultOnline] $Vault ) [KeeperSecurity.Vault.FolderNode]$folder = $null if ($Vault.TryGetKeeperNSFFolder($Identifier, [ref]$folder)) { return $folder } $match = @($Vault.KeeperNSFFolderNodes | Where-Object { $_.Name -and $_.Name -ieq $Identifier }) if ($match.Count -eq 1) { return $match[0] } return $null } function resolveKeeperNSFRecord { Param( [Parameter(Mandatory = $true)][string] $Identifier, [Parameter(Mandatory = $true)][KeeperSecurity.Vault.VaultOnline] $Vault ) [KeeperSecurity.Vault.KeeperNSFRecord]$record = $null if ($Vault.TryGetKeeperNSFRecord($Identifier, [ref]$record)) { return $record } $titleMatch = @($Vault.KeeperNSFRecordEntries | Where-Object { $_.Title -and $_.Title -ieq $Identifier }) if ($titleMatch.Count -eq 1) { return $titleMatch[0] } if ($titleMatch.Count -eq 0 -and $Vault.TryResolveKeeperNSFRecord($Identifier, [ref]$record)) { return $record } return $null } function Get-KeeperLocation { <# .Synopsis Get current Keeper folder #> [CmdletBinding()] [KeeperSecurity.Vault.VaultOnline]$vault = getVault [string]$currentFolder = $Script:Context.CurrentFolder [KeeperSecurity.Vault.FolderNode]$folder = $vault.RootFolder if ($currentFolder) { $vault.TryGetFolder($currentFolder, [ref]$folder) | Out-Null } exportKeeperNode $folder } New-Alias -Name kpwd -Value Get-KeeperLocation function Set-KeeperLocation { <# .Synopsis Change current Keeper folder .Parameter Path New location #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Path ) [KeeperSecurity.Vault.VaultOnline]$vault = getVault if ($Path) { [KeeperSecurity.Vault.FolderNode]$folder = $null if (!$vault.TryGetFolder($Script:Context.CurrentFolder, [ref]$folder)) { $folder = $vault.RootFolder } $components = splitKeeperPath $Path $rs = parseKeeperPath $components $vault $folder if ($rs -and !$rs[1]) { $folder = $rs[0] $uid = $folder.FolderUid if ($vault.TryGetFolder($uid, [ref]$folder)) { $Script:Context.CurrentFolder = $uid } else { $Script:Context.CurrentFolder = '' } } } getVaultFolderPath $vault $Script:Context.CurrentFolder } $Keeper_FolderPathRecordCompleter = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $result = @() [KeeperSecurity.Vault.VaultOnline]$vault = $Script:Context.Vault if ($vault) { [KeeperSecurity.Vault.FolderNode] $folder = $null if (!$vault.TryGetFolder($Script:Context.CurrentFolder, [ref]$folder)) { $folder = $vault.RootFolder } $pattern = '' $toComplete = $wordToComplete if ($toComplete.Length -ge 2) { if ($toComplete[0] -eq '''' -and $toComplete[-1] -eq '''') { $toComplete = $toComplete.Substring(1, $toComplete.Length - 2) $toComplete = $toComplete -replace '''', '''' } } if ($toComplete) { $components = splitKeeperPath $toComplete if ($components.Count -gt 1) { if ($components[-1]) { $pattern = $components[-1] $components[-1] = '' } $rs = parseKeeperPath $components $vault $folder if ($rs -and $rs.Count -eq 2) { if (!$rs[1]) { $folder = $rs[0] } else { $folder = $null } } } else { if ($components) { $pattern = $components $components = @('') } else { $folder = $vault.RootFolder $pattern = '' $components = @('') } } } else { $components = @('') $pattern = $wordToComplete } if ($folder) { $pattern += '*' foreach ($uid in $folder.Subfolders) { $subfolder = $null if ($vault.TryGetFolder($uid, [ref]$subfolder)) { if ($subfolder.Name -like $pattern) { $path = @() $components | ForEach-Object { $path += $_ } $path[-1] = $subfolder.Name $expansion = ($path | ForEach-Object { $_ -replace '\\', '\\' }) -join $Script:PathDelimiter if ($expansion -match '[\s'']') { $expansion = $expansion -replace '''', '''''' $expansion = "'${expansion}'" } $result += $expansion } } } } } if ($result.Count -gt 0) { return $result } else { return $null } } Register-ArgumentCompleter -CommandName Set-KeeperLocation -ParameterName Path -ScriptBlock $Keeper_FolderPathRecordCompleter New-Alias -Name kcd -Value Set-KeeperLocation function Get-KeeperChildItem { <# .Synopsis Get the content of Keeper folder. Output and parameters are similar to Get-ChildItem cmdlet .Parameter Path Keeper folder .Parameter Filter Match the string in Title, Uid, Login, and Link fields .Parameter Recursive Get child items in subfolders recursively .Parameter Depth Recursion depth .Parameter SkipGrouping Do not group result set by folder .Parameter ObjectType Limit result set to Folders or Records only .Parameter List Display detailed list output .Parameter Format Output format: table, csv, or json .Parameter Output Path to output file. If specified, formatted output is written to the file #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Path, [string] $Filter, [Switch] $Recursive, [int] $Depth, [Switch] $SkipGrouping, [ValidateSet('Folder' , 'Record')][string] $ObjectType, [Alias('l')][Switch] $List, [ValidateSet('table', 'csv', 'json')][string] $Format, [string] $Output ) $showFolder = $true $showRecord = $true if ($ObjectType) { $showFolder = $ObjectType -eq 'Folder' $showRecord = !$showFolder } [KeeperSecurity.Vault.VaultOnline]$vault = getVault [KeeperSecurity.Vault.FolderNode] $currentDir = $null if (!$vault.TryGetFolder($Script:Context.CurrentFolder, [ref]$currentDir)) { $currentDir = $vault.RootFolder } [KeeperSecurity.Vault.FolderNode] $baseDir = $null if ($Path) { if (-not $vault.TryGetFolder($Path, [ref]$baseDir)) { $components = splitKeeperPath $Path $rs = parseKeeperPath $components $vault $currentDir if ($rs -is [array]) { if (-not $rs[1]) { $baseDir = $rs[0] } } } } else { $baseDir = $currentDir } if (-not $baseDir) { Write-Error -Message "Cannot find path '$Path'" -ErrorAction Stop } [KeeperSecurity.Vault.FolderNode[]]$folders = @($baseDir) if ($Recursive.IsPresent) { $pos = 0 $dep = 0 while ($pos -lt $folders.Count) { if ($Depth -gt 0) { if ($dep -ge $Depth) { break } } $lastPos = $folders.Count for ($i = $pos; $i -lt $lastPos; $i++) { foreach ($uid in $folders[$i].Subfolders) { [KeeperSecurity.Vault.FolderNode] $sf = $null; if ($vault.TryGetFolder($uid, [ref]$sf)) { $folders += $sf } } } $pos = $lastPos $dep++ } } $entries = @() $recordEntries = @{} for ($i = 0; $i -lt $folders.Count; $i++) { [KeeperSecurity.Vault.FolderNode]$f = $folders[$i] $path = getVaultFolderPath $vault $f.FolderUid if ($showFolder) { foreach ($uid in $f.Subfolders) { [KeeperSecurity.Vault.FolderNode]$sf = $null if ($vault.TryGetFolder($uid, [ref]$sf)) { $match = $true if ($Filter) { $match = @($sf.Name, $sf.FolderUid) | Select-String $Filter | Select-Object -First 1 } if ($match) { $entry = [PSCustomObject]@{ PSTypeName = "KeeperSecurity.Commander.FolderEntry$(if ($SkipGrouping.IsPresent) {'Flat'} else {''})" Uid = $sf.FolderUid Name = $sf.Name OwnerFolder = $path FolderType = $sf.FolderType Shared = $sf.FolderType -ne [KeeperSecurity.Vault.FolderType]::UserFolder SortGroup = 0 } $entries += $entry } } } } if ($showRecord) { foreach ($uid in $f.Records) { [KeeperSecurity.Vault.KeeperRecord] $r = $null if ($vault.TryGetKeeperRecord($uid, [ref]$r)) { if ($r.Version -ne 2 -and $r.Version -ne 3) { continue } $match = $true if ($Filter) { $match = @($r.Title, $r.Uid) | Select-String $Filter | Select-Object -First 1 } if ($match) { if ($Flat.IsPresent -and $recordEntries.ContainsKey($uid)) { $entry = $recordEntries[$uid] $entry.OwnerFolder += $path } else { $type = [KeeperSecurity.Utils.RecordTypesUtils]::KeeperRecordType($r) $publicInfo = [KeeperSecurity.Utils.RecordTypesUtils]::KeeperRecordPublicInformation($r) $entry = [PSCustomObject]@{ PSTypeName = "KeeperSecurity.Commander.RecordEntry$(if ($SkipGrouping.IsPresent) {'Flat'} else {''})" Uid = $r.Uid Name = $r.Title Shared = $r.Shared Owner = $r.Owner Type = $type PublicInformation = $publicInfo HasAttachments = ($vault.RecordAttachments($r).Count -gt 0) SortGroup = 1 } if ($SkipGrouping.IsPresent) { Add-Member -InputObject $entry -NotePropertyName OwnerFolder -NotePropertyValue @($path) } else { Add-Member -InputObject $entry -NotePropertyName OwnerFolder -NotePropertyValue $path } $recordEntries[$uid] = $entry $entry = $null } } } } } } if ($recordEntries) { $entries += $recordEntries.Values } if ($entries) { $sortedEntries = if ($SkipGrouping.IsPresent) { @($entries | Sort-Object SortGroup, Name) } else { @($entries | Sort-Object OwnerFolder, SortGroup, Name) } $useFormattedOutput = $List.IsPresent -or $PSBoundParameters.ContainsKey('Format') -or $PSBoundParameters.ContainsKey('Output') if (-not $useFormattedOutput) { $sortedEntries return } $selectedFormat = if ($PSBoundParameters.ContainsKey('Format')) { $Format } else { 'table' } $result = foreach ($entry in $sortedEntries) { if ($entry.PSTypeNames -contains 'KeeperSecurity.Commander.FolderEntry' -or $entry.PSTypeNames -contains 'KeeperSecurity.Commander.FolderEntryFlat') { [PSCustomObject][ordered]@{ EntryType = 'Folder' Uid = $entry.Uid Name = $(if ($entry.Name) { $entry.Name } else { '' }) FolderType = $entry.FolderType Shared = $entry.Shared OwnerFolder = $entry.OwnerFolder RecordType = '' Description = '' Owner = '' HasAttachments = '' } } else { [PSCustomObject][ordered]@{ EntryType = 'Record' Uid = $entry.Uid Name = $(if ($entry.Name) { $entry.Name } else { '' }) FolderType = '' Shared = $entry.Shared OwnerFolder = $entry.OwnerFolder RecordType = $entry.Type Description = $entry.PublicInformation Owner = $entry.Owner HasAttachments = $entry.HasAttachments } } } if ($Output) { switch ($selectedFormat) { 'json' { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } 'csv' { $result | Export-Csv -Path $Output -NoTypeInformation -Encoding utf8 } default { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } Write-Host "Output written to $Output" return } switch ($selectedFormat) { 'json' { $result | ConvertTo-Json -Depth 5 } 'csv' { $result | ConvertTo-Csv -NoTypeInformation } default { $result | Format-Table -AutoSize } } } } Register-ArgumentCompleter -CommandName Get-KeeperChildItem -ParameterName Path -ScriptBlock $Keeper_FolderPathRecordCompleter New-Alias -Name kdir -Value Get-KeeperChildItem function Get-KeeperObject { <# .Synopsis Get Keeper object by Uid .Parameter Uid Keeper UID .Parameter ObjectType One of the following Record, SharedFolder, Folder, Team .Parameter PropertyName Return object property not the entire object #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)][string[]] $Uid, [string] [ValidateSet('Record' , 'SharedFolder', 'Folder', 'Team')] $ObjectType, [string] $PropertyName ) Begin { [KeeperSecurity.Vault.VaultOnline]$vault = getVault $testRecord = if ($ObjectType) { $ObjectType -eq 'Record' } else { $true } $testSharedFolder = if ($ObjectType) { $ObjectType -eq 'SharedFolder' } else { $true } $testFolder = if ($ObjectType) { $ObjectType -eq 'Folder' } else { $true } $testTeam = if ($ObjectType) { $ObjectType -eq 'Team' } else { $true } } Process { ForEach ($oid in $Uid) { if ($testRecord) { [KeeperSecurity.Vault.KeeperRecord] $record = $null if ($vault.TryGetKeeperRecord($oid, [ref]$record)) { if ($PropertyName) { $mp = $record | Get-Member -MemberType Properties -Name $PropertyName if ($mp) { $record | Select-Object -ExpandProperty $PropertyName } } else { $record } continue } } if ($testSharedFolder) { [KeeperSecurity.Vault.SharedFolder] $sf = $null if ($vault.TryGetSharedFolder($oid, [ref]$sf)) { if ($PropertyName) { $mp = $sf | Get-Member -MemberType Properties -Name $PropertyName if ($mp) { $sf | Select-Object -ExpandProperty $PropertyName } } else { $sf } continue } } if ($testFolder) { [KeeperSecurity.Vault.FolderNode] $f = $null if ($vault.TryGetFolder($oid, [ref]$f)) { if ($PropertyName) { $mp = $f | Get-Member -MemberType Properties -Name $PropertyName if ($mp) { $f | Select-Object -ExpandProperty $PropertyName } } else { $f } continue } } if ($testTeam) { [KeeperSecurity.Vault.Team] $t = $null if ($vault.TryGetTeam($oid, [ref]$t)) { if ($PropertyName) { $mp = $t | Get-Member -MemberType Properties -Name $PropertyName if ($mp) { $t | Select-Object -ExpandProperty $PropertyName } } else { $t } continue } ensureAvalableLoaded [KeeperSecurity.Vault.TeamInfo] $teamInfo = $null $teamInfo = $Script:Context.AvailableTeams | Where-Object { $_.TeamUid -ceq $oid } | Select-Object -First 1 if ($teamInfo) { if ($PropertyName) { $mp = $teamInfo | Get-Member -MemberType Properties -Name $PropertyName if ($mp) { $teamInfo | Select-Object -ExpandProperty $PropertyName } } else { $teamInfo } continue } } if ($testRecord) { $nsfRecord = resolveKeeperNSFRecord -Identifier $oid -Vault $vault if ($nsfRecord) { if ($PropertyName) { $mp = $nsfRecord | Get-Member -MemberType Properties -Name $PropertyName -ErrorAction SilentlyContinue if ($mp) { $nsfRecord | Select-Object -ExpandProperty $PropertyName } } else { $nsfRecord } continue } } if ($testFolder) { $nsfFolder = resolveKeeperNSFFolder -Identifier $oid -Vault $vault if ($nsfFolder) { if ($PropertyName) { $mp = $nsfFolder | Get-Member -MemberType Properties -Name $PropertyName -ErrorAction SilentlyContinue if ($mp) { $nsfFolder | Select-Object -ExpandProperty $PropertyName } } else { $nsfFolder } continue } } } } } New-Alias -Name ko -Value Get-KeeperObject function parseKeeperPath { Param ( [string[]]$components, [KeeperSecurity.Vault.VaultOnline]$vault, [KeeperSecurity.Vault.FolderNode]$folder ) if ($components) { if (!$components[0]) { $folder = $vault.RootFolder $_, $components = $components } while ($components) { $resume = $false $component, $rest = $components if ($component -eq '..') { if ($folder.ParentUid) { $resume = $vault.TryGetFolder($folder.ParentUid, [ref]$folder) } else { $folder = $vault.RootFolder $resume = $true } } elseif (!$component -or $component -eq '.') { $resume = $true } else { foreach ($x in $folder.Subfolders) { [KeeperSecurity.Vault.FolderNode] $subfolder = $null if ($vault.TryGetFolder($x, [ref]$subfolder)) { if ($subfolder.Name -eq $component) { $resume = $true $folder = $subfolder break } } } } if ($resume) { $components = $rest } else { break } } $folder $components -join $Script:PathDelimiter } else { $folder $path } } function splitKeeperPath { Param ([string] $path) [bool]$isDelimiter = $false [string]$component = '' foreach ($x in $path.ToCharArray()) { if ($x -eq $Script:PathDelimiter) { if ($isDelimiter) { $component += $x $isDelimiter = $false } else { $isDelimiter = $true } } else { if ($isDelimiter) { $component $component = '' $isDelimiter = $false } $component += $x } } $component if ($isDelimiter) { '' } } function exportKeeperNode { Param ([KeeperSecurity.Vault.FolderNode] $folder) [PSCustomObject]@{ PSTypeName = 'KeeperSecurity.Commander.FolderInfo' FolderUid = $folder.FolderUid Path = getVaultFolderPath $vault $folder.FolderUid Name = $folder.Name ParentUid = $folder.ParentUid FolderType = $folder.FolderType } } function escapePathComponent { Param ([string] $component) $component = $component -replace '\\', '\\' $component = $component -replace '''', '''''' if ($component -match '[\s'']') { "'${component}'" } else { $component } } function getVaultFolderPath { Param ( [KeeperSecurity.Vault.VaultOnline]$vault, [string] $folderUid ) $comps = @() traverseFolderToRoot $vault $folderUid ([ref]$comps) $path = '' if ($comps) { [Array]::Reverse($comps) $comps += '' $path = ($comps | ForEach-Object { $_ -replace [Regex]::Escape($Script:PathDelimiter), "${Script:PathDelimiter}${Script:PathDelimiter}" }) -join $Script:PathDelimiter } "${Script:PathDelimiter}${path}" } function traverseFolderToRoot ([KeeperSecurity.Vault.VaultOnline]$vault, [string] $folderUid, [ref] $components) { if ($folderUid) { [KeeperSecurity.Vault.FolderNode]$folder = $null if ($vault.TryGetFolder($folderUid, [ref]$folder)) { $components.Value += $folder.Name traverseFolderToRoot $vault $folder.ParentUid $components } } } function Export-KeeperVault { <# .Synopsis Export vault data to a JSON file .Parameter FileName JSON export filename (will automatically add .json extension if not present) .Parameter Force Overwrite existing file without prompting .Parameter ExcludeSharedFolders Exclude shared folders from export .Description Exports all vault records and optionally shared folders to a JSON file. The JSON format is compatible with Keeper's import functionality. .Example Export-KeeperVault -FileName "vault_backup" Exports vault to "vault_backup.json" and prompts if file exists .Example Export-KeeperVault -FileName "vault.json" -Force Exports vault and overwrites existing file without prompting .Example Export-KeeperVault -FileName "records_only.json" -ExcludeSharedFolders Exports only records, excluding shared folders #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, Position = 0)] [string] $FileName, [Parameter(Mandatory = $false)] [switch] $Force, [Parameter(Mandatory = $false)] [switch] $ExcludeSharedFolders ) [KeeperSecurity.Vault.VaultOnline]$vault = getVault if (-not $FileName.EndsWith(".json", [StringComparison]::OrdinalIgnoreCase)) { $FileName += ".json" } if ((Test-Path $FileName) -and -not $Force) { $response = Read-Host "File `"$FileName`" already exists. Overwrite? (y/n)" if ($response -notmatch '^y(es)?$') { Write-Host "Export cancelled." return } } Write-Host "Exporting vault data..." $includeSharedFolders = -not $ExcludeSharedFolders.IsPresent $jsonContent = [KeeperSecurity.Vault.KeeperExport]::ExportVaultToJson( $vault, $null, $includeSharedFolders ) $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FileName) $directory = [System.IO.Path]::GetDirectoryName($fullPath) if (-not [string]::IsNullOrEmpty($directory) -and -not (Test-Path $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } [System.IO.File]::WriteAllText($fullPath, $jsonContent) Write-Debug "Exported to $fullPath" $fileInfo = Get-Item $fullPath $recordCount = ($vault.KeeperRecords | Where-Object { $_.Version -eq 2 -or $_.Version -eq 3 }).Count $sharedFolderCount = if ($ExcludeSharedFolders) { 0 } else { $vault.SharedFolders.Count } Write-Host "" Write-Host "Export Summary:" Write-Host " Records Exported: $recordCount" if (-not $ExcludeSharedFolders) { Write-Host " Shared Folders: $sharedFolderCount" } Write-Host " File Size: $($fileInfo.Length.ToString('N0')) bytes" Write-Host " Output File: $fullPath" Write-Host "" Write-Host "Export completed successfully." -ForegroundColor Green } New-Alias -Name kexport -Value Export-KeeperVault function Script:ConvertTo-ImportJsonValue { <# .SYNOPSIS Converts PowerShell/ConvertFrom-Json output into KeeperSecurity.Commands.ImportJsonValue for KeeperImport.LoadJsonDictionary. (Not for NSF record Fields bags — use ConvertTo-NSFJsonValue.) #> Param([Parameter(Mandatory = $true)][AllowNull()] $InputObject) if ($null -eq $InputObject) { return [KeeperSecurity.Commands.ImportJsonValue]::Null } $base = $InputObject if ($InputObject -is [System.Management.Automation.PSObject] -and $null -ne $InputObject.PSObject) { $bo = $InputObject.PSObject.BaseObject if ($null -ne $bo) { $base = $bo } } if ($base -is [string]) { return [KeeperSecurity.Commands.ImportJsonValue]::FromString([string]$base) } if ($base -is [bool]) { return [KeeperSecurity.Commands.ImportJsonValue]::FromBoolean([bool]$base) } if ($base -is [byte] -or $base -is [sbyte] -or $base -is [int16] -or $base -is [uint16] -or $base -is [int] -or $base -is [uint32] -or $base -is [int64] -or $base -is [uint64] -or $base -is [single] -or $base -is [double] -or $base -is [decimal]) { # Store as string to avoid double precision loss (e.g. large numeric uids). return [KeeperSecurity.Commands.ImportJsonValue]::FromString([string]$base) } if ($base -is [KeeperSecurity.Commands.ImportJsonValue]) { return $base } if ($base -is [System.Collections.IDictionary]) { $ht = New-Object 'System.Collections.Generic.Dictionary[string,KeeperSecurity.Commands.ImportJsonValue]' foreach ($key in $base.Keys) { $ht[[string]$key] = ConvertTo-ImportJsonValue -InputObject $base[$key] } return [KeeperSecurity.Commands.ImportJsonValue]::FromObject($ht) } if ($base -is [System.Collections.IEnumerable]) { $list = New-Object 'System.Collections.Generic.List[KeeperSecurity.Commands.ImportJsonValue]' foreach ($item in $base) { $list.Add((ConvertTo-ImportJsonValue -InputObject $item)) | Out-Null } return [KeeperSecurity.Commands.ImportJsonValue]::FromArray($list) } if ($null -ne $InputObject.PSObject -and $InputObject.PSObject.Properties.Count -gt 0) { $ht = New-Object 'System.Collections.Generic.Dictionary[string,KeeperSecurity.Commands.ImportJsonValue]' foreach ($prop in $InputObject.PSObject.Properties) { $ht[$prop.Name] = ConvertTo-ImportJsonValue -InputObject $prop.Value } return [KeeperSecurity.Commands.ImportJsonValue]::FromObject($ht) } return [KeeperSecurity.Commands.ImportJsonValue]::FromString([string]$base) } function Script:Repair-ImportJsonSingleElementArrays { <# .SYNOPSIS PowerShell unwraps single-element arrays to the element; restore records/shared_folders as arrays. #> Param( [Parameter(Mandatory = $true)] [KeeperSecurity.Commands.ImportJsonValue] $JsonValue ) if ($null -eq $JsonValue -or $JsonValue.Kind -ne [KeeperSecurity.Commands.ImportJsonValue+JsonKind]::Object) { return $JsonValue } $changed = $false $map = New-Object 'System.Collections.Generic.Dictionary[string,KeeperSecurity.Commands.ImportJsonValue]' foreach ($key in $JsonValue.ObjectValue.Keys) { $existing = $JsonValue.ObjectValue[$key] if (($key -eq 'records' -or $key -eq 'shared_folders') -and $null -ne $existing -and $existing.Kind -eq [KeeperSecurity.Commands.ImportJsonValue+JsonKind]::Object) { $single = New-Object 'System.Collections.Generic.List[KeeperSecurity.Commands.ImportJsonValue]' $single.Add($existing) | Out-Null $map[$key] = [KeeperSecurity.Commands.ImportJsonValue]::FromArray($single) $changed = $true } else { $map[$key] = $existing } } if (-not $changed) { return $JsonValue } return [KeeperSecurity.Commands.ImportJsonValue]::FromObject($map) } function Script:Read-KeeperImportFile { Param( [Parameter(Mandatory = $true)] [string] $JsonText ) if ([string]::IsNullOrWhiteSpace($JsonText)) { throw "JSON text cannot be empty." } # ConvertFrom-Json + ImportJsonValue preserves nested custom_fields. # JsonUtils.ParseJson / DataContractJsonSerializer turns those into empty System.Object. $parsed = $JsonText | ConvertFrom-Json -ErrorAction Stop $jsonValue = ConvertTo-ImportJsonValue -InputObject $parsed if ($null -eq $jsonValue -or $jsonValue.Kind -ne [KeeperSecurity.Commands.ImportJsonValue+JsonKind]::Object) { throw "Import JSON root must be an object." } $jsonValue = Repair-ImportJsonSingleElementArrays -JsonValue $jsonValue return [KeeperSecurity.Vault.KeeperImport]::LoadJsonDictionary($jsonValue) } function Import-KeeperVault { <# .Synopsis Import vault data from a JSON file .Parameter FileName JSON import filename (from Export-KeeperVault or compatible format). When used with -DownloadSampleRecords, this is the output path (default: keeper-import-sample.json). .Parameter DownloadSampleRecords Writes a sample import JSON file to disk and exits without importing. The sample uses the same records and shared_folders structure as Export-KeeperVault / kimport. Edit the file with your own data, then import with Import-KeeperVault -FileName. .Parameter Force Proceed without confirming when file is large .Description Imports records and shared folders from a JSON file into the vault. Use a file produced by Export-KeeperVault or the same JSON structure (records, shared_folders). Nested custom_fields objects (e.g. $host, $paymentCard) are preserved; the same record JSON shape is used by NSF batch create/update (Add-KeeperNSFRecords / Edit-KeeperNSFRecords). .Example Import-KeeperVault -FileName "vault_backup.json" Imports vault data from vault_backup.json .Example Import-KeeperVault -DownloadSampleRecords Writes keeper-import-sample.json in the current directory .Example Import-KeeperVault -DownloadSampleRecords -FileName "my-import-template.json" Writes a sample import file to my-import-template.json .Example Import-KeeperVault -FileName "restore.json" -Force Imports without size confirmation #> [CmdletBinding(DefaultParameterSetName = 'Import')] Param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Import')] [Parameter(Mandatory = $false, Position = 0, ParameterSetName = 'DownloadSample')] [string] $FileName, [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')] [switch] $DownloadSampleRecords, [Parameter(Mandatory = $false)] [switch] $Force ) if ($DownloadSampleRecords) { if (-not $FileName) { $FileName = 'keeper-import-sample.json' } $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FileName) $parentDir = Split-Path -Parent $fullPath if ($parentDir -and -not (Test-Path -LiteralPath $parentDir)) { New-Item -ItemType Directory -Path $parentDir -Force | Out-Null } $sampleJson = Get-KeeperImportSampleJson $utf8NoBom = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText($fullPath, $sampleJson, $utf8NoBom) Write-Host "Sample import file written to: $fullPath" -ForegroundColor Green Write-Host "Edit the file with your records and shared folders, then run:" Write-Host " Import-KeeperVault -FileName `"$FileName`"" return } if (-not $FileName) { Write-Error "FileName is required when -DownloadSampleRecords is not specified." return } [KeeperSecurity.Vault.VaultOnline]$vault = getVault if (-not $vault) { Write-Error "Not connected to Keeper. Please login first." return } $MaxFileSizeBytes = 50 * 1024 * 1024 # 50 MB $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FileName) if (-not (Test-Path -LiteralPath $fullPath -PathType Leaf)) { Write-Error "File `"$fullPath`" not found" return } $fileInfo = Get-Item -LiteralPath $fullPath if ($fileInfo.Length -gt $MaxFileSizeBytes -and -not $Force) { $maxMB = [math]::Round($MaxFileSizeBytes / (1024 * 1024), 2) $sizeMB = [math]::Round($fileInfo.Length / (1024 * 1024), 2) $response = Read-Host "File size ($sizeMB MB) exceeds $maxMB MB. Continue? (y/n)" if ($response -notmatch '^y(es)?$') { Write-Host "Import cancelled." return } } try { $jsonText = [System.IO.File]::ReadAllText($fullPath, [System.Text.Encoding]::UTF8) $importFile = Read-KeeperImportFile -JsonText $jsonText } catch { Write-Error "Error reading or parsing JSON file: $_" return } $recordCount = if ($importFile.Records) { $importFile.Records.Length } else { 0 } $sharedFolderCount = if ($importFile.SharedFolders) { $importFile.SharedFolders.Length } else { 0 } if ($recordCount -eq 0 -and $sharedFolderCount -eq 0) { Write-Error "The import file contains no valid records or shared folders." return } Write-Host "Importing $recordCount record(s), $sharedFolderCount shared folder(s)..." try { $result = [KeeperSecurity.Vault.KeeperImport]::ImportJson($vault, $importFile).GetAwaiter().GetResult() Write-Host "" Write-Host "Import Summary:" if ($result.SharedFolderCount -gt 0) { Write-Host " Shared Folders: $($result.SharedFolderCount)" } if ($result.FolderCount -gt 0) { Write-Host " Folders: $($result.FolderCount)" } if ($result.LegacyRecordCount -gt 0) { Write-Host " Legacy Records: $($result.LegacyRecordCount)" } if ($result.TypedRecordCount -gt 0) { Write-Host " Typed Records: $($result.TypedRecordCount)" } if ($result.UpdatedRecordCount -gt 0) { Write-Host " Records Updated: $($result.UpdatedRecordCount)" } if ($result.UpdatedFolderCount -gt 0) { Write-Host " Folders Updated: $($result.UpdatedFolderCount)" } if ($result.FolderFailure.Count -gt 0) { Write-Host " Folder Failures: $($result.FolderFailure.Count)" -ForegroundColor Yellow } if ($result.RecordFailure.Count -gt 0) { Write-Host " Record Failures: $($result.RecordFailure.Count)" -ForegroundColor Yellow } if ($result.MembershipFailure.Count -gt 0) { Write-Host " Membership Failures: $($result.MembershipFailure.Count)" -ForegroundColor Yellow } Write-Host "" Write-Host "Import completed successfully." -ForegroundColor Green } catch { Write-Error "Import failed: $_" } } New-Alias -Name kimport -Value Import-KeeperVault # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCQboms0DyUesu0 # OtuJvXVMXYXNcyB4oSMXi5YUojl0DKCCITswggWNMIIEdaADAgECAhAOmxiO+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 # BDEiBCAm65H5GAlzMdWRmU9+mJiGtbX3dwI8+qwGMxU41qqPATANBgkqhkiG9w0B # AQEFAASCAYBrZygFmbWU3S2qYVJGjCVQDlxpnaBHzWjFyzimV75OJzc45tOKk+GB # F/ZOjNq6X7nSqM72a3heRsL7reAerWn6p/uMnPLV6spvi2+TM2q5ue0krOtrdJiL # r0P0LqMD3QVU7YGl4/RKGkwIm40dPhYt3pwdmdsYkEg/GUXRySVQ2uFL/WjWMcmL # yUB/UXDu1aLPfX1roA1j//qFMzRMBHsOJoN51klz+Wtb1XQv97KcNgl4Y6iHOHp+ # POu+hcBDyCUjNBlL8YgvUlg7PnC0g0dYZmzQ2HROdbOdR1dkJKbxQvYG5kl8QahC # /vNssrwwlaVzC1YWJGc4hWZVrbX/nnSVn7FFi2MyQsXu78lTXpZBbMH7mqnctr3J # wL8CdvHA3ZUqAxEELZeYx9MeZSIFVq5MQt8LZ5aPuQs3w1o3MGhbcZEixJ1XJiH/ # x5+DuaoMqWZgCdQ3BxN9pN4nWusTVHVY19GnLkrLz+Uw0m2EN6xNAWA1ChgGeoks # fBZIquvajmKhggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwODE0MjExOTE5WjAv # BgkqhkiG9w0BCQQxIgQgRi7qw/QKcLRKAypls4h8iRd5WGZ8U12aho1ih1YSYEMw # DQYJKoZIhvcNAQEBBQAEggIAw0SAEDNwf8syqI5L/0ygFKvoqb3n6PryUX9GBiml # BOOhC3gOHyGhtnFD4EbknJfWHJiyJfpBqXQq3FXaQFQzq4H5OUIDQdUiZWclIksg # CT2hx9cZPNGZpBrqdXhkHG3PzWgoXGuswXK00bzrFqhRpkyJhxY/yITIjOKTBNC5 # LBzEukcf77M4ab+/9EiCvRQ0cMiYI1oqeA08NbBM2Koi6vPOSM25hQk+TLW0B0j+ # BnA7Abqhe9CYXWxqWk1RwYj8Zk3ODxqN1WiWcdhbLGkrubUsCBbPS391l8l+QR60 # j3z1iCEzNJhPaXQUlJHhyQ3HrGpVd7PndGLYwwkJM27MNthrymxrX4T37JdIbHaU # n6sewg0G3Df8Q+SY7ti9+hQN/n6jtAOYtxkR0zpnxA596mieF4i4LjdQxoZuCfv4 # jzTzVj4bQYYRe0cAIc717bcdH1Dbv1IPxOmcs1cWMfp5nMy9zhr9aAU+Mabx56PR # QlrxJsbBYTQnjlFiuctHj2O7jWW/p6WoqrM0dagK1ptKTpG5p8GXQyyfhroqZmgj # 128qkIW9r5ABgqOdBxjl3KjMM7QzSP+vNXD69lLXfXNKImbT33HBgxNGc8Vjl/Bt # V4371eFlsasiB70zDyvDFihHs7BOBTj/VCauA96r6xfA3EhcjRcb5rHa6XG5OvkR # BHo= # SIG # End signature block |