SecretManagement.1Password.Extension/SecretManagement.1Password.Extension.psm1
using namespace Microsoft.PowerShell.SecretManagement function Invoke-OpCommand{ <# .SYNOPSIS Calls the 1Password CLI console application and returns an object with three properties: StdOut: Text, excluding errors, returned by the application StdErr: Error text returned by the application, if any. ExitCode: Exit code of the command. ExitCode=0 means "Success". .DESCRIPTION Calling the op.exe application directly from PowerShell (with the prefix "&") doesn't allow to capture the text outputted in case of an error. This function solves the issue and suppress the need to redirecting the error text to "$null", to prevent its display to the user. .PARAMETER ArgumentList Argument list to be passed to the 1Password CLI console application. #> param( [Parameter( Mandatory=$true, Position=0, HelpMessage="Argument list to be passed to the 1Password CLI console application.")] [String[]]$ArgumentList ) $pinfo = [System.Diagnostics.ProcessStartInfo]::new(); $pinfo.FileName = "op.exe"; $pinfo.RedirectStandardError = $true; $pinfo.RedirectStandardOutput = $true; $pinfo.UseShellExecute = $false; $pinfo.Arguments = ($ArgumentList -join " "); $p = New-Object System.Diagnostics.Process; $p.StartInfo = $pinfo; $p.Start() | Out-Null; $stdout = $p.StandardOutput.ReadToEnd(); $stderr = $p.StandardError.ReadToEnd(); $p.WaitForExit(); return [PSCustomObject]@{ StdOut = $stdout; StdErr = $stderr; ExitCode = $p.ExitCode; } } function Test-SecretVault { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory)] [string]$VaultName, [Parameter(ValueFromPipelineByPropertyName)] [hashtable]$AdditionalParameters ) if (-not $VaultName) { Write-Error 'The name SecretManagement vault must be provided.' return $false } Write-Verbose "Validating the SecretManagement Vault '$($VaultName)'..." $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue if ($null -eq $secretVault){ Write-Error "The SecretManagement vault '$($VaultName)' is not registered." return $false } if ($null -eq $AdditionalParameters){ $VaultParameters = $secretVault.VaultParameters }else{ $VaultParameters = $AdditionalParameters } Write-Verbose "Validating the 1Password Vault parameters> AccountName: '$($VaultParameters.AccountName)'; OPVault: '$($VaultParameters.OPVault)'" if (-not $VaultParameters.AccountName) { Write-Warning 'The 1Password account (AccountName) is missing in the SecretManagement vault parameters.' } if (-not $VaultParameters.OPVault) { Write-Warning 'The 1Password vault name (OPVault) is missing in the SecretManagement vault parameters.' } Write-Verbose "Trying to read the 1Password vaults ..." $commandArgs = [System.Collections.ArrayList]::new(); $commandArgs.AddRange(@('vault', 'list')); if ($VaultParameters.AccountName) { $commandArgs.AddRange(@('--account', "$($VaultParameters.AccountName)")); } $commandArgs.AddRange(@('--format', 'json')); $result = Invoke-OpCommand $commandArgs; if ($result.ExitCode -ne 0){ #Error on execution Write-Error "An arror occurred while accessing 1Password: $($result.StdErr)"; return $false; }else{ Write-Verbose "1Password vaults successfully read." } $vaults = $result.StdOut | ConvertFrom-Json; if (-not $vaults) { Write-Error "No vaults were found in 1Password." return false; } if ($VaultParameters.OPVault) { $targetVault = $vaults.Where({ $_.name -eq $VaultParameters.OPVault -or $_.id -eq $VaultParameters.OPVault }) if ($targetVault){ Write-Verbose "1Password vault '$($VaultParameters.OPVault)' successfully found." return $true }else{ Write-Error "The vault '$($VaultParameters.OPVault)' was not found in 1Password." return $false } }else{ Write-Verbose "1Password contains '$($vaults.Count)' vaults." return ($vaults.Count -gt 0) } } function Get-SecretInfo { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory)] [string]$VaultName, [Parameter()] [string]$Filter, [Parameter()] [hashtable] $AdditionalParameters ) Write-Verbose "'Get-SecretInfo' invoked ..." if ($null -ne $AdditionalParameters){ $VaultParameters = $AdditionalParameters }else{ if ($null -eq $VaultName){$VaultName = ""} $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue if ($null -eq $secretVault){ Write-Error "The SecretManagement vault '$($VaultName)' is not registered." return $null } $VaultParameters = $secretVault.VaultParameters } $commandArgs = [System.Collections.ArrayList]::new(); $commandArgs.AddRange(@('item', 'list')); if ($VaultParameters.AccountName) { $commandArgs.AddRange(@('--account', "$($VaultParameters.AccountName)")); } if ($VaultParameters.OPVault) { $commandArgs.AddRange(@('--vault', "$($VaultParameters.OPVault)")); } $commandArgs.AddRange(@('--categories', '"LOGIN,PASSWORD"', '--format', 'json')); $result = Invoke-OpCommand $commandArgs; if ($result.ExitCode -eq 0){ $items = $result.StdOut -replace 'b5UserUUID', 'B5UserUUID' | ConvertFrom-Json; if (-not [string]::IsNullOrEmpty($Name)){ $items = $items | Where-Object { $_.title -eq $Name }; }else{ if ([string]::IsNullOrEmpty($Filter)){ $Filter = "*" } $items = $items | Where-Object { $_.title -like $Filter }; } }else{ $items = $null; } $keyList = [System.Collections.Generic.Dictionary[[string],[SecretInformation]]]::new(); foreach ($item in $items) { if ( $keyList.ContainsKey(($item.title).ToLower()) ) { Write-Verbose "Get-SecretInfo: An item with the same key has already been added. Key: [$($item.title)]" } else { $type = switch ($item.category) { 'LOGIN' { [SecretType]::PSCredential } 'PASSWORD' { [SecretType]::SecureString } Default { [SecretType]::Unknown } } Write-Verbose $item.title # The vault name to be returned within the SecretInformation object must be the name of the SecretManagement # vault because the SecretInformation object can be passed to the Get-Secret cmdlet to query secrets, which # will require to have the name of the SecretManagement vault. # See: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.secretmanagement/get-secret?view=ps-modules#-inputobject $keyList.Add( ` $(($item.title).ToLower()), ` [SecretInformation]::new($item.title, $type, $($VaultName)) ` ); } } return [SecretInformation[]]$keyList.Values } function Get-Secret { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter()] [string]$Name, [Parameter()] [string]$Filter, [Parameter()] [string]$VaultName, [Parameter()] [switch]$AsPlainText, [Parameter()] [hashtable] $AdditionalParameters ) Write-Verbose "'Get-Secret' invoked ..." if ($null -ne $AdditionalParameters){ $VaultParameters = $AdditionalParameters }else{ if ($null -eq $VaultName){$VaultName = ""} $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue if ($null -eq $secretVault){ Write-Error "The SecretManagement vault '$($VaultName)' is not registered." return $null } $VaultParameters = $secretVault.VaultParameters } $commandArgs = [System.Collections.ArrayList]::new(); $commandArgs.AddRange(@('item', 'get', """$($Name)""")); if ($VaultParameters.AccountName) { $commandArgs.AddRange(@('--account', "$($VaultParameters.AccountName)")); } if ($VaultParameters.OPVault) { $commandArgs.AddRange(@('--vault', "$($VaultParameters.OPVault)")); } $commandArgs.AddRange(@('--format', 'json')); $result = Invoke-OpCommand $commandArgs; if ($result.ExitCode -ne 0){ Write-Verbose $result.StdErr; return $null; # Not found } $item = $result.StdOut | ConvertFrom-Json; # Check existence of Time-based One Time Password (TOTP) $totp = -1 if ($item.fields.type -contains "OTP") { $totp = $item.fields.Where({ $_.type -eq 'OTP' }) | Select-Object -ExpandProperty totp } $password = $item.fields.Where({ $_.id -eq 'password' }) $username = $item.fields.Where({ $_.id -eq 'username' }) if ( -not [string]::IsNullOrEmpty($password.value) -and -not $AsPlainText) { [securestring]$secureStringPassword = ConvertTo-SecureString $password.value -AsPlainText -Force } $output = $null if ([string]::IsNullOrEmpty($password.value) -and -not [string]::IsNullOrEmpty($username.value)) { $output = @{UserName = $username.value } } elseif ([string]::IsNullOrEmpty($username.value)) { if ($AsPlainText) { if($totp -gt -1){ $output = @{Password = $username.value; totp = $totp } } else { $output = $username.value } } else { if($totp -gt -1){ $output = @{Password = $secureStringPassword; totp = $totp } } else { $output = $secureStringPassword } } } else { if ($AsPlainText) { if($totp -gt -1){ $output = @{UserName = $username.value; Password = $username.value; totp = $totp } } else { $output = $username.value } } else { if($totp -gt -1){ $output = @{ Credentials = [PSCredential]::new( $username.value, $secureStringPassword ); totp = $totp } } else { $output = [PSCredential]::new( $username.value, $secureStringPassword ) } } } return $output } function Set-Secret { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter()] [string]$Name, [Parameter()] [object]$Secret, [Parameter()] [string]$VaultName, [Parameter()] [hashtable] $AdditionalParameters ) Write-Verbose "'Set-Secret' invoked ..." if ($null -ne $AdditionalParameters){ $VaultParameters = $AdditionalParameters }else{ if ($null -eq $VaultName){$VaultName = ""} $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue if ($null -eq $secretVault){ Write-Error "The SecretManagement vault '$($VaultName)' is not registered." return $null } $VaultParameters = $secretVault.VaultParameters } $commandArgs = [System.Collections.ArrayList]::new(); $commandArgs.AddRange(@('item', 'get', """$($Name)""")); if ($VaultParameters.AccountName) { $commandArgs.AddRange(@('--account', "$($VaultParameters.AccountName)")); } if ($VaultParameters.OPVault) { $commandArgs.AddRange(@('--vault', "$($VaultParameters.OPVault)")); } $commandArgs.AddRange(@('--format', 'json')); $result = Invoke-OpCommand $commandArgs; if ($result.ExitCode -ne 0){ if ($result.StdErr.Contains("More than one item matches")){ throw [Exception]::new($result.StdErr); return $null; } # Not found $verb = 'create'; }else{ # Found and there is only one $verb = 'edit'; } Write-Verbose $verb $commandArgs = [System.Collections.ArrayList]::new(); $commandArgs.AddRange(@('item', $verb)); if ($VaultParameters.AccountName) { $commandArgs.AddRange(@('--account', "$($VaultParameters.AccountName)")); } if ($VaultParameters.OPVault) { $commandArgs.AddRange(@('--vault', "$($VaultParameters.OPVault)")); } $commandArgs.AddRange(@('--format', 'json')); <# op item create --category=login --title='My Example Item' --vault='Test' ` --url https://www.acme.com/login ` --generate-password='letters,digits,symbols,32' ` username=jane@acme.com ` 'Test Field 1=my test secret' ` 'Test Section 1.Test Field2[text]=Jane Doe' ` 'Test Section 1.Test Field3[date]=1995-02-23' ` 'Test Section 2.Test Field4[text]=Testing 1Password CLI' #> Write-Verbose "Secret type [$($Secret.GetType().Name)]" switch ($Secret.GetType()) { { $_.Name -eq 'String' -or $_.IsValueType } { $category = "Password" Write-Verbose "Processing [string] as '$category'" if ('create' -eq $verb ) { Write-Verbose "Creating '$Name'" $commandArgs.Add("--category=$category") | Out-Null $commandArgs.Add("--title=""$Name""") | Out-Null $commandArgs.Add("password=""$Secret""") | Out-Null } else { Write-Verbose "Updating '$Name'" $commandArgs.Add("""$Name""") | Out-Null $commandArgs.Add("password=""$Secret""") | Out-Null } break } { $_.Name -eq 'securestring' } { $category = "Password" Write-Verbose "Processing [securestring] as '$category'" if ('create' -eq $verb ) { Write-Verbose "Creating ""$Name""" $commandArgs.Add("--category=$category") | Out-Null $commandArgs.Add("--title=""$Name""") | Out-Null $commandArgs.Add("password=""$([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secret)))""") | Out-Null } else { Write-Verbose "Updating '$Name'" $commandArgs.Add("""$Name""") | Out-Null $commandArgs.Add("password=""$([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secret)))""") | Out-Null } break } { $_.Name -eq 'PSCredential' } { $category = "Login" Write-Verbose "Processing [PSCredential] as $category" if ('create' -eq $verb ) { Write-Verbose "Creating '$Name'" $commandArgs.Add("--category=$category") | Out-Null $commandArgs.Add("--title=""$Name""") | Out-Null $commandArgs.Add("username=""$($Secret.UserName)""") | Out-Null $commandArgs.Add("password=""$($Secret.GetNetworkCredential().Password)""") | Out-Null } else { Write-Verbose "Updating '$Name'" $commandArgs.Add("""$Name""") | Out-Null $commandArgs.Add("username=""$($Secret.UserName)""") | Out-Null $commandArgs.Add("password=""$($Secret.GetNetworkCredential().Password)""") | Out-Null } break } Default {} } $sanitizedArgs = $commandArgs | ForEach-Object { if ($_ -like 'password=*') { 'password=*****' } else { $_ } } Write-Verbose ($sanitizedArgs -join ' ') $result = Invoke-OpCommand $commandArgs; #$result.StdOut; #$result.StdErr; return ($result.ExitCode -eq 0); } function Remove-Secret { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter()] [string]$Name, [Parameter()] [string]$VaultName, [Parameter()] [hashtable] $AdditionalParameters ) Write-Verbose "'Remove-Secret' invoked ..." if ($null -ne $AdditionalParameters){ $VaultParameters = $AdditionalParameters }else{ if ($null -eq $VaultName){$VaultName = ""} $secretVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue if ($null -eq $secretVault){ Write-Error "The SecretManagement vault '$($VaultName)' is not registered." return $null } $VaultParameters = $secretVault.VaultParameters } $commandArgs = [System.Collections.ArrayList]::new(); $commandArgs.AddRange(@('item', 'delete', """$($Name)""")); if ($VaultParameters.AccountName) { $commandArgs.AddRange(@('--account', "$($VaultParameters.AccountName)")); } if ($VaultParameters.OPVault) { $commandArgs.AddRange(@('--vault', "$($VaultParameters.OPVault)")); } $commandArgs.Add("--archive") | Out-Null Write-Verbose ($commandArgs -join ' ') $result = Invoke-OpCommand $commandArgs; #$result.StdOut; #$result.StdErr; if ($result.ExitCode -ne 0){ Write-Error "An arror occurred while trying to delete the secret '$($Name)' in 1Password: $($result.StdErr)"; } return ($result.ExitCode -eq 0); } |