Private/Credential.ps1
|
# The user's saved credentials, by their target names only. # # Whether a credential is saved for an account is read off the names Windows' Credential # Manager lists for the user who runs it, through cmdkey /list: it prints the target names # and never a secret. Nothing here calls CredEnumerate or CredRead, which hand over the # secret with the name. See ADR-0016. # # cmdkey's output is localised ("Ziel:", "Typ:" on a German Windows, "Cible :" on a French # one), so it is read by position: the first line of each indented block is the target. # Observed on a German Windows 11 26200, 2026-09-25: blocks of three or four lines, # separated by empty lines or by lines of four spaces, the persistence line sometimes # missing. First read for OutlookAuth (#24); the Nextcloud Kind reads it too (#185). function ConvertFrom-CmdkeyList { <# .SYNOPSIS The target names out of cmdkey /list output, in the order listed. Pure. .DESCRIPTION Read by position, never by label, because the labels are localised: an entry is a block of indented lines, and its first line is the target, "<label>: <target>" (German "Ziel:", French "Cible :"). The label holds no colon, the target may, so the line is split at the first one. The "LegacyGeneric:target=" style prefix cmdkey puts in front says how the entry was stored and is dropped, leaving the name Credential Manager shows. The unindented heading and any line without a colon (an empty store's message) are not entries. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyCollection()][AllowEmptyString()][string[]]$Line = @()) $inBlock = $false foreach ($l in @($Line)) { if (-not "$l".Trim()) { $inBlock = $false; continue } if ($inBlock) { continue } $inBlock = $true if ("$l" -notmatch '^\s+[^:]+:\s*(\S.*)$') { continue } $target = $Matches[1].Trim() -replace '^[A-Za-z]+:target=', '' if ($target) { $target } } } function Read-CredentialTargetName { <# .SYNOPSIS The target names of the credentials saved for whoever runs this, as cmdkey /list prints them: Targets, and Error where the store could not be listed. Asks only; reads no secret. Never throws. .DESCRIPTION Targets is $null where the store was not read, and an empty list where it was and holds nothing: a store with nothing in it is a different fact from one whose listing failed. Error is said in words, as the Report prints it: that cmdkey ended with an error instead of a list, or was not found. Never its bare exit code: cmdkey has no list of what its codes mean. #> [CmdletBinding()] [OutputType([psobject])] param() $targets = $null $problem = $null try { $listed = @(& cmdkey.exe /list 2>$null) if ($LASTEXITCODE -ne 0) { $problem = Get-Text 'Value.Credential.CmdkeyFailed' } else { $targets = @(ConvertFrom-CmdkeyList -Line $listed) } } catch [System.Management.Automation.CommandNotFoundException] { $problem = Get-Text 'Value.Credential.CmdkeyMissing' } catch { $problem = $_.Exception.Message } [pscustomobject]@{ Targets = $targets; Error = $problem } } |