├── LICENSE ├── PSLastPass.psd1 ├── PSLastPass.psm1 ├── Private ├── ConvertFrom-LPEncryptedString.ps1 ├── ConvertFrom-LPHexString.ps1 ├── Get-LPKeys.ps1 ├── Get-LPLogin.ps1 ├── Get-LPVault.ps1 └── Invoke-LPLogin.ps1 ├── Public ├── Get-LPAccounts.ps1 ├── Get-LPCredential.ps1 ├── Save-LPData.ps1 └── Sync-LPData.ps1 ├── README.md └── lib └── PBKDF2.NET.dll /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Scott Evtuch 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. 22 | -------------------------------------------------------------------------------- /PSLastPass.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | # Script module or binary module file associated with this manifest. 4 | RootModule = 'PSLastPass' 5 | 6 | # Version number of this module. 7 | ModuleVersion = '1.2' 8 | 9 | # Supported PSEditions 10 | # CompatiblePSEditions = @() 11 | 12 | # ID used to uniquely identify this module 13 | GUID = 'f6325817-684a-4fa3-b063-e85be0f0edf6' 14 | 15 | # Author of this module 16 | Author = 'Scott Evtuch' 17 | 18 | # Company or vendor of this module 19 | CompanyName = 'Scott Evtuch' 20 | 21 | # Copyright statement for this module 22 | Copyright = 'Copyright (c) 2018 Scott Evtuch - MIT License' 23 | 24 | # Description of the functionality provided by this module 25 | Description = 'An unofficial PowerShell module for invoking the LastPass API' 26 | 27 | # Minimum version of the Windows PowerShell engine required by this module 28 | # PowerShellVersion = '' 29 | 30 | # Name of the Windows PowerShell host required by this module 31 | # PowerShellHostName = '' 32 | 33 | # Minimum version of the Windows PowerShell host required by this module 34 | # PowerShellHostVersion = '' 35 | 36 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 37 | # DotNetFrameworkVersion = '' 38 | 39 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 40 | # CLRVersion = '' 41 | 42 | # Processor architecture (None, X86, Amd64) required by this module 43 | # ProcessorArchitecture = '' 44 | 45 | # Modules that must be imported into the global environment prior to importing this module 46 | # RequiredModules = @() 47 | 48 | # Assemblies that must be loaded prior to importing this module 49 | # RequiredAssemblies = @() 50 | 51 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 52 | # ScriptsToProcess = @() 53 | 54 | # Type files (.ps1xml) to be loaded when importing this module 55 | # TypesToProcess = @() 56 | 57 | # Format files (.ps1xml) to be loaded when importing this module 58 | # FormatsToProcess = @() 59 | 60 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 61 | # NestedModules = @() 62 | 63 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 64 | FunctionsToExport = 'Get-LPAccounts', 'Get-LPCredential', 'Save-LPData', 'Sync-LPData' 65 | 66 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 67 | CmdletsToExport = @() 68 | 69 | # Variables to export from this module 70 | # VariablesToExport = @() 71 | 72 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 73 | AliasesToExport = 'lastpass' 74 | 75 | # DSC resources to export from this module 76 | # DscResourcesToExport = @() 77 | 78 | # List of all modules packaged with this module 79 | # ModuleList = @() 80 | 81 | # List of all files packaged with this module 82 | # FileList = @() 83 | 84 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 85 | PrivateData = @{ 86 | 87 | PSData = @{ 88 | 89 | # Tags applied to this module. These help with module discovery in online galleries. 90 | Tags = 'LastPass' 91 | 92 | # A URL to the license for this module. 93 | LicenseUri = 'https://raw.githubusercontent.com/ScottEvtuch/PSLastPass/master/LICENSE' 94 | 95 | # A URL to the main website for this project. 96 | ProjectUri = 'https://github.com/ScottEvtuch/PSLastPass' 97 | 98 | # A URL to an icon representing this module. 99 | # IconUri = '' 100 | 101 | # ReleaseNotes of this module 102 | # ReleaseNotes = '' 103 | 104 | # External dependent modules of this module 105 | # ExternalModuleDependencies = '' 106 | 107 | } # End of PSData hashtable 108 | 109 | } # End of PrivateData hashtable 110 | 111 | # HelpInfo URI of this module 112 | # HelpInfoURI = '' 113 | 114 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 115 | # DefaultCommandPrefix = '' 116 | 117 | } 118 | 119 | -------------------------------------------------------------------------------- /PSLastPass.psm1: -------------------------------------------------------------------------------- 1 | # Setup variables 2 | 3 | $ExportParams = @{} 4 | 5 | # Basic variables 6 | $LPURL = "https://lastpass.com" 7 | $LPUserAgent = "LastPass-CLI/1.2.1" 8 | 9 | # Import the PBKDF2 dll 10 | Add-Type -Path "$PSScriptRoot\lib\PBKDF2.NET.dll" -ErrorAction Stop 11 | 12 | # Set a variables for text encoding 13 | $BasicEncoding = [System.Text.Encoding]::GetEncoding("iso-8859-1") 14 | $TextEncoding = [System.Text.Encoding]::GetEncoding("UTF-8") 15 | 16 | # Set up a session variable 17 | $LPSession = New-Object -TypeName Microsoft.PowerShell.Commands.WebRequestSession 18 | 19 | # Load saved data if possible 20 | try 21 | { 22 | $SavedData = Import-Clixml -Path "$env:APPDATA\PSLastPass.xml" -ErrorAction Stop 23 | 24 | $LPLogin = $SavedData.Login 25 | $LPKeys = $SavedData.LPKeys 26 | $LPIterations = $SavedData.Iterations 27 | 28 | $SavedData.Cookies | ForEach-Object { 29 | $LPSessionCookie = New-Object -TypeName System.Net.Cookie 30 | $_.psobject.Properties | Where-Object "Name" -NE "TimeStamp" | ForEach-Object { 31 | $LPSessionCookie.($_.Name) = $_.Value 32 | } 33 | $LPSession.Cookies.Add($LPSessionCookie) 34 | } 35 | if ($SavedData.Vault) { 36 | Write-Verbose "Importing saved vault data" 37 | $LPVault = $SavedData.Vault 38 | } 39 | } 40 | catch 41 | { 42 | Write-Verbose "No saved data to load: $_" 43 | } 44 | 45 | #region Public Functions 46 | 47 | # Name of the folder for public function ps1 files 48 | $PublicFunctionFolder = "Public" 49 | 50 | # Setup variables 51 | $PublicFunctionPath = "$PSScriptRoot\$PublicFunctionFolder" 52 | $PublicFunctions = @() 53 | $PublicAliases = @() 54 | 55 | # Get all of the public function files we'll be importing 56 | Write-Verbose "Searching for scripts in $PublicFunctionPath" 57 | $PublicFunctionFiles = Get-ChildItem -File -Filter *-*.ps1 -Path $PublicFunctionPath -Recurse -ErrorAction Continue 58 | Write-Debug "Found $($PublicFunctionFiles.Count) function files in $PublicFunctionPath" 59 | 60 | # Iterate through each of the public function files 61 | foreach ($PublicFunctionFile in $PublicFunctionFiles) 62 | { 63 | $PublicFunctionName = $PublicFunctionFile.BaseName 64 | Write-Verbose "Importing function $PublicFunctionName" 65 | try 66 | { 67 | # Dot source the file and extract the function name and any aliases 68 | . $PublicFunctionFile.FullName 69 | $PublicFunctions += $PublicFunctionName 70 | $PublicFunctionAliases = Get-Alias -Definition $PublicFunctionName -Scope Local -ErrorAction Ignore 71 | Write-Debug "Aliases for $PublicFunctionName`: $PublicFunctionAliases" 72 | $PublicAliases += $PublicFunctionAliases 73 | } 74 | catch 75 | { 76 | Write-Error "Failed to import $($PublicFunctionFile): $_" 77 | } 78 | } 79 | 80 | # Add to the export parameters 81 | $ExportParams.Add("Function",$PublicFunctions) 82 | $ExportParams.Add("Alias",$PublicAliases) 83 | 84 | #endregion 85 | 86 | #region Private Functions 87 | 88 | # Name of the folder for private function ps1 files 89 | $PrivateFunctionFolder = "Private" 90 | 91 | # Setup variables 92 | $PrivateFunctionPath = "$PSScriptRoot\$PrivateFunctionFolder" 93 | 94 | # Get all of the private function files we'll be importing 95 | Write-Verbose "Searching for scripts in $PrivateFunctionPath" 96 | $PrivateFunctionFiles = Get-ChildItem -File -Filter *-*.ps1 -Path $PrivateFunctionPath -Recurse -ErrorAction Continue 97 | Write-Debug "Found $($PrivateFunctionFiles.Count) function files in $PrivateFunctionPath" 98 | 99 | # Iterate through each of the private function files 100 | foreach ($PrivateFunctionFile in $PrivateFunctionFiles) 101 | { 102 | $PrivateFunctionName = $PrivateFunctionFile.BaseName 103 | Write-Verbose "Importing function $PrivateFunctionName" 104 | try 105 | { 106 | # Dot source the file 107 | . $PrivateFunctionFile.FullName 108 | } 109 | catch 110 | { 111 | Write-Error "Failed to import $PrivateFunctionFile`: $_" 112 | } 113 | } 114 | 115 | #endregion 116 | 117 | # Export the public items 118 | 119 | Export-ModuleMember @ExportParams -------------------------------------------------------------------------------- /Private/ConvertFrom-LPEncryptedString.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Returns the plaintext for an AES-encrypted string from the LastPass vault 4 | .DESCRIPTION 5 | Uses the decryption key from the user's password to AES decrypt their vault 6 | entires. Returns the unencrytped string. 7 | .EXAMPLE 8 | ConvertFrom-LPEncryptedString -String $String 9 | #> 10 | function ConvertFrom-LPEncryptedString 11 | { 12 | [CmdletBinding()] 13 | Param( 14 | # The encrypted string to decrypt 15 | [Parameter(Mandatory=$true, 16 | ValueFromPipeline=$true)] 17 | [ValidateNotNullOrEmpty()] 18 | [String] 19 | $String, 20 | 21 | # The applicable sharing key 22 | [String] 23 | $Key 24 | ) 25 | 26 | Begin 27 | { 28 | if (!$LPKeys) 29 | { 30 | Invoke-LPLogin | Out-Null 31 | } 32 | if ($Key) 33 | { 34 | $KeyBytes = $BasicEncoding.GetBytes($Key) 35 | } 36 | else 37 | { 38 | $KeyBytes = $BasicEncoding.GetBytes($LPKeys.GetNetworkCredential().Password) 39 | } 40 | } 41 | Process 42 | { 43 | if (($String[0] -eq '!') -and (($String.Length % 16) -eq 1) -and ($String.Length -gt 32)) 44 | { 45 | Write-Verbose "Decrypting using AES" 46 | $StringBytes = $BasicEncoding.GetBytes($String) 47 | $AES = New-Object -TypeName "System.Security.Cryptography.AesManaged" 48 | $AES.Key = $KeyBytes 49 | $AES.IV = $StringBytes[1..16] 50 | $AES.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7 51 | $Decryptor = $AES.CreateDecryptor() 52 | $PlainBytes = $Decryptor.TransformFinalBlock($StringBytes,17,$($StringBytes.Length-17)) 53 | $OutString = $TextEncoding.GetString($PlainBytes) 54 | $Decryptor.Dispose() 55 | $AES.Dispose() 56 | } 57 | else 58 | { 59 | Write-Verbose "Not AES encrypted, returning unaltered string" 60 | $OutString = $String 61 | } 62 | 63 | $OutString.Trim([byte]0) 64 | } 65 | } -------------------------------------------------------------------------------- /Private/ConvertFrom-LPHexString.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Returns the plaintext for a hex string from the LastPass vault 4 | .DESCRIPTION 5 | Loops through the hex characters in a string and returns a decoded string. 6 | .EXAMPLE 7 | ConvertFrom-LPHexString -String $String 8 | #> 9 | function ConvertFrom-LPHexString 10 | { 11 | [CmdletBinding()] 12 | Param( 13 | # The encrypted string to decrypt 14 | [Parameter(Mandatory=$true, 15 | ValueFromPipeline=$true)] 16 | [ValidateNotNullOrEmpty()] 17 | [String] 18 | $String 19 | ) 20 | 21 | Process 22 | { 23 | $CharArray = @() 24 | 25 | Write-Verbose "Converting from Hex string" 26 | Write-Debug "Hex string: $String" 27 | for ($i = 0; $i -lt $String.Length; $i = $i + 2) 28 | { 29 | if ($BasicEncoding.GetBytes($String.Substring($i,2)) -ne [byte]16) 30 | { 31 | $CharArray += [char][System.Convert]::ToInt16($String.Substring($i,2),16) 32 | } 33 | } 34 | 35 | -join $CharArray 36 | } 37 | } -------------------------------------------------------------------------------- /Private/Get-LPKeys.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Generates the encryption keys 4 | .DESCRIPTION 5 | Retrieves the number of iterations for the user. Uses PBKDF2.NET to create 6 | the encryption key and the login hex string. 7 | .EXAMPLE 8 | Get-LPKeys 9 | #> 10 | function Get-LPKeys 11 | { 12 | [CmdletBinding()] 13 | Param() 14 | 15 | Begin 16 | { 17 | if (!$LPLogin) 18 | { 19 | $LPLogin = Get-LPLogin 20 | } 21 | } 22 | Process 23 | { 24 | Write-Verbose "Setting up common variables" 25 | $WebRequestSettings = @{ 26 | "UserAgent" = $LPUserAgent; 27 | "WebSession" = $LPSession; 28 | "UseBasicParsing" = $true; 29 | "ErrorAction" = "Stop"; 30 | } 31 | 32 | if (!$LPIterations) 33 | { 34 | Write-Verbose "Getting the number of iterations" 35 | try 36 | { 37 | $IterationsResponse = Invoke-WebRequest -Uri "$LPUrl/iterations.php" -Method Post -Body @{"username"=$LPLogin.UserName.ToLower();} @WebRequestSettings 38 | Write-Debug $($IterationsResponse | Out-String) 39 | 40 | $script:LPIterations = if ([int]$IterationsResponse.Content -eq 1) {100100} else {[int] $IterationsResponse.Content} 41 | Write-Debug "Using $LPIterations iterations" 42 | } 43 | catch 44 | { 45 | throw "Failed to get iterations from LastPass API: $_" 46 | } 47 | } 48 | 49 | Write-Verbose "Producing the keys" 50 | try 51 | { 52 | $UsernameBytes = $BasicEncoding.GetBytes($LPLogin.UserName.ToLower()) 53 | $PasswordBytes = $BasicEncoding.GetBytes($LPLogin.GetNetworkCredential().Password) 54 | 55 | $KeyPBKDF2 = [System.Security.Cryptography.PBKDF2]::new($PasswordBytes,$UsernameBytes,$LPIterations,"HMACSHA256") 56 | $KeyBytes = $KeyPBKDF2.GetBytes(32) 57 | $KeyString = $BasicEncoding.GetString($KeyBytes) | ConvertTo-SecureString -AsPlainText -Force 58 | 59 | $LoginPBKDF2 = [System.Security.Cryptography.PBKDF2]::new($KeyBytes,$PasswordBytes,1,"HMACSHA256") 60 | $LoginBytes = $LoginPBKDF2.GetBytes(32) 61 | $LoginString = [System.BitConverter]::ToString($LoginBytes).Replace("-","").ToLower() 62 | 63 | $script:LPKeys = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($LoginString,$KeyString) 64 | $script:LPKeys 65 | } 66 | catch 67 | { 68 | throw "Failed to generate the login and decryption keys: $_" 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Private/Get-LPLogin.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Prompts the user for credentials 4 | .DESCRIPTION 5 | Uses Get-Credential to prompt the user and save a PSCredential object with 6 | the user's LastPass login. 7 | .EXAMPLE 8 | Get-LPLogin 9 | #> 10 | function Get-LPLogin 11 | { 12 | [CmdletBinding()] 13 | Param() 14 | 15 | Process 16 | { 17 | Write-Verbose "Prompting the user for credentials" 18 | $LPLogin = Get-Credential -Message "Please input your LastPass credentials" 19 | 20 | if (!$LPLogin) 21 | { 22 | throw "No credentials provided" 23 | } 24 | 25 | $script:LPLogin = $LPLogin 26 | $script:LPLogin 27 | } 28 | } -------------------------------------------------------------------------------- /Private/Get-LPVault.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Retrieves the LastPass vault 4 | .DESCRIPTION 5 | Downloads the vault payload from the LastPass API and iterates over the 6 | VaultItems to create an individual PowerShell object for each. 7 | .EXAMPLE 8 | Get-LPVault 9 | #> 10 | function Get-LPVault 11 | { 12 | [CmdletBinding()] 13 | Param() 14 | 15 | Begin 16 | { 17 | if (!$LPSession.Cookies.GetCookies($LPURL)["PHPSESSID"]) 18 | { 19 | $LPSession = Invoke-LPLogin 20 | } 21 | } 22 | Process 23 | { 24 | Write-Verbose "Setting up common variables" 25 | $WebRequestSettings = @{ 26 | "UserAgent" = $LPUserAgent; 27 | "WebSession" = $LPSession; 28 | "UseBasicParsing" = $true; 29 | "ErrorAction" = "Stop"; 30 | } 31 | 32 | Write-Verbose "Getting the vault" 33 | try 34 | { 35 | $VaultBody = @{ 36 | "mobile" = 1; 37 | "requestsrc" = "cli"; 38 | "hasplugin" = "1.2.1"; 39 | } 40 | 41 | $VaultResponse = Invoke-WebRequest -Uri "$LPUrl/getaccts.php" -Method Post -Body $VaultBody @WebRequestSettings 42 | } 43 | catch 44 | { 45 | throw "Failed to get vault from LastPass API: $_" 46 | } 47 | 48 | Write-Verbose "Converting vault into raw bytes" 49 | $VaultBytes = $BasicEncoding.GetBytes($VaultResponse.Content) 50 | 51 | $VaultCursor = 0 52 | $Vault = @() 53 | Write-Verbose "Iterating through the vault entries" 54 | while ($VaultCursor -lt $VaultBytes.Count) 55 | { 56 | Write-Debug "Cursor is $VaultCursor" 57 | $ID = $BasicEncoding.GetString($VaultBytes[$VaultCursor..$($VaultCursor+3)]) 58 | Write-Debug "Entry ID is $ID" 59 | $VaultCursor = $VaultCursor + 4 60 | $Length = [System.BitConverter]::ToUInt32($VaultBytes[$($VaultCursor+3)..$VaultCursor],0) 61 | Write-Debug "Entry length is $Length" 62 | $VaultCursor = $VaultCursor + 4 63 | $Data = $VaultBytes[$VaultCursor..$($VaultCursor+$Length-1)] 64 | $VaultCursor = $VaultCursor + $Length 65 | 66 | Write-Verbose "Adding item with ID $ID" 67 | $VaultItem = @{ 68 | "ID" = $ID; 69 | "Length" = $Length; 70 | "Data" = $BasicEncoding.GetString($Data); 71 | } 72 | $Vault += New-Object -TypeName PSObject -Property $VaultItem 73 | } 74 | 75 | $script:LPVault = $Vault 76 | $Vault 77 | } 78 | } -------------------------------------------------------------------------------- /Private/Invoke-LPLogin.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Logs in to LastPass 4 | .DESCRIPTION 5 | Sends the login request to LastPass and throws an error if it fails. 6 | .EXAMPLE 7 | Invoke-LPLogin 8 | #> 9 | function Invoke-LPLogin 10 | { 11 | [CmdletBinding()] 12 | Param() 13 | 14 | Begin 15 | { 16 | if (!$LPKeys) 17 | { 18 | $LPKeys = Get-LPKeys 19 | } 20 | } 21 | Process 22 | { 23 | Write-Verbose "Setting up common variables" 24 | $WebRequestSettings = @{ 25 | "UserAgent" = $LPUserAgent; 26 | "WebSession" = $LPSession; 27 | "UseBasicParsing" = $true; 28 | "ErrorAction" = "Stop"; 29 | } 30 | 31 | Write-Verbose "Attempting to login" 32 | try 33 | { 34 | $LoginBody = @{ 35 | "xml" = 2; 36 | "username" = $LPLogin.UserName.ToLower(); 37 | "hash" = $LPKeys.UserName; 38 | "iterations" = $LPIterations; 39 | "includeprivatekeyenc" = 1; 40 | "method" = "cli"; 41 | "outofbandsupported" = 1; 42 | } 43 | 44 | $LoginResponse = Invoke-WebRequest -Uri "$LPUrl/login.php" -Method Post -Body $LoginBody @WebRequestSettings 45 | Write-Debug $($LoginResponse | Out-String) 46 | 47 | switch ($([xml]$LoginResponse.Content).response.error.cause) { 48 | $null 49 | { 50 | if ($([xml]$LoginResponse.Content).response.ok) 51 | { 52 | Write-Verbose "Sucessful login" 53 | } 54 | else 55 | { 56 | throw "Malformed response from server" 57 | } 58 | } 59 | "outofbandrequired" 60 | { 61 | Write-Host "Out of band authentication is required" 62 | Write-Verbose "Trying login again with out of band request" 63 | $LoginBody.Add("outofbandrequest",1) 64 | $LoginResponse = Invoke-WebRequest -Uri "$LPUrl/login.php" -Method Post -Body $LoginBody @WebRequestSettings 65 | Write-Debug $($LoginResponse | Out-String) 66 | 67 | if ($([xml]$LoginResponse.Content).response.error) 68 | { 69 | throw "$($([xml]$LoginResponse.Content).response.error.message)" 70 | } 71 | if ($([xml]$LoginResponse.Content).response.ok) 72 | { 73 | Write-Verbose "Sucessful login" 74 | } 75 | else 76 | { 77 | throw "Malformed response from server" 78 | } 79 | } 80 | "googleauthrequired" 81 | { 82 | Write-Host "Two-factor authentication is required" 83 | $2faCode = Read-Host -Prompt "Please provide two-factor code" 84 | Write-Verbose "Trying login again with two-factor request" 85 | $LoginBody.Add("otp",$2faCode) 86 | $LoginResponse = Invoke-WebRequest -Uri "$LPUrl/login.php" -Method Post -Body $LoginBody @WebRequestSettings 87 | Write-Debug $($LoginResponse | Out-String) 88 | 89 | if ($([xml]$LoginResponse.Content).response.error) 90 | { 91 | throw "$($([xml]$LoginResponse.Content).response.error.message)" 92 | } 93 | if ($([xml]$LoginResponse.Content).response.ok) 94 | { 95 | Write-Verbose "Sucessful login" 96 | } 97 | else 98 | { 99 | throw "Malformed response from server" 100 | } 101 | } 102 | "unknownpassword" 103 | { 104 | Write-Host "Invalid LastPass password" 105 | $script:LPLogin = $null 106 | } 107 | Default 108 | { 109 | throw "$($([xml]$LoginResponse.Content).response.error.message)" 110 | } 111 | } 112 | 113 | $script:LPSession = $LPSession 114 | $LPSession 115 | } 116 | catch 117 | { 118 | throw "Failed to login: $_" 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /Public/Get-LPAccounts.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Returns account objects from the encrypted vault 4 | .DESCRIPTION 5 | Iterates through all of the ACCT objects from the vault, decrypts them 6 | with the user's key, and then returns an array of objects. 7 | .EXAMPLE 8 | Get-LPAccounts 9 | #> 10 | function Get-LPAccounts 11 | { 12 | [CmdletBinding()] 13 | Param( 14 | # Force a refresh 15 | [Parameter()] 16 | [Switch] 17 | $Refresh 18 | ) 19 | 20 | Begin 21 | { 22 | if (!$LPAccounts -or $Refresh) 23 | { 24 | if (!$LPVault -or $Refresh) 25 | { 26 | $LPVault = Get-LPVault 27 | } 28 | if (!$LPKeys) 29 | { 30 | $LPKeys = Get-LPKeys 31 | } 32 | } 33 | } 34 | Process 35 | { 36 | if (!$LPAccounts -or $Refresh) 37 | { 38 | $VaultAccounts = $LPVault | Where-Object -Property 'ID' -Match "(ACCT|SHAR)" 39 | 40 | $SharingKey = $null 41 | $LPAccounts = @() 42 | foreach ($VaultAccount in $VaultAccounts) 43 | { 44 | switch ($VaultAccount.ID) { 45 | 'ACCT' 46 | { 47 | Write-Debug "Starting ACCT processing" 48 | $AccountBytes = $BasicEncoding.GetBytes($VaultAccount.Data) 49 | 50 | $AccountCursor = 0 51 | $AccountData = @() 52 | while ($AccountCursor -lt $AccountBytes.Count) 53 | { 54 | Write-Verbose "Cursor is $AccountCursor" 55 | $Length = [System.BitConverter]::ToUInt32($AccountBytes[$($AccountCursor+3)..$AccountCursor],0) 56 | Write-Debug "Data item length is $Length" 57 | $AccountCursor = $AccountCursor + 4 58 | 59 | $DataItem = $BasicEncoding.GetString($AccountBytes[$AccountCursor..$($AccountCursor+$Length-1)]) 60 | $AccountCursor = $AccountCursor + $Length 61 | 62 | $AccountData += $DataItem 63 | } 64 | 65 | $Username = $AccountData[7] | ConvertFrom-LPEncryptedString -Key $SharingKey 66 | $Password = $AccountData[8] | ConvertFrom-LPEncryptedString -Key $SharingKey 67 | if ($Password -ne "") 68 | { 69 | $Password = $Password | ConvertTo-SecureString -AsPlainText -Force 70 | if ($Username -ne "") 71 | { 72 | $PSCredential = New-Object -TypeName PSCredential -ArgumentList @($Username,$Password); 73 | } 74 | else 75 | { 76 | $Username = $null 77 | $PSCredential = $null 78 | } 79 | } 80 | else 81 | { 82 | $Password = $null 83 | $PSCredential = $null 84 | } 85 | 86 | $Account = @{ 87 | "ID" = $AccountData[0] | ConvertFrom-LPEncryptedString; 88 | "Name" = $AccountData[1] | ConvertFrom-LPEncryptedString -Key $SharingKey; 89 | "Group" = $AccountData[2] | ConvertFrom-LPEncryptedString -Key $SharingKey; 90 | "URL" = $AccountData[3] | ConvertFrom-LPEncryptedString | ConvertFrom-LPHexString; 91 | "Notes" = $AccountData[4] | ConvertFrom-LPEncryptedString -Key $SharingKey; 92 | "PSCredential" = $PSCredential; 93 | "Username" = $Username; 94 | "Password" = $Password; 95 | "SecureNote" = $($AccountData[11] | ConvertFrom-LPEncryptedString); 96 | } 97 | 98 | $LPAccounts += New-Object -TypeName PSObject -Property $Account 99 | } 100 | 'SHAR' 101 | { 102 | Write-Debug "Starting SHAR processing" 103 | $ShareBytes = $BasicEncoding.GetBytes($VaultAccount.Data) 104 | 105 | $ShareCursor = 0 106 | $ShareData = @() 107 | while ($ShareCursor -lt $ShareBytes.Count) 108 | { 109 | Write-Verbose "Cursor is $ShareCursor" 110 | $Length = [System.BitConverter]::ToUInt32($ShareBytes[$($ShareCursor+3)..$ShareCursor],0) 111 | Write-Debug "Data item length is $Length" 112 | $ShareCursor = $ShareCursor + 4 113 | 114 | $DataItem = $BasicEncoding.GetString($ShareBytes[$ShareCursor..$($ShareCursor+$Length-1)]) 115 | $ShareCursor = $ShareCursor + $Length 116 | 117 | $ShareData += $DataItem 118 | } 119 | 120 | $SharingKey = $ShareData[5] | ConvertFrom-LPEncryptedString | ConvertFrom-LPHexString 121 | } 122 | } 123 | } 124 | $script:LPAccounts = $LPAccounts 125 | } 126 | 127 | $script:LPAccounts 128 | } 129 | } -------------------------------------------------------------------------------- /Public/Get-LPCredential.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Returns a PSCredential object for a URL 4 | .DESCRIPTION 5 | Performs a regular expression match on a URL to create strings for various 6 | fuzzy matching scenarios, then returns an array of PSCredential objects 7 | that match the given URL. Optionally return only one entry. 8 | .EXAMPLE 9 | Get-LPCredential 10 | #> 11 | function Get-LPCredential 12 | { 13 | [CmdletBinding(PositionalBinding=$true, 14 | DefaultParameterSetName='URL', 15 | ConfirmImpact='Low')] 16 | [Alias("lastpass")] 17 | Param 18 | ( 19 | # URL to find a credential for 20 | [Parameter(Mandatory=$true, 21 | ParameterSetName='URL', 22 | Position=0, 23 | ValueFromPipelineByPropertyName=$true, 24 | ValueFromPipeline=$true)] 25 | [ValidateNotNull()] 26 | [ValidateNotNullOrEmpty()] 27 | [String] 28 | $URL, 29 | 30 | # Only return the first result 31 | [Parameter()] 32 | [switch] 33 | $First, 34 | 35 | # Force a refresh 36 | [Parameter()] 37 | [Switch] 38 | $Refresh 39 | ) 40 | 41 | Begin 42 | { 43 | if (!$LPVault) 44 | { 45 | $LPVault = Get-LPVault 46 | } 47 | if (!$LPKeys) 48 | { 49 | $LPKeys = Get-LPKeys 50 | } 51 | if (!$LPAccounts -or $Refresh) 52 | { 53 | $LPAccounts = Get-LPAccounts -Refresh:$Refresh 54 | } 55 | } 56 | Process 57 | { 58 | if (!($URL -match '(((((?:\w+\:\/\/)?(((?:\d{1,3}\.){3}\d{1,3})|(?:\w+\.)*((?:\w+\.)(?:\w+))|\w+))(?:\:\d+)?)(?:\/[^\?\.]*)*(?:\/)?(?:[-\w\.]*)?)(?:\??[^\/]*)?)?')) 59 | { 60 | throw "Bad URL format" 61 | } 62 | 63 | Write-Verbose "Searching through $($LPAccounts.Count) accounts" 64 | foreach ($match in $matches.GetEnumerator()) { 65 | Write-Verbose "Searching for: $($match.Value)" 66 | $Candidates = @($LPAccounts | Where-Object PSCredential -NE $null | Where-Object {$_.URL -like "*$($match.Value)" -or $_.URL -like "*$($match.Value)/*" -or $_.URL -like "*$($match.Value):*"}) 67 | Write-Verbose "Found $($Candidates.Count) candidates" 68 | if ($Candidates.Count -gt 0) 69 | { 70 | if ($First) 71 | { 72 | return $($Candidates.PSCredential | Select-Object -First 1) 73 | } 74 | else 75 | { 76 | return $Candidates.PSCredential 77 | } 78 | } 79 | } 80 | 81 | throw "No matches found in LastPass vault" 82 | 83 | } 84 | } -------------------------------------------------------------------------------- /Public/Save-LPData.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Saves the encrypted LastPass data to the user's APPDATA 4 | .DESCRIPTION 5 | Runs Export-CliXml to a file in the user's APPDATA directory so that future 6 | module loads can pull the cached data instead of contacting LastPass. 7 | .EXAMPLE 8 | Save-LPData 9 | #> 10 | function Save-LPData 11 | { 12 | Param( 13 | # Optionally save the vaulted passwords offline 14 | [Parameter()] 15 | [Switch] 16 | $SaveVault 17 | ) 18 | 19 | Begin 20 | { 21 | if (!$LPVault) 22 | { 23 | $LPVault = Get-LPVault 24 | } 25 | if (!$LPKeys) 26 | { 27 | $LPKeys = Get-LPKeys 28 | } 29 | if (!$LPAccounts) 30 | { 31 | $LPAccounts = Get-LPAccounts 32 | } 33 | } 34 | Process 35 | { 36 | try { 37 | $SavedData = @{ 38 | 'Login' = $LPLogin 39 | 'LPKeys' = $LPKeys 40 | 'Iterations' = $LPIterations 41 | 'Cookies' = $LPSession.Cookies.GetCookies($LPURL) 42 | } 43 | if ($SaveVault) { 44 | $SavedData.Vault = $LPVault 45 | } 46 | $SavedData | Export-CliXml $env:APPDATA\PSLastPass.xml 47 | } 48 | catch { 49 | throw "Failed to export LastPass data: $_" 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Public/Sync-LPData.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Force updates LastPass data from the online database 4 | .DESCRIPTION 5 | Runs Get-LPVault, Get-LPKeys, and Get-LPAccounts with the refresh flag 6 | forced to make sure the modules data is up to date. 7 | .EXAMPLE 8 | Sync-LPData 9 | #> 10 | function Sync-LPData 11 | { 12 | [CmdletBinding()] 13 | Param() 14 | 15 | Process 16 | { 17 | $LPVault = Get-LPVault 18 | $LPKeys = Get-LPKeys 19 | $LPAccounts = Get-LPAccounts -Refresh 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSLastPass 2 | An unofficial PowerShell module for invoking the LastPass API 3 | 4 | # Attribution 5 | 6 | This project utilizes the [PBKDF2.NET library by Michael Johnson](https://github.com/therealmagicmike/PBKDF2.NET) licensed under the [MIT License](https://raw.githubusercontent.com/therealmagicmike/PBKDF2.NET/master/License.txt). 7 | 8 | # Usage 9 | 10 | ## General Notes 11 | 12 | When the module is first loaded, regardless of command, it will prompt you for your LastPass credentials or load your saved credentials and attempt to login. Saved credentials are encrypted so that only the same user account on the same machine can decrypt them. 13 | 14 | Passwords of any type are never stored in plaintext anywhere. Your LastPass email and the contents of your session cookie are stored in plaintext as variables and on your disk if you use Save-LPData. 15 | 16 | While the module is loaded, your entry names, URLs, usernames, and notes will be stored unencrypted in module-scoped PowerShell variables. 17 | 18 | ## Caching and Offline Use 19 | 20 | New caching behavior in v1.2 means that Save-LPData will now only save your credentials by default. Any subsequent module imports will cause the vault to be retrieved from the LastPass API. You can return to the previous behavior by adding the -SaveVault switch parameter which will save the entire vault offline for future use. You can also force an update of the vault by running Sync-LPData or adding the -Refresh switch parameter to any Get function. 21 | 22 | ## Get-LPCredential 23 | 24 | This command will return a PSCredential object for the best matching entry for a URL. If more than one entry exists that is equally specific (based on protocol, port number, directory, and query string) then they will all be returned unless you specify the "First" switch. 25 | 26 | ``` 27 | Get-LPCredential "https://example.com" -First 28 | 29 | UserName Password 30 | -------- -------- 31 | username System.Security.SecureString 32 | ``` 33 | 34 | You can also use this command (or its alias "lastpass") inline with other PowerShell commands that take a PSCredential parameter 35 | 36 | ``` 37 | Enter-PSSession -ComputerName server.example.com -Credential (lastpass server.example.com) 38 | ``` 39 | 40 | ## Get-LPAccounts 41 | 42 | This command will return an array of all of your LastPass entries. 43 | 44 | ``` 45 | Get-LPAccounts | Where-Object Name -Like "*example*" 46 | 47 | 48 | URL : https://example.com/ 49 | ID : 1234567890 50 | Username : username 51 | Password : System.Security.SecureString 52 | Notes : Same as the combination on my luggage 53 | SecureNote : 0 54 | Name : Example Site 55 | Group : Best sites ever 56 | PSCredential : System.Management.Automation.PSCredential 57 | ``` 58 | 59 | ## Save-LPData 60 | 61 | This command will save your LastPass credentials, cookie, and optionally your encrypted vault to the %APPDATA% directory on your machine. Everything that is saved is encrypted with the exception of your LastPass email address, its PBKDF2 hash, and the cookie. 62 | 63 | ``` 64 | Save-LPData -SaveVault 65 | ``` 66 | 67 | ## Sync-LPData 68 | 69 | This command forces a refresh of your LastPass data including any new entries you have added since the module was loaded or the vault was saved with Save-LPData. 70 | 71 | ``` 72 | Sync-LPData 73 | ``` 74 | -------------------------------------------------------------------------------- /lib/PBKDF2.NET.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottEvtuch/PSLastPass/5e9c9f9304475a365fdf334d5f5f2a5598975f72/lib/PBKDF2.NET.dll --------------------------------------------------------------------------------