lib.ps1
################ # Shared helpers ################ function epoch([uint64] $Seconds) { (New-Object DateTime @(1970,1,1,0,0,0,0,'Utc')).AddSeconds($seconds).ToLocalTime() } function SecureString2String([SecureString] $ss) { (New-Object PSCredential @('xyz', $ss)).GetNetworkCredential().Password } function ClipboardCopy([string[]] $Data) { # turbo h4x to get stuff working cross-plat, with support for # copying w/o trailing newline $clipTemplate = if (Get-Command 'clip.exe' -CommandType Application -ea 0) { 'cmd.exe /c "type {0} | clip.exe"' } elseif (Get-Command 'pbcopy' -CommandType Application -ea 0) { "bash --noprofile --norc -c `"cat {0} | pbcopy`"" } elseif (Get-Command 'xclip' -CommandType Application -ea 0) { "bash --noprofile --norc -c `"cat {0} | xclip -selection clipboard`"" } else { Write-Error "Unable to locate clipboard utility" } $tmp = [IO.Path]::GetTempFileName() try { [IO.File]::WriteAllText($tmp, $data -join [Environment]::NewLine) Invoke-Expression ($clipTemplate -f $tmp) } finally { Remove-Item $tmp } } function NormalizeEntryType([string] $Type) { switch -regex ($type) { '001|WebForm' { 'Login' } '003|SecureNote' { 'SecureNote' } '005|Password' { 'Password' } 'GenericAccount' { 'GenericAccount' } default { $type } } } function DeriveKeyPbkdf2([string] $Password, [byte[]] $Salt, [int] $Iterations, [int] $byteCount, [string] $HashName) { $passBytes = [System.Text.UTF8Encoding]::UTF8.GetBytes($password) $derivation = # if hash algorithm is SHA1, can use built-in Rfc2898DeriveBytes # otherwise, need to use custom code if ($hashName -eq 'SHA1') { New-Object System.Security.Cryptography.Rfc2898DeriveBytes @($passBytes, $salt, $iterations) } else { $hashAlg = Invoke-Expression "New-Object System.Security.Cryptography.HMAC$hashName" Add-Type -TypeDefinition ((Get-Content "$psScriptRoot/pbkdf2.cs") -join "`n") ` -ReferencedAssemblies ([System.Security.Cryptography.HMAC].Assembly.Location),'System.IO' New-Object Medo.Security.Cryptography.Pbkdf2 @($hashAlg, $passBytes, $salt, $iterations) } $keyData = $derivation.GetBytes($byteCount) [PSCustomObject] @{ Key = $keyData | Select-Object -First ($byteCount / 2) Aux = $keyData | Select-Object -Last ($byteCount / 2) } } function DeriveKeyMD5([byte[]] $Key, [byte[]] $Salt) { $key = $key[0 .. ($key.length - 17)] $md5 = [System.Security.Cryptography.MD5]::Create() $prev = @() $keyData = @() while ($keyData.Length -lt 32) { $prev = $md5.ComputeHash($prev + $key + $salt) $keyData += $prev } [PSCustomObject] @{ Key = $keyData[0 .. 15] Aux = $keyData[16 .. 31] } } function AESDecrypt([byte[]] $Data, [byte[]] $Key, [byte[]] $IV) { $aes = [System.Security.Cryptography.Aes]::Create() $aes.Padding = 'None' $decryptor = $aes.CreateDecryptor($key, $iv) $memStream = New-Object System.IO.MemoryStream @(,$data) $cryptStream = New-Object System.Security.Cryptography.CryptoStream $memStream,$decryptor,'Read' $result = $( $b = $cryptStream.ReadByte() while ($b -ne -1) { $b $b = $cryptStream.ReadByte() } ) $cryptStream.Dispose() $memStream.Dispose() $decryptor.Dispose() $aes.Dispose() $result } function PickDecryptionKey([PSCustomObject] $Entry) { $keys = Get-Content "$($entry.VaultPath)/data/default/encryptionKeys.js" | ConvertFrom-Json |% List if ($entry.KeyId) { $keys |? Identifier -eq $entry.KeyId } else { $keys |? Level -eq $entry.SecurityLevel } } function GetPayloadFromDecryptedEntry([string] $DecryptedJson, [PSCustomObject] $Entry) { $decryptedEntry = $decryptedJson | ConvertFrom-Json $username = $null $password = $null $text = $null Set-StrictMode -Off switch($entry.Type) { 'Login' { $password = $decryptedEntry.fields |? Designation -eq 'password' |% Value $username = $decryptedEntry.fields |? Designation -eq 'username' |% Value } 'Password' { $password = $decryptedEntry.password } 'GenericAccount' { $username = $decryptedEntry.username $password = $decryptedEntry.password } 'SecureNote' { $text = $decryptedEntry.notesPlain } default { Write-Error "Entry type $($entry.Type) is not supported" } } Set-StrictMode -Version 2 [PSCustomObject] @{ Username = $username Password = $password SecureNote = $text } } ####################### # AgileKeychain helpers ####################### function DecodeAgileKeychainSaltedString([string] $EncodedString) { $bytes = [System.Convert]::FromBase64String($encodedString.Trim(0)) [PSCustomObject] @{ Salt = $bytes[8 .. 15] Data = $bytes[16 .. ($bytes.Length - 1)] } } function DecryptAgileKeychainData([string] $Data, [object] $Key, [int] $Iterations, [switch] $MD5, [switch] $Pbkdf2) { $decoded = DecodeAgileKeychainSaltedString $data $finalKey = if ($md5) { DeriveKeyMD5 ([byte[]] $key) $decoded.Salt } elseif ($pbkdf2) { $plainPass = SecureString2String $password DeriveKeyPbkdf2 $plainPass $decoded.Salt $iterations 32 'SHA1' } AESDecrypt $decoded.Data $finalKey.Key $finalKey.Aux } function DecryptAgileKeychainEntry([PSCustomObject] $Entry, [securestring] $Password) { $decryptionKey = PickDecryptionKey $entry $dataKey = DecryptAgileKeychainData -Pbkdf2 $decryptionKey.Data $password $decryptionKey.Iterations $dataKeyCheck = DecryptAgileKeychainData -MD5 $decryptionkey.Validation $dataKey if (Compare-Object $dataKey $dataKeyCheck) { Write-Error "Unable to validate master password" } $entryBytes = DecryptAgileKeychainData -MD5 $entry.EncryptedData $dataKey $entryString = [System.Text.Encoding]::UTF8.GetString($entryBytes).Trim() -replace '\p{C}+$' GetPayloadFromDecryptedEntry $entryString $entry } function GetAgileKeychainEntries([string] $VaultPath, [string] $name) { $contents = Get-Content "$vaultPath/data/default/contents.js" | ConvertFrom-Json $entryIds = $contents |? { $_[2] -like $name } |% { $_[0] } Set-StrictMode -Off $entryIds |%{ Get-ChildItem "$vaultPath/data/default/$_.1password" } | Get-Content | ConvertFrom-Json ` |? { $_.Uuid -and ($_.Trashed -ne 'true') } |% { [PSCustomObject] @{ Name = $_.Title Id = $_.Uuid VaultPath = (Resolve-Path $vaultPath).Path SecurityLevel = $_.SecurityLevel KeyId = $_.KeyId Location = $_.Location CreatedAt = (epoch $_.CreatedAt) Type = (NormalizeEntryType $_.TypeName) LastUpdated = (epoch $_.UpdatedAt) EncryptedData = $_.Encrypted KeyData = $null } } | Add-Member -TypeName 'Entry' -PassThru Set-StrictMode -Version 2 } function 1PTabExpansion($entryStub, $vaultPath) { $quote = $null if ($entryStub -match "^(`"|')") { $quote = $matches[1] $entryStub = $entryStub -replace "^(`"|')+" } $entryStub = $entryStub -replace "(`"|')+$" GetAgileKeychainEntries $vaultPath "$entryStub*" |% Name |% { $localQuote = if ($quote) { $quote } elseif ($_ -match '\s') { "'" } else { $null } "$localQuote$_$localQuote" } } ################# # OPVault helpers ################# function DecryptOPVaulOPData([string] $Data, [PSObject] $Key) { $dataBytes = [Convert]::FromBase64String($data) $dataLen = 0 $mul = 1 $dataBytes[8..15] |% { $dataLen += $mul * $_; $mul *= 256 } $padLength = 16 - ($dataLen % 16) $computedHash = (New-Object System.Security.Cryptography.HMACSHA256 @(,$key.Aux)).ComputeHash(($dataBytes | Select-Object -First (32 + $padLength + $dataLen))) $declaredHash = $dataBytes | Select-Object -Skip (32 + $padLength + $dataLen) if (Compare-Object $computedHash $declaredHash) { Write-Error "Unable to validate master password" } $iv = $dataBytes[16..31] $encryptedBytes = $dataBytes | Select-Object -Skip 32 | Select-Object -First ($dataLen + $padLength) AESDecrypt $encryptedBytes $key.Key $iv | Select-Object -Skip $padLength } function DecryptOPVaultItemKey([string] $Data, [PSObject] $Key) { $dataBytes = [Convert]::FromBase64String($data) $iv = $dataBytes[0..15] $encryptedKey = $dataBytes[16..79] $computedHash = (New-Object System.Security.Cryptography.HMACSHA256 @(,$key.Aux)).ComputeHash(($dataBytes | Select-Object -First 80)) $declaredHash = $dataBytes | Select-Object -Last 32 if (Compare-Object $computedHash $declaredHash) { Write-Error "Unable to validate master password" } AESDecrypt $encryptedKey $key.Key $iv } function GetOPVaultKeyFromBytes([byte[]] $Bytes, [switch] $NoHash) { $resultBytes = if ($noHash) { $bytes } else { [System.Security.Cryptography.SHA512]::Create().ComputeHash($bytes) } [PSCustomObject] @{ Key = $resultBytes | Select-Object -First 32 Aux = $resultBytes | Select-Object -Last 32 } } function DecryptOPVaultEntry([PSCustomObject] $Entry, [securestring] $Password) { $vaultProfile = ((Get-Content "$($entry.VaultPath)/default/profile.js") -replace '^var profile=(.+);$','$1') | ConvertFrom-Json $plainPass = SecureString2String $password $derivedKey = DeriveKeyPbkdf2 $plainPass ([Convert]::FromBase64String($vaultProfile.Salt)) $vaultProfile.Iterations 64 'SHA512' $encryptionKeyData = DecryptOPVaulOPData $vaultProfile.MasterKey $derivedKey $encryptionKey = GetOPVaultKeyFromBytes $encryptionKeyData $itemKeyBytes = DecryptOPVaultItemKey $entry.KeyData $encryptionKey $itemKey = GetOPVaultKeyFromBytes $itemKeyBytes -NoHash $entryBytes = DecryptOPVaulOPData $entry.EncryptedData $itemKey $entryString = [System.Text.Encoding]::UTF8.GetString($entryBytes) GetPayloadFromDecryptedEntry $entryString $entry } function GetOPVaultEntries([string] $VaultPath, [string] $Name, [securestring] $Password) { $vaultProfile = ((Get-Content "$vaultPath/default/profile.js") -replace '^var profile=(.+);$','$1') | ConvertFrom-Json $plainPass = SecureString2String $password $derivedKey = DeriveKeyPbkdf2 $plainPass ([Convert]::FromBase64String($vaultProfile.Salt)) $vaultProfile.Iterations 64 'SHA512' $overviewKeyData = DecryptOPVaulOPData $vaultProfile.OverviewKey $derivedKey $overviewKey = GetOPVaultKeyFromBytes $overviewKeyData $entries = Get-ChildItem "$vaultPath/default/band_*.js" | Get-Content |% { $bandEntries = $_ -replace '^ld\((.+)\);$', '$1' | ConvertFrom-Json $bandEntries | Get-Member -MemberType NoteProperty |% Name |% { $bandEntries.$_ } } Set-StrictMode -Off $entries |? Category -ne '099' |%{ $entryBytes = DecryptOPVaulOPData $_.o $overviewKey $entryData = [System.Text.Encoding]::UTF8.GetString($entryBytes) | ConvertFrom-Json [PSCustomObject] @{ Name = $entryData.Title Id = $_.Uuid VaultPath = (Resolve-Path $vaultPath).Path SecurityLevel = $null KeyId = $null Location = $entryData.Url CreatedAt = (epoch $_.Created) Type = (NormalizeEntryType $_.Category) LastUpdated = (epoch $_.Updated) EncryptedData = $_.D KeyData = $_.K } | Add-Member -TypeName 'Entry' -PassThru } |? Name -like $name Set-StrictMode -Version 2 } |