lib/TMD.Credentials.ps1
function Add-StoredCredential { <# .SYNOPSIS Stores the given credential on the local hard drive .DESCRIPTION Exports the given credential object to an XML file which is saved in the TMD_Files directory .PARAMETER Name The name to be used when referencing the stored credential .PARAMETER Credential The credential object to be stored .PARAMETER EncryptionKey An optional key to be used to encrypt the credential using AES. The specified key must be either a base64 encoded string or a byte array with a size of 128, 192, or 256 bits .PARAMETER Passthru Returns the PSCredential object created and stored with the Name provided .EXAMPLE Get-Credential | Add-StoredCredential -Name 'MyCreds' .EXAMPLE $Key = Get-AesKey -Size 128 Add-StoredCredential -Name 'MyEncryptedCreds' -EncryptionKey $Key .OUTPUTS None #> [CmdletBinding(DefaultParameterSetName = 'EncryptedWithDPAPI')] [Alias('New-StoredCredential')] param( [CmdletBinding()] [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('CredentialName')] [String]$Name, [Parameter(Mandatory = $false, Position = 1, ValueFromPipelineByPropertyName = $true)] [PSCredential]$Credential, [Parameter(Mandatory = $false, Position = 2, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'EncryptedWithKey')] [Alias('Key')] [Object]$EncryptionKey, ## Allow returning the PSCredential Object (useful when prompted for new) [switch]$Passthru ) begin { # Validate the encryption key provided if ($PSCmdlet.ParameterSetName -eq 'EncryptedWithKey') { # Make sure the encryption key is a string or a byte array if ($EncryptionKey.GetType() -notin [String], [Byte[]]) { throw "EncryptionKey must be either a Base64 encoded string or byte array with a size of 128, 192, or 256 bits" } # Convert the base64 string into a byte array if ($EncryptionKey.GetType() -eq [String]) { $EncryptionKey = [Convert]::FromBase64String($EncryptionKey) } # Make sure the byte array is the correct size if ($EncryptionKey.Count -notin 16, 24, 32) { throw "The encryption key must have a size of 128, 192, or 256 bits" } } } process { if (!$Credential) { if (Test-TMDIsRunningAction) { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [Exception]::new("A Credential must be provided"), "TMD.CRED01", [System.Management.Automation.ErrorCategory]::InvalidArgument, $null ) $PSCmdlet.WriteError($ErrorRecord) } else { $Credential = (Get-Credential) } } ## Encrypt the credential using either AES or Windows DP API and save to disk $CredentialFilePath = Join-Path $Global:UserPaths.Credentials ($Name + ($PSCmdlet.ParameterSetName -eq 'EncryptedWithKey' ? '.ecredential' : '.credential')) if ($PSCmdlet.ParameterSetName -eq 'EncryptedWithKey') { $EncryptedPassword = ConvertFrom-SecureString -SecureString $Credential.Password -Key $EncryptionKey $EncryptedCredential = [PSCustomObject]@{ UserName = $Credential.UserName Password = $EncryptedPassword } New-Item -Path $CredentialFilePath -ItemType File -Value ($EncryptedCredential | ConvertTo-Json) -Force | Out-Null } else { Export-Clixml -InputObject $Credential -Path $CredentialFilePath } ## Return the PSCredential Object if($Passthru.isPresent){ $Credential } } } Set-Alias -Name 'New-StoredCredential' -Value 'Add-StoredCredential' -Option AllScope -Scope Global function Remove-StoredCredential { <# .SYNOPSIS Removes a previously stored credential from the local hard drive .DESCRIPTION Overwrites a stored .credential file multiple times with a random bytestream before deleting it from the local hard drive .PARAMETER Name The name of the credential to be removed .PARAMETER OverwriteCount The number of times to overwrite the file before removing it .EXAMPLE Remove-StoredCredential -Name 'MyCreds' .EXAMPLE Get-StoredCredential | Remove-StoredCredential .OUTPUTS None #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('CredentialName')] [ArgumentCompleter({ Get-StoredCredential -List })] [String]$Name, [Parameter(Mandatory = $false)] [Int]$OverwriteCount = 500 ) process { $CredentialFiles = Get-ChildItem -Path $Global:UserPaths.Credentials -File | Where-Object { $_.BaseName -eq $Name } if ($CredentialFiles) { foreach ($File in $CredentialFiles) { Write-Verbose "Processing file '$($File.FullName)'" $BufferSize = $Name.Length $RandomDataBuffer = [System.Byte[]]::new($BufferSize) Write-Verbose "Overwriting the data within the file $OverwriteCount time(s)" for ($i = 0; $i -lt $OverwriteCount; $i++) { $Random = [System.Random]::new() $Random.NextBytes($RandomDataBuffer) | Set-Content -Path $File.FullName -AsByteStream -Force } Write-Verbose "Removing the file" $File | Remove-Item -Force } } else { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [Exception]::new("A stored credential with the name '$Name' does not exist."), "TMD.CRED02", [System.Management.Automation.ErrorCategory]::InvalidArgument, $_.FullName ) $PSCmdlet.WriteError($ErrorRecord) } } } function Get-StoredCredential { <# .SYNOPSIS Gets the specified credential that is stored on the local hard drive .DESCRIPTION This function either retrieves a specified credential or a list of all stored credentials .PARAMETER Name The name of the credential to get .PARAMETER EncryptionKey The key that was used to encrypt the credential .PARAMETER List Switch specifying that a list of the credentials stored on the local hard drive should be returned .EXAMPLE $StoredCredential = Get-StoredCredential -Name 'MyCreds' .EXAMPLE $StoredCredential = Get-StoredCredential -Name 'MyCreds' -EncryptionKey .OUTPUTS A PSCredential object containing the specified credential #> [CmdletBinding(DefaultParameterSetName = "Single")] param( [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "Single")] [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true, ParameterSetName = "EncryptedWithKey")] [ArgumentCompleter({ Get-StoredCredential -List })] [Alias('CredentialName')] [String]$Name, [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'EncryptedWithKey')] [Alias('Key')] [Object]$EncryptionKey, [Parameter(Mandatory = $false, ParameterSetName = "List")] [switch]$List ) begin { function DecryptCredential { param ( [Parameter(Mandatory = $true)] [System.IO.FileInfo]$File, [Parameter(Mandatory = $false)] [Byte[]]$Key ) if ($File.Extension -eq '.credential') { Import-Clixml -Path $File.FullName } elseif ($null -ne $Key) { $EncryptedCredential = Get-Content -Path $File.FullName | ConvertFrom-Json try { $SecurePassword = $EncryptedCredential.Password | ConvertTo-SecureString -Key $Key -ErrorAction Stop [PSCredential]::new($EncryptedCredential.UserName, $SecurePassword) } catch { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [Exception]::new("Could not decrypt credential with provided key"), "TMD.CRED03", [System.Management.Automation.ErrorCategory]::InvalidArgument, $Key ) $PSCmdlet.WriteError($ErrorRecord) } } } # Validate the encryption key provided if ($PSCmdlet.ParameterSetName -eq 'EncryptedWithKey') { # Make sure the encryption key is a string or a byte array if ($EncryptionKey.GetType() -notin [String], [Byte[]]) { throw "EncryptionKey must be either a Base64 encoded string or byte array with a size of 128, 192, or 256 bits" } # Convert the base64 string into a byte array if ($EncryptionKey.GetType() -eq [String]) { $EncryptionKey = [Convert]::FromBase64String($EncryptionKey) } # Make sure the byte array is the correct size if ($EncryptionKey.Count -notin 16, 24, 32) { throw "The encryption key must have a size of 128, 192, or 256 bits" } } } process{ try { $CredentialFiles = Get-ChildItem -Path $Global:UserPaths.Credentials -File if (-not $Name -or $List) { $CredentialFiles | ForEach-Object { if ($List) { $_.Name -replace $_.Extension, '' } else { [PSCustomObject]@{ Name = $_.Name.Replace($_.Extension, '') Credential = (DecryptCredential -File $_ -Key $EncryptionKey) Encrypted = ($_.Extension -eq '.ecredential') } } } } else { $CredentialFilePath = Join-Path $Global:UserPaths.Credentials ($Name + ($PSCmdlet.ParameterSetName -eq 'EncryptedWithKey' ? '.ecredential' : '.credential')) if (Test-Path -Path $CredentialFilePath) { DecryptCredential -File (Get-Item -Path $CredentialFilePath) -Key $EncryptionKey } } } catch { $PSCmdlet.WriteError($_) } } } function Copy-StoredCredential { <# .SYNOPSIS Copies a stored credential .DESCRIPTION This function copies a StoredCredential from the -ExistingCredentialName to -NewCredentialName .PARAMETER ExistingCredentialName The name of the source StoredCredentail to copy .PARAMETER NewCredentialName The name of the new StoredCredential to create .PARAMETER Passthru Returns the new credential .EXAMPLE Copy-StoredCredential -ExistingCredentialName 'TMServer' -NewCredentialName 'NewTMServer' .EXAMPLE Copy-StoredCredential -ExistingCredentialName 'TMServer' -NewCredentialName 'NewTMServer' -Passthru .OUTPUTS When Passthru is used, a PSCredential is returned #> [CmdletBinding()] # Always add CmdletBinding to expose the common Cmdlet variables [OutputType([Bool])] # Add this if the function has an output param( ## TODO: Add an Argument Completer. This will require additional work on the Get- command to allow matching. [Parameter(Mandatory = $true, Position = 0)] [ValidateScript({$_ -in (Get-StoredCredential -List)})] [String]$ExistingCredentialName, [Parameter(Mandatory = $true, Position = 1)] [String]$NewCredentialName, [Switch]$Passthru ) ## Get and Set the Credential $PSCredential = Get-StoredCredential -Name $ExistingCredentialName Add-StoredCredential -Name $NewCredentialName -Credential $PSCredential ## Return the credential if desired if($Passthru){ return $PSCredential } } function Get-AesKey { <# .SYNOPSIS Creates a random base64 encoded string or byte array that can be used as an AES key .DESCRIPTION Creates a byte array of the specified size that can be used as an AES key. Can optionally be output as a base54 encoded string .PARAMETER Size The size in Bits of the output encryption key .PARAMETER Format The format/type of the output encryption key .EXAMPLE $Key = Get-RandomEncryptionKey New-StoredCredential -Name 'ExampleCred' -EncryptionKey $Key .OUTPUTS Either a Base64 encoded String or a Byte Array of the specified size/length #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [ValidateSet(128, 192, 256)] [Int32]$Size = 256, [Parameter(Mandatory = $false, Position = 1)] [ValidateSet('String', 'Bytes')] [String]$Format = 'Bytes' ) $Aes = [System.Security.Cryptography.Aes]::Create() $Aes.KeySize = $Size if ($Format -eq 'String') { [Convert]::ToBase64String($Aes.Key) } else { , $Aes.Key } } function Test-IsAdmin { <# .SYNOPSIS Checks if the current user has administrative rights .EXAMPLE $Message = Test-IsAdmin ? "User is an administrator" : "User is not an administrator" Write-Host $Message .OUTPUTS True if the current user is in the Administrators group, otherwise false #> [CmdletBinding()] $user = [Security.Principal.WindowsIdentity]::GetCurrent(); [Security.Principal.WindowsPrincipal]::new($user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } |