├── LICENSE.txt ├── README.md ├── SecretManagement.BitWarden.Extension ├── SecretManagement.BitWarden.Extension.psd1 └── SecretManagement.BitWarden.Extension.psm1 ├── SecretManagement.BitWarden.psd1 └── build.ps1 /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gaston Paquette 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SecretManagement extension for BitWarden 2 | 3 | > **NOTE: This is not a maintained project and it's specifically not maintained _by_ BitWarden.** 4 | > **I work on it in my free time because I use BitWarden personally.** 5 | 6 | > **Special Thanks to @TylerLeonhardt for publishing a baseline for this module extention ** 7 | > **Please check out his [`LastPass Extention`](https://github.com/TylerLeonhardt/SecretManagement.LastPass) ** 8 | 9 | ## Prerequisites 10 | 11 | Download and Install 12 | 13 | 14 | * [PowerShell](https://github.com/PowerShell/PowerShell) 15 | * The [`bitwarden-cli`](https://bitwarden.com/help/article/cli/#download-and-install) 16 | * The [SecretManagement](https://github.com/PowerShell/SecretManagement) PowerShell module 17 | 18 | You can get the `SecretManagement` module from the PowerShell Gallery: 19 | 20 | Using PowerShellGet v2: 21 | 22 | ```pwsh 23 | Install-Module Microsoft.PowerShell.SecretManagement -AllowPrerelease 24 | ``` 25 | 26 | Using PowerShellGet v3: 27 | 28 | ```pwsh 29 | Install-PSResource Microsoft.PowerShell.SecretManagement -Prerelease 30 | ``` 31 | 32 | ## Installation 33 | 34 | You an install this module from the PowerShell Gallery: 35 | 36 | Using PowerShellGet v2: 37 | 38 | ```pwsh 39 | Install-Module SecretManagement.BitWarden 40 | ``` 41 | 42 | Using PowerShellGet v3: 43 | 44 | ```pwsh 45 | Install-PSResource SecretManagement.BitWarden 46 | ``` 47 | 48 | ## Registration 49 | 50 | Once you have it installed, 51 | you need to register the module as an extension: 52 | 53 | ```pwsh 54 | Register-SecretVault -ModuleName SecretManagement.BitWarden 55 | ``` 56 | 57 | Optionally, you can set it as the default vault by also providing the 58 | `-DefaultVault` 59 | parameter. 60 | 61 | 62 | At this point, 63 | you should be able to use 64 | `Get-Secret`, `Set-Secret` 65 | and all the rest of the 66 | `SecretManagement` 67 | commands! 68 | 69 | #### outputType 70 | (Accept: Default,Detailed,TOTP) 71 | 72 | By default, regular credentials are returned as string (for notes) and PSCredential (for credentials) 73 | Setting this parameter to **Detailed** will always return a hashtable. Effectively, this mean that the URL / Notes parameter of the regular credential will be exposed. 74 | TOTP code is for Two-factor authentication of compatible websites -------------------------------------------------------------------------------- /SecretManagement.BitWarden.Extension/SecretManagement.BitWarden.Extension.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ModuleVersion = '0.1.0' 3 | RootModule = 'SecretManagement.BitWarden.Extension.psm1' 4 | FunctionsToExport = @('Set-Secret','Get-Secret','Remove-Secret','Get-SecretInfo','Test-SecretVault','Unlock-BWSession','Get-SecretTemplate') 5 | } 6 | -------------------------------------------------------------------------------- /SecretManagement.BitWarden.Extension/SecretManagement.BitWarden.Extension.psm1: -------------------------------------------------------------------------------- 1 | using namespace Microsoft.PowerShell.SecretManagement 2 | 3 | function Invoke-bwcmd { 4 | [CmdletBinding()] 5 | param ( 6 | [Parameter()] 7 | [string[]]$Arguments, 8 | [bool]$loginrequired = $false 9 | ) 10 | Begin { 11 | $bwPath = (Get-Command 'bw').Source 12 | # Check if logged in or unlocked 13 | $addsession = $null 14 | 15 | IF ($loginrequired) { 16 | Write-Verbose 'Login requried' 17 | $env:BW_Session = $NULL 18 | $status = invoke-bwcmd "status" 19 | Write-Verbose $status 20 | switch ($status.status) { 21 | 'unauthenticated' { 22 | Write-Verbose "New login" 23 | $credential = Get-Credential 24 | $username = $Credential.UserName 25 | $password = $Credential.GetNetworkCredential().Password 26 | $codetype = Read-Host -Prompt 'Two Step Login Methods - Please enter numeric value 0) Authenticator 1) Email 3) Yubikey' 27 | $code = Read-Host -Prompt 'Please enter code' 28 | $env:BW_Session = invoke-bwcmd "login ""$username "" ""$password"" --method $codetype --code $code --raw" 29 | } 30 | 'locked' { 31 | Write-Verbose "Unlocking" 32 | $credential = Get-Credential $status.userEmail 33 | $password = $Credential.GetNetworkCredential().Password 34 | $env:BW_Session = invoke-bwcmd "unlock ""$password"" --raw" 35 | Start-Sleep 1 36 | Write-Verbose $env:BW_Session 37 | $loginrequired = $false 38 | } 39 | 'unlocked' { Write-Verbose 'Account Already Unlocked' } 40 | } 41 | } 42 | IF ($arguments -match 'get|list|create|edit') { 43 | $addsession = "--session $env:BW_SESSION" 44 | } 45 | } 46 | Process { 47 | if ($bwPath) { 48 | 49 | $ps = new-object System.Diagnostics.Process 50 | $ps.StartInfo.Filename = $bwPath 51 | $ps.StartInfo.Arguments = "$Arguments --nointeraction $addsession" 52 | Write-Verbose $ps.StartInfo.Arguments 53 | $ps.StartInfo.RedirectStandardOutput = $True 54 | $ps.StartInfo.RedirectStandardError = $True 55 | $ps.StartInfo.UseShellExecute = $False 56 | $ps.start() | Out-Null 57 | $ps.WaitForExit(1000) | Out-Null 58 | $BWOutput = $ps.StandardOutput.ReadToEnd() 59 | $global:BWError = $ps.StandardError.ReadToEnd() #| Out-String 60 | 61 | IF ($BWError) { 62 | Switch -Wildcard ($BWError) { 63 | '*session*' { 64 | Write-Verbose "Wrong Password, Try again $PSBoundParameters" 65 | invoke-bwcmd $PSBoundParameters.Item('Arguments') -loginrequired $true 66 | 67 | } 68 | 'You are not logged in.' { 69 | invoke-bwcmd $PSBoundParameters.Item('Arguments') -loginrequired $true 70 | } 71 | 'Session key is invalid.' { 72 | Write-Verbose "Invalid Key" 73 | } 74 | 'Vault is locked.' { 75 | Write-Warning $BWError 76 | invoke-bwcmd $PSBoundParameters.Item('Arguments') -loginrequired $true 77 | } 78 | 'More than one result was found*' { 79 | 80 | $errparse = @() 81 | $BWError.split("`n") | Select-Object -skip 1 | ForEach-Object { 82 | $errparse += invoke-bwcmd "get item $_" 83 | } 84 | Write-Warning @" 85 | More than one result was found. Try getting a specific object by `id` instead. The following objects were found: 86 | $($errparse | FT ID, Name | Out-String ) 87 | "@ 88 | } 89 | Default { 90 | 91 | Write-Warning "Default - $BWError" 92 | } 93 | } 94 | } 95 | IF ($BWOutput) { 96 | Write-Verbose "BWOutput" 97 | Try { 98 | $BWOutput | ConvertFrom-Json -ErrorAction Stop 99 | 100 | } 101 | Catch { 102 | $BWOutput 103 | } 104 | } 105 | 106 | 107 | } 108 | ELSE { 109 | throw "bw executable not found or installed." 110 | } 111 | } 112 | End { 113 | #Lock-BWSession 114 | } 115 | } 116 | function Get-Secret { 117 | [CmdletBinding()] 118 | param ( 119 | [Parameter(ValueFromPipelineByPropertyName)] 120 | [string] $Name, 121 | [Parameter(ValueFromPipelineByPropertyName)] 122 | [string] $VaultName, 123 | [Parameter(ValueFromPipelineByPropertyName)] 124 | [hashtable] $AdditionalParameters 125 | ) 126 | 127 | $res = Invoke-bwcmd "get item $Name" 128 | 129 | 130 | Switch ($AdditionalParameters.outputType ) { 131 | 'Detailed' { 132 | Write-Verbose "Getting Detailed Secret" 133 | $Output = $res 134 | $Output.PSObject.TypeNames.Insert(0, "BW_SECRET_Detailed") 135 | } 136 | 'Totp' { 137 | $Output = Invoke-bwcmd "get totp $Name" 138 | $Output.PSObject.TypeNames.Insert(0, "BW_SECRET_TOTP") 139 | } 140 | Default { 141 | Write-Verbose "Getting Simple Secret" 142 | $username = $res.login.Username 143 | $password = $res.login.Password 144 | if ($username -or $password) { 145 | Write-Verbose "Getting Login Account" 146 | if ($null -eq $username) { $username = '' } 147 | if ($null -eq $password) { $password = '' } 148 | if ("" -ne $password) { $password = $password | ConvertTo-SecureString -AsPlainText -Force } 149 | $Output = [System.Management.Automation.PSCredential]::new($username, $password) 150 | } 151 | # Secure Note 152 | if ($null -ne $res.Notes) { 153 | Write-Verbose "Getting SecureNote" 154 | return $res.Notes 155 | } 156 | } 157 | } 158 | 159 | return $Output 160 | } 161 | 162 | <# 163 | .SYNOPSIS 164 | Create a BitWarden Secret Template Object 165 | .DESCRIPTION 166 | Create a BitWarden Secret Template Object 167 | .PARAMETER Vault 168 | Name of the vault to connect to. 169 | .PARAMETER User 170 | Username to connect with. 171 | .PARAMETER Trust 172 | Cause subsquent logins to not require multifactor authentication. 173 | .PARAMETER StayConnected 174 | Save the LastPass decryption key on the hard drive so re-entering password once the connection window close is not required anymore. 175 | This operation will prompt the user. 176 | .PARAMETER Force 177 | Force switch. 178 | .EXAMPLE 179 | PS> Get-SecretTemplate -url 'https://github.com/' -Note "Version control using Git" -Type 'Login' | Set-Secret 180 | Create login templated secret and create secret in BitWarden. This will automatically prompt to set credentials 181 | PS> Get-SecretTemplate -url 'https://github.com/' -Note "Version control using Git" -Type 'SecureNote' | Set-Secret 182 | Create SecureNote templated secret and create secret in BitWarden. 183 | #> 184 | Function Get-SecretTemplate { 185 | [CmdletBinding()] 186 | param ( 187 | [parameter(Mandatory = $true)] 188 | [string]$Name, 189 | [parameter(Mandatory = $true)] 190 | [ValidateSet("SecureNote", "Login")] 191 | [string]$Type, 192 | [string]$Note, 193 | [string]$url 194 | ) 195 | 196 | Switch ($type) { 197 | 'Login' { 198 | $credential = Get-Credential 199 | $username = $Credential.UserName 200 | $password = $Credential.GetNetworkCredential().Password 201 | $object = @" 202 | {"organizationId":null,"folderId":null,"type":1,"name":"$Name","notes":"$Note","favorite":false,"fields":[],"login":{"uris":[{"match":null,"uri":"$url"}],"username":"$username","password":"$password","totp":"JBSWY3DPEHPK3PXP"},"secureNote":null,"card":null,"identity":null} 203 | "@ 204 | } 205 | 'SecureNote' { 206 | $object = @" 207 | {"organizationId":null,"folderId":null,"type":2,"name":"$Name","notes":"$Note","favorite":false,"secureNote":{"type":0}} 208 | "@ 209 | } 210 | } 211 | 212 | $object = $object | ConvertFrom-Json 213 | $object.PSObject.TypeNames.Insert(0, "BW_SECRET_Template") 214 | $object 215 | } 216 | 217 | 218 | function Set-Secret { 219 | [CmdletBinding()] 220 | param ( 221 | [Parameter(ValueFromPipelineByPropertyName)] 222 | [string] $Name, 223 | [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] 224 | [ValidateScript( { $_.PSObject.TypeNames[0] -eq 'BW_SECRET_Template' -or $_.PSObject.TypeNames[0] -eq 'BW_SECRET_Detailed' -or [PSCredential] })] 225 | $Secret, 226 | [Parameter(ValueFromPipelineByPropertyName)] 227 | [string] $VaultName, 228 | [Parameter(ValueFromPipelineByPropertyName)] 229 | [hashtable] $AdditionalParameters 230 | ) 231 | 232 | if ($Secret -is [pscredential]) { 233 | $object = Get-Secret -Name $Name -AdditionalParameters @{OutputType = 'Detailed' } 234 | $object.login.username = $Secret.Username 235 | $object.login.password = $Secret.GetNetworkCredential().password 236 | $res = invoke-bwcmd "edit item $($object.ID) $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(($Object | Convertto-Json -depth 10 -compress))))" 237 | } 238 | IF ($Secret.PSObject.TypeNames -eq 'BW_SECRET_Detailed') { 239 | Write-Verbose "Editing Item $($Secret.Name)" 240 | $res = invoke-bwcmd "edit item $($secret.ID) $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(($secret | Convertto-Json -depth 10 -compress))))" 241 | } 242 | IF ($Secret.PSObject.TypeNames -eq 'BW_SECRET_Template') { 243 | Write-Verbose "Creating new object $($Secret.Name)" 244 | $res = invoke-bwcmd "create item $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(($secret | Convertto-Json -depth 10 -compress))))" 245 | } 246 | 247 | $res 248 | } 249 | 250 | function Remove-Secret { 251 | [CmdletBinding()] 252 | param ( 253 | [Parameter(ValueFromPipelineByPropertyName)] 254 | [string] $Name, 255 | [Parameter(ValueFromPipelineByPropertyName)] 256 | [string] $VaultName, 257 | [Parameter(ValueFromPipelineByPropertyName)] 258 | [hashtable] $AdditionalParameters 259 | ) 260 | 261 | 262 | 263 | Invoke-bwcmd "delete item $Name" 264 | } 265 | 266 | function Get-SecretInfo { 267 | param( 268 | [string] $Filter, 269 | [string] $VaultName, 270 | [hashtable] $AdditionalParameters 271 | ) 272 | 273 | $vaultSecretInfos = invoke-bwcmd "list items" | Where-Object Name -match "$filter" 274 | 275 | 276 | foreach ($vaultSecretInfo in $vaultSecretInfos) { 277 | 278 | IF ($vaultSecretInfo.type -eq 1) { 279 | $type = [Microsoft.PowerShell.SecretManagement.SecretType]::PSCredential 280 | } 281 | ELSE 282 | { $type = [Microsoft.PowerShell.SecretManagement.SecretType]::SecureString } 283 | Write-Output ( 284 | [Microsoft.PowerShell.SecretManagement.SecretInformation]::new( 285 | $vaultSecretInfo.Name, 286 | $type, 287 | $VaultName) 288 | ) | Select-Object *, @{Name = 'GUID_Name'; Expression = { $vaultSecretInfo.ID } } 289 | 290 | } 291 | } 292 | 293 | function Test-SecretVault { 294 | [CmdletBinding()] 295 | param ( 296 | [Parameter(ValueFromPipelineByPropertyName)] 297 | [string] $VaultName, 298 | [Parameter(ValueFromPipelineByPropertyName)] 299 | [hashtable] $AdditionalParameters 300 | ) 301 | invoke-bwcmd "sync" | Out-Null 302 | $status = invoke-bwcmd "status" 303 | return $status 304 | } 305 | -------------------------------------------------------------------------------- /SecretManagement.BitWarden.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # Script module or binary module file associated with this manifest. 3 | #RootModule = 'SecretManagement.BitWarden.psm1' 4 | 5 | # Version number of this module. 6 | ModuleVersion = '0.1.0' 7 | 8 | # ID used to uniquely identify this module 9 | GUID = 'ace6b81f-24b2-4f6e-a9ce-ba0563e31c32' 10 | 11 | # Author of this module 12 | Author = 'Gaston Paquette' 13 | 14 | # Company or vendor of this module 15 | CompanyName = 'Gaston Paquette' 16 | 17 | # Copyright statement for this module 18 | Copyright = '(c) Gaston Paquette. All rights reserved.' 19 | 20 | # Description of the functionality provided by this module 21 | Description = 'SecretManagement extension for BitWarden!' 22 | 23 | # Minimum version of the PowerShell engine required by this module 24 | # PowerShellVersion = '' 25 | 26 | # Modules that must be imported into the global environment prior to importing this module 27 | # RequiredModules = @() 28 | 29 | # Assemblies that must be loaded prior to importing this module 30 | # RequiredAssemblies = @() 31 | 32 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 33 | # ScriptsToProcess = @() 34 | 35 | # Type files (.ps1xml) to be loaded when importing this module 36 | # TypesToProcess = @() 37 | 38 | # Format files (.ps1xml) to be loaded when importing this module 39 | # FormatsToProcess = @() 40 | 41 | NestedModules = './SecretManagement.BitWarden.Extension' 42 | VariablesToExport = '*' 43 | PrivateData = @{ 44 | PSData = @{ 45 | # Tags applied to this module. These help with module discovery in online galleries. 46 | Tags = 'SecretManagement', 'Secrets', 'BitWarden', 'MacOS', 'Linux', 'Windows' 47 | # A URL to the main website for this project. 48 | ProjectUri = 'https://github.com/Gaspack/SecretManagement.BitWarden' 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter()] 4 | [switch] 5 | $Test, 6 | 7 | [Parameter()] 8 | [switch] 9 | $Package, 10 | 11 | [Parameter()] 12 | [switch] 13 | $Publish 14 | ) 15 | 16 | Push-Location $PSScriptRoot 17 | 18 | if ($Test) { 19 | Invoke-Pester test 20 | } 21 | 22 | if ($Package) { 23 | $outDir = Join-Path 'out' 'SecretManagement.BitWarden' 24 | Remove-Item out -Recurse -Force -ErrorAction SilentlyContinue | Out-Null 25 | 26 | @( 27 | 'SecretManagement.BitWarden.Extension' 28 | 'SecretManagement.BitWarden.psd1' 29 | 'LICENSE.txt' 30 | 'README.md' 31 | ) | ForEach-Object { 32 | Copy-Item -Path $_ -Destination (Join-Path $outDir $_) -Force -Recurse 33 | } 34 | } 35 | 36 | if ($Publish) { 37 | Write-Host -ForegroundColor Green "Publishing module... here are the details:" 38 | $moduleData = Import-Module -Force ./out/SecretManagement.BitWarden -PassThru 39 | Write-Host "Version: $($moduleData.Version)" 40 | Write-Host "Prerelease: $($moduleData.PrivateData.PSData.Prerelease)" 41 | Write-Host -ForegroundColor Green "Here we go..." 42 | 43 | Publish-Module -Path ./out/SecretManagement.BitWarden -NuGetApiKey (Get-Secret -Name PowershellGalleryAPIKey) 44 | } 45 | 46 | Pop-Location --------------------------------------------------------------------------------