1Poshword.psm1
#Requires -Version 4 param([string] $DefaultVaultPath) Set-StrictMode -Version 2 $errorActionPreference = 'Stop' $defaultVaultPath = $defaultVaultPath,"$home/Dropbox/1Password/1Password.agilekeychain","$home/Dropbox/1Password/1Password.opvault" ` |? { $_ -and (Test-Path $_) } ` | Resolve-Path ` | Select-Object -First 1 if(-not $DefaultVaultPath) { Write-Warning "Unable to auto-detect a 1Password vault location. Use Set-1PDefaultVaultPath to set a default." } . $psScriptRoot/lib.ps1 <# .SYNOPSIS Sets the default 1Password vault directory to a new value. .DESCRIPTION Sets the default 1Password vault directory to a new value. The 1Password vault at this location will be used by other 1Poshword cmdlets unless otherwise specified. .PARAMETER Path Specifies the root directory of the default 1Password vault. This is the ".agilekeychain" or ".opvault" directory. .EXAMPLE PS ~$ Set-1PDefaultVaultPath /Users/calvin/Dropbox/OtherVault.agilekeychain .EXAMPLE PS ~$ Set-1PDefaultVaultPath /Users/calvin/Dropbox/OtherVault.opvault #> function Set-1PDefaultVaultPath { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [ValidateScript({ (Test-Path $_ -PathType Container) -and ($_ -match '\.(agilekeychain|opvault)(/|\\)?$') })] [string] $Path ) if ($psCmdlet.ShouldProcess($path)) { $script:DefaultVaultPath = Resolve-Path $path } } <# .SYNOPSIS Gets the default 1Password root directory. .DESCRIPTION Gets the default 1Password root directory. The 1Password vault at this location will be used by all other 1Poshword cmdlets unless otherwise specified. .EXAMPLE PS ~$ Get-1PDefaultVaultPath #> function Get-1PDefaultVaultPath { $script:DefaultVaultPath } <# .SYNOPSIS Gets encrypted 1Password entries and their associated metadata. .DESCRIPTION Gets one or more encrypted 1Password entries by name, along with associated metadata. The 'agilekeychain' vault format leaves entry metadata in plaintext, so no password is required for this operation. The 'opvault' vault format encrypts all entry metadata, so a password is required if this operation is run against an 'opvault' vault. .PARAMETER Name Specifies the name of the 1Password entry. A case-insensitive wildcard match is used. .PARAMETER VaultPassword Specifies the 1Password vault password. Required only if the 1Password vault is in 'opvault' format. In this case, if no value is specified, the user will be prompted to enter password interactively. .PARAMETER VaultPath Specifies the root directory of the 1Password vault from which to read. The default root directory can be read via Get-1PDefaultVaultPath, and changed via Set-1PDefaultVaultPath. .EXAMPLE # Gets an entry by name PS ~$ Get-1PEntry gmail Name Type LastUpdated Location ---- ---- ----------- -------- gmail Login 11/30/15 12:11:50 AM https://accounts.gmail.com/ServiceLogin # show all available properties PS ~$ Get-1PEntry gmail | Format-List * Name : gmail Id : 11C5741DE2294A1EB32FB088F5838951 VaultPath : /Users/calvin/Dropbox/1Password/1Password.agilekeychain SecurityLevel : SL5 KeyId : KeyData : Location : https://accounts.gmail.com/ServiceLogin Type : Login CreatedAt : 10/28/15 11:21:15 PM LastUpdated : 11/30/15 12:11:50 AM EncryptedData : U2FsdGVkX19ESuKr39T+d4185iU1NzMhKcfffu8 ... .EXAMPLE # Gets the list of all 1Password entries, sorted by last modified time PS ~$ Get-1PEntry | Sort-Object LastUpdated Name Type LastUpdated Location ---- ---- ----------- -------- Twitter Login 11/29/15 11:53:44 PM https://twitter.com/ Github Login 11/29/15 11:58:12 PM https://github.com/login Facebook Login 11/30/15 12:02:04 AM https://www.facebook.com/login.php Linkedin Login 11/30/15 12:09:11 AM https://www.linkedin.com/uas/login-submit ... #> function Get-1PEntry { param( [Parameter(Position = 0)] [string] $Name, [Parameter(Position = 1)] [SecureString] $VaultPassword, [ValidateScript({ (Test-Path $_ -PathType Container) -and ($_ -match '\.(agilekeychain|opvault)(/|\\)?$') })] [string] $VaultPath = ($script:DefaultVaultPath) ) if(-not $name){ $name = '*' } $result = $null if ($vaultPath -match '\.agilekeychain\b') { $result = GetAgileKeychainEntries $vaultPath $name } elseif ($vaultPath -match '\.opvault\b') { if (-not $vaultPassword) { $vaultPassword = Read-Host -AsSecureString -Prompt "1Password vault password" } $result = GetOPVaultEntries $vaultPath $name $vaultPassword } if((-not $result) -and ($name -notmatch '\*')) { Write-Error "No 1Password entries found with name $name" } $result } <# .SYNOPSIS Decrypts a 1Password Login, Password, Secure Note, or Generic Account. .DESCRIPTION Decrypts a 1Password Login, Password, Secure Note, or Generic Account to various output formats. Logins and Generic Adcounts are returned as PSCredential by default. Passwords and Secure Notes are returned as SecureString by default. All forms can optionally be returned as plaintext strings or copied to the clipboard. .PARAMETER Name Specifies the name of the 1Password entry. A case-insensitive wildcard match is used. An error is thrown if no entries, or more than one entry, match the specified name. .PARAMETER Entry Specifies the 1Password entry to decrypt. .PARAMETER VaultPassword Specifies the 1Password vault password. If no value is specified, the user will be prompted to enter password interactively. .PARAMETER Plaintext If specified, the decrypted data will be returned as plaintext strings. Logins and Generic Accounts will be returned as 2 strings (username followed by password) unless -PasswordOnly is also specified. Passwords and Secure Notes will be returned as 1 string. .PARAMETER PasswordOnly If specified, only the password field is included in the output. This parameter has no effect when returning Password or Secure Note entries. .PARAMETER Clip If specified, the plaintext content of the entry will be copied to the clipboard. Attempts to use a system utility for copying: - Windows: clip.exe - Mac: pbcopy - Linux: xclip .PARAMETER VaultPath Specifies the root directory of the 1Password vault from which to read. The default root directory can be read via Get-1PDefaultVaultPath, and changed via Set-1PDefaultVaultPath. .EXAMPLE # Gets a login as a PSCredential. PS ~$ Unprotect-1PEntry email 1Password vault password: ********** UserName Password -------- -------- calvin@gmail.com System.Security.SecureString .EXAMPLE # Pipes a decrypted password into another command which normally prompts for a password. PS ~$ Unprotect-1PEntry systemlogin -Plaintext -PasswordOnly | sudo -Sk echo "`ndude, sweet" 1Password vault password: ********** Password: dude, sweet .EXAMPLE # Temporarily reveals a Secure Note by piping it to 'less' PS ~$ Get-1PEntry mynote | Unprotect-1PEntry -Plaintext | less 1Password vault password: ********** .EXAMPLE # Copies a password to the clipboard PS ~$ Unprotect-1PEntry mylogin -Clip -PasswordOnly 1Password vault password: ********** .EXAMPLE # Uses a bound SecureString object to specify the 1Password vault password. PS ~$ $p = Read-Host -AsSecureString "Speak, friend, and enter" Speak, friend, and enter: ********** PS ~$ Unprotect-1PEntry mynote $p System.Security.SecureString PS ~$ Unprotect-1PEntry mynote $p -Plaintext s3cret m3ssage #> function Unprotect-1PEntry { [CmdletBinding(DefaultParameterSetName = 'Name/Secure')] param( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Name/Secure')] [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Name/Plain')] [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Name/Clip')] [string] $Name, [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'Entry/Secure')] [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'Entry/Plain')] [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'Entry/Clip')] [PSCustomObject] $Entry, [Parameter(Position = 1)] [SecureString] $VaultPassword, [Parameter(Mandatory = $true, ParameterSetName = 'Name/Plain')] [Parameter(Mandatory = $true, ParameterSetName = 'Entry/Plain')] [switch] $Plaintext, [Parameter(ParameterSetName = 'Name/Secure')] [Parameter(ParameterSetName = 'Name/Plain')] [Parameter(ParameterSetName = 'Name/Clip')] [Parameter(ParameterSetName = 'Entry/Secure')] [Parameter(ParameterSetName = 'Entry/Plain')] [Parameter(ParameterSetName = 'Entry/Clip')] [Alias('po')] [switch] $PasswordOnly, [Parameter(Mandatory = $true, ParameterSetName = 'Name/Clip')] [Parameter(Mandatory = $true, ParameterSetName = 'Entry/Clip')] [switch] $Clip, [Parameter(ParameterSetName = 'Name/Secure')] [Parameter(ParameterSetName = 'Name/Plain')] [Parameter(ParameterSetName = 'Name/Clip')] [ValidateScript({ (Test-Path $_ -PathType Container) -and ($_ -match '\.(agilekeychain|opvault)(/|\\)?$') })] [string] $VaultPath = ($script:DefaultVaultPath) ) $paramSet = $psCmdlet.ParameterSetName $opVault = ($name -and ($vaultPath -match '\.opvault\b')) -or ($entry -and $entry.KeyData) if ($name) { if ($opVault -and (-not $vaultPassword)) { $vaultPassword = Read-Host -AsSecureString -Prompt "1Password vault password" } $entries = Get-1PEntry -Name $name -VaultPath $vaultPath -VaultPassword $vaultPassword if (-not $entries) { Write-Error "No 1Password entries found with name $name" } if (@($entries).Length -gt 1) { Write-Error "More than one entry matches ${name}: $($entries -join ', ')" } $entry = $entries } if(-not $vaultPassword){ $vaultPassword = Read-Host -AsSecureString -Prompt "1Password vault password" } $decrypted = if ($opVault) { DecryptOPVaultEntry $entry $vaultPassword } else { DecryptAgileKeychainEntry $entry $vaultPassword } if ($paramSet -match 'Secure') { if ($entry.Type -eq 'SecureNote') { ConvertTo-SecureString $decrypted.SecureNote -AsPlainText -Force } elseif (($entry.Type -eq 'Password') -or ($passwordOnly)) { ConvertTo-SecureString $decrypted.Password -AsPlainText -Force } else { New-Object PSCredential @($decrypted.Username, (ConvertTo-SecureString $decrypted.Password -AsPlainText -Force)) } } else { $result = $( if(-not $passwordOnly) { $decrypted.Username |? { $_ } } $decrypted.SecureNote |? { $_ } $decrypted.Password |? { $_ } ) if ($paramSet -match 'Plain') { $result } elseif ($paramSet -match 'Clip') { ClipboardCopy $result } } } $1pArgCompleter = { param($commandName, $parameterName, $wordToComplete, $commandAst, $boundParameters) if ($script:DefaultVaultPath -match '\.agilekeychain\b') { 1PTabExpansion $wordToComplete $script:DefaultVaultPath } } if (Get-Command 'Register-ArgumentCompleter' -ea 0) { Register-ArgumentCompleter -CommandName 'Get-1PEntry','Unprotect-1PEntry' -ParameterName Name -ScriptBlock $1pArgCompleter } else { $global:1pTabExpansionOptions = @{ CustomArgumentCompleters = @{} NativeArgumentCompleters = @{} } $global:1pTabExpansionOptions['CustomArgumentCompleters']['Get-1PEntry:Name'] = $1pArgCompleter $global:1pTabExpansionOptions['CustomArgumentCompleters']['Unprotect-1PEntry:Name'] = $1pArgCompleter $function:tabexpansion2 = $function:tabexpansion2 -replace 'End(\r|\n|\s)*{','End { if ($null -ne $options) { $options += $global:1pTabExpansionOptions} else {$options = $global:1pTabExpansionOptions};' } New-Alias g1p Get-1PEntry New-Alias 1p Unprotect-1PEntry Update-TypeData -TypeName 'Entry' -DefaultDisplayPropertySet Name,Type,LastUpdated,Location -Force Export-ModuleMember ` -Function 'Get-1PDefaultVaultPath','Set-1PDefaultVaultPath','Get-1PEntry','Unprotect-1PEntry','TabExpansion' ` -Alias 'g1p','1p' |