PersonalVault.functions.ps1
function _generateKey { $newChar = @() $char = [char[]](48..93) $char += [char[]](97..122) For($i=0; $i -lt $char.Count; $i++) { $newChar += $char[$i] } [String]$p = Get-Random -InputObject $newChar -Count 32 return $p.Replace(" ","") } function _getBytes([string] $key) { return [System.Text.Encoding]::UTF8.GetBytes($key) } function _encrypt([string] $plainText, [string] $key) { return $plainText | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString -Key (_getBytes $key) } function _decrypt([string] $encryptedText, [string] $key) { try { $cred = [pscredential]::new("x", ($encryptedText | ConvertTo-SecureString -Key (_getBytes $key) -ErrorAction SilentlyContinue)) return $cred.GetNetworkCredential().Password } catch { Write-Warning "Cannot get the value as plain text; Use the right key to get the secret value as plain text." } } function _getOS { if ($env:OS -match 'Windows') { return 'Windows' } elseif ($IsLinux) { return 'Linux' } elseif ($IsMacOs -or $IsOSX) { return 'MacOs' } } function _getUser { # should work in both Mac and Linux return [System.Environment]::UserName } function _clearHistory([string] $functionName) { $path = (Get-PSReadLineOption).HistorySavePath $contents = Get-Content -Path $path if ($contents -notmatch $functionName) { $contents -notmatch $functionName | Set-Content -Path $path -Encoding UTF8 } } function _createDb { $path = Join-Path -Path $Home -ChildPath ".cos_$((_getUser).ToLower())" $pathExists = Test-Path $path $file = Join-Path -Path $path -ChildPath "_.db" # Metadata section is required so that we know what we are storing. $query = "CREATE TABLE _ (Name NVARCHAR PRIMARY KEY, Value TEXT, Metadata TEXT)" $fileExists = Test-Path $file if (!$pathExists) { $null = New-Item -Path $path -ItemType Directory } if (!$fileExists) { $null = New-Item -Path $file -ItemType File Invoke-SqliteQuery -DataSource $file -Query $query _hideFile $file } return $file } function _connectionWarning { Write-Warning "You must create a connection to the vault to manage the secrets. Check your connection object and pass the right credential." } function _getDbPath { return _createDb } function _getKeyFile { $path = Split-Path -Path (_getDbPath) -Parent return (Join-Path -Path $path -ChildPath "private.key") } function _getConnectionFile { $path = Split-Path -Path (_getKeyFile) -Parent return (Join-Path -Path $path -ChildPath "connection.clixml") } function _archiveKeyFile { $path = Split-Path -Path (_getKeyFile) -Parent [string] $keyFile = _getKeyFile $file = $keyFile.Replace("private", "private_$(Get-Date -Format ddMMyyyy-HH_mm_ss)") $archivePath = Join-Path -Path $path -ChildPath "archive" if (!(Test-Path $archivePath)) { $null = New-Item -Path $archivePath -ItemType Directory } _unhideFile (_getKeyFile) Rename-Item -Path (_getKeyFile) -NewName $file Move-Item -Path $file -Destination "$archivePath" -Force } function _isKeyFileExists { return (Test-Path (_getKeyFile)) } function _saveKey([string] $key, [switch] $force) { $file = _getKeyFile $fileExists = _isKeyFileExists if ($fileExists -and !$force.IsPresent) { Write-Warning "Key file already exists; Use Force parameter to update the file." } if ($fileExists -and $force.IsPresent) { _unhideFile $file $encryptedKey = [pscredential]::new("key", ($key | ConvertTo-SecureString -AsPlainText -Force)) $encryptedKey.Password | Export-Clixml -Path $file -Force _hideFile $file } if (!$fileExists) { $encryptedKey = [pscredential]::new("key", ($key | ConvertTo-SecureString -AsPlainText -Force)) $encryptedKey.Password | Export-Clixml -Path $file -Force _hideFile $file } } function _hideFile([string] $filePath) { if ((_getOS) -eq "Windows") { if ((Get-Item $filePath -Force).Attributes -notmatch 'Hidden') { (Get-Item $filePath).Attributes += 'Hidden' } } } function _unhideFile([string] $filePath) { if ((_getOS) -eq "Windows") { if ((Get-Item $filePath -Force).Attributes -match 'Hidden') { (Get-Item $filePath -Force).Attributes -= 'Hidden' } } } function _isNameExists([string] $name) { return [bool] (Get-PSSecret -Name $name -WarningAction 'SilentlyContinue') } function _getHackedPasswords { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline=$true)] [String[]]$secureStringList ) begin { #initialize function variables $encoding = [System.Text.Encoding]::UTF8 $result = @() $hackedCount = @() } process { foreach ($string in $secureStringList) { $SHA1Hash = New-Object -TypeName "System.Security.Cryptography.SHA1CryptoServiceProvider" $Hashcode = ($SHA1Hash.ComputeHash($encoding.GetBytes($string)) | ` ForEach-Object { "{0:X2}" -f $_ }) -join "" $Start, $Tail = $Hashcode.Substring(0, 5), $Hashcode.Substring(5) $Url = "https://api.pwnedpasswords.com/range/" + $Start $Request = Invoke-RestMethod -Uri $Url -UseBasicParsing -Method Get $hashedArray = $Request.Split() foreach ($item in $hashedArray) { if (!([string]::IsNullOrEmpty($item))) { $encodedPassword = $item.Split(":")[0] $count = $item.Split(":")[1] $Hash = [PSCustomObject]@{ "HackedPassword" = $encodedPassword.Trim() "Count" = $count.Trim() } $result += $Hash } } foreach ($pass in $result) { if($pass.HackedPassword -eq $Tail) { $newHash = [PSCustomObject]@{ Name = $string Count = $pass.Count } $hackedCount += $newHash } } if ($string -notin $hackedCount.Name) { $finalHash = [PSCustomObject]@{ Name = $string Count = 0 } $hackedCount += $finalHash } } return $hackedCount } } function _isHacked([string] $value) { $res = (_getHackedPasswords $value -ErrorAction SilentlyContinue).Count if ($res -gt 0) { Write-Warning "Secret '$value' was hacked $($res) time(s); Consider changing the secret value." } } function _clearPersonalVault { Remove-Item -Path (Split-Path -Path (_getDbPath) -Parent) -Recurse -Force } function _clearConnection { Remove-Item -Path (_getConnectionFile) -Force [System.Environment]::SetEnvironmentVariable("PERSONALVAULT_U", "", [System.EnvironmentVariableTarget]::Process) [System.Environment]::SetEnvironmentVariable("PERSONALVAULT_P", "", [System.EnvironmentVariableTarget]::Process) } function _isValidConnection ([PersonalVault] $connection) { $verified = $false if (($null -ne $connection.UserName) -and ($null -ne $connection.Password)) { if (Test-Path -Path (_getConnectionFile)) { $properties = Import-Clixml -Path (_getConnectionFile) $prop = [pscredential]::new($properties.UserName, $properties.Password) $propPassword = $prop.GetNetworkCredential().Password $conn = [pscredential]::new($connection.UserName, $connection.Password) $connPassword = $conn.GetNetworkCredential().Password if (($prop.UserName -eq $conn.UserName) -and ($propPassword -eq $connPassword)) { $verified = $true } } } return $verified } function _setEnvironmentVariable ([string] $key, [string] $value) { if (!([string]::IsNullOrEmpty($key)) -and !([string]::IsNullOrEmpty($value))) { [System.Environment]::SetEnvironmentVariable($key, $value, [System.EnvironmentVariableTarget]::Process) } } function _getEnvironmentVariable([string] $key) { if (!([string]::IsNullOrEmpty($key))) { return [System.Environment]::GetEnvironmentVariable($key) } } function _getConnectionObject { $connection = [PersonalVault]::new() $userName = _getEnvironmentVariable -key "PERSONALVAULT_U" $password = (_getEnvironmentVariable -key "PERSONALVAULT_P") if (!([string]::IsNullOrEmpty($userName)) -and !([string]::IsNullOrEmpty($password))) { $connection.UserName = $userName $connection.Password = $password | ConvertTo-SecureString -ErrorAction SilentlyContinue return $connection } } function _isValidRecoveryWord ([securestring] $recoveryWord) { $res = Import-Clixml -Path (_getConnectionFile) $key = [pscredential]::new("Key", $res.Key) $recKey = [pscredential]::new("Key", $recoveryWord) return ($recKey.GetNetworkCredential().Password -eq $key.GetNetworkCredential().Password) } function Add-PSSecret { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] $Value, [Parameter( Mandatory = $true, Position = 2, HelpMessage = "Provide the details of what you are storing", ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] $Metadata, [ValidateNotNullOrEmpty()] [string] $Key = (Get-PSKey -WarningAction SilentlyContinue) ) process { if (_isValidConnection (_getConnectionObject)) { _isHacked $Value if (_isNameExists $Name) { Write-Warning "Given name '$Name' already exists; Pass different name and try again." } else { $encryptedValue = _encrypt -plainText $Value -key $Key # create the database and save the KV pair $null = _createDb Invoke-SqliteQuery ` -DataSource (_getDbPath) ` -Query "INSERT INTO _ (Name, Value, Metadata) VALUES (@N, @V, @M)" ` -SqlParameters @{ N = $Name V = $encryptedValue M = $Metadata } # cleaning up _clearHistory $MyInvocation.MyCommand.Name } } else { _connectionWarning } } } function Connect-PSPersonalVault { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [pscredential] $Credential ) process { $personalVault = [PersonalVault]::new() $personalVault.UserName = if ([string]::IsNullOrEmpty($Credential.UserName)) { _getUser } else { $Credential.UserName } $personalVault.Password = $Credential.Password # Return the PersonalVault object so that it can be consumed and verified by other cmdlets _setEnvironmentVariable -key "PERSONALVAULT_U" -value $personalVault.UserName _setEnvironmentVariable -key "PERSONALVAULT_P" -value ($personalVault.Password | ConvertFrom-SecureString) return $personalVault } } function Get-PSArchivedKey { [CmdletBinding()] [OutputType([object[]])] param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [datetime] $DateModified ) process { if (_isValidConnection (_getConnectionObject)) { $archivePath = Join-Path -Path (Split-Path -Path (_getKeyFile) -Parent) -ChildPath "archive" if (Test-Path $archivePath) { $results = @() $archivedFiles = Get-ChildItem -Path $archivePath | Select-Object FullName, LastWriteTime if ($PSBoundParameters.ContainsKey('DateModified')) { $archivedFiles = $archivedFiles | Where-Object { (Get-Date $_.LastWriteTime -Format ddMMyyyy) -eq (Get-Date $DateModified -Format ddMMyyyy) } } $archivedFiles | ForEach-Object { $key = Import-Clixml $_.FullName $keyObj = [pscredential]::new("key", $key) $obj = [PSCustomObject]@{ DateModified = $_.LastWriteTime Key = $keyObj.GetNetworkCredential().Password } $results += $obj } return $results } } else { _connectionWarning } } } function Get-PSKey { [CmdletBinding()] [OutputType([string])] param ( [switch] $Force ) process { if (_isValidConnection (_getConnectionObject)) { if (_isKeyFileExists) { $res = Import-Clixml (_getKeyFile) $key = [pscredential]::new("key", $res) $key = $key.GetNetworkCredential().Password } if (!(_isKeyFileExists)) { $key = _generateKey _saveKey -key $key } if ($Force.IsPresent) { _archiveKeyFile $key = _generateKey; _saveKey -key $key -force } return $key } else { _connectionWarning } } } function Get-PSSecret { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] param ( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ArgumentCompleter([NameCompleter])] [ValidateNotNullOrEmpty()] [string] $Name, [ValidateNotNullOrEmpty()] [string] $Key = (Get-PSKey -WarningAction SilentlyContinue), [switch] $AsPlainText ) process { # check if the credential are valid. if (_isValidConnection (_getConnectionObject)) { $res = Invoke-SqliteQuery -DataSource (_getDbPath) -Query "SELECT * FROM _" if (!($AsPlainText.IsPresent) -and ($PSBoundParameters.ContainsKey('Name'))) { $res = $res | Where-Object { $_.Name -eq $Name } | Select-Object -ExpandProperty Value if (!$res) { Write-Warning "Couldn't find the value for given Name '$Name'; Pass the correct value and try again." } else { return $res } } if ($AsPlainText.IsPresent -and ($PSBoundParameters.ContainsKey('Name'))) { $res = $res | Where-Object { $_.Name -eq $Name } | Select-Object -ExpandProperty Value if (!$res) { Write-Warning "Couldn't find the value for given Name '$Name'; Pass the correct value and try again." } else { return _decrypt -encryptedText $res -key $Key } } if ($AsPlainText.IsPresent -and !($PSBoundParameters.ContainsKey('Name'))) { $result = @() $res | ForEach-Object { $r = [PSCustomObject]@{ Name = $_.Name Value = (_decrypt -encryptedText $_.Value -key $Key) Metadata = $_.Metadata } $result += $r } if ([bool] ($result.Value)) { return $result } } else { return $res } } else { _connectionWarning } } } function Import-PSPersonalVault { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [securestring] $RecoveryWord ) process { if (_isValidConnection (_getConnectionObject)) { if (_isValidRecoveryWord $RecoveryWord) { $res = Import-Clixml -Path (_getConnectionFile) return [PSCustomObject]@{ UserName = $res.UserName Password = ([pscredential]::new("P", $res.Password)).GetNetworkCredential().Password } } else { Write-Warning "Recovery word is incorrect. Please pass the valid recovery word and try again." } } else { _connectionWarning } } } function Register-PSPersonalVault { [CmdletBinding(DefaultParameterSetName = "Default")] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [pscredential] $Credential, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [securestring] $RecoveryWord, [switch] $Force ) process { $personalVault = [PersonalVault]::new() $personalVault.UserName = if ([string]::IsNullOrEmpty($Credential.UserName)) { _getUser } else { $Credential.UserName } $personalVault.Password = $Credential.Password $personalVault.Key = $RecoveryWord if (!(Test-Path (_getConnectionFile))) { $personalVault | Export-Clixml -Path (_getConnectionFile) } if ($Force.IsPresent) { if (_isValidConnection (_getConnectionObject)) { $personalVault | Export-Clixml -Path (_getConnectionFile) -Force } else { _connectionWarning } } } } function Remove-PSPersonalVault { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [switch] $Force ) process { if (_isValidConnection (_getConnectionObject)) { if ($Force.IsPresent -or $PSCmdlet.ShouldProcess("Personal Vault", "Remove-PSPersonalVault")) { _clearPersonalVault } } else { _connectionWarning } } } function Remove-PSPersonalVaultConnection { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [switch] $Force ) process { if (_isValidConnection (_getConnectionObject)) { if ($Force.IsPresent -or $PSCmdlet.ShouldProcess("Connection", "Remove-PSPersonalVaultConnection")) { _clearConnection } } else { _connectionWarning } } } function Remove-PSSecret { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ArgumentCompleter([NameCompleter])] [ValidateNotNullOrEmpty()] [string] $Name, [switch] $Force ) process { if (_isValidConnection (_getConnectionObject)) { if (!(_isNameExists $Name)) { Write-Warning "Couldn't find the value for given Name '$Name'; Pass the correct value and try again." } else { if ($Force -or $PSCmdlet.ShouldProcess($Name, "Remove-Secret")) { Invoke-SqliteQuery -DataSource (_getDbPath) -Query "DELETE FROM _ WHERE Name = '$Name'" } } } else { _connectionWarning } } } function Update-PSSecret { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ArgumentCompleter([NameCompleter])] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] $Value, [ValidateNotNullOrEmpty()] [string] $Key = (Get-PSKey -WarningAction SilentlyContinue), [switch] $Force ) process { if (_isValidConnection (_getConnectionObject)) { _isHacked $Value if (!(_isNameExists $Name)) { Write-Warning "Couldn't find the value for given Name '$Name'; Pass the correct value and try again." } else { if ($Force -or $PSCmdlet.ShouldProcess($Value, "Update-Secret")) { $encryptedValue = _encrypt -plainText $Value -key $Key Invoke-SqliteQuery ` -DataSource (_getDbPath) ` -Query "UPDATE _ SET Value = '$encryptedValue' WHERE Name = '$Name'" # cleaning up _clearHistory $MyInvocation.MyCommand.Name } } } else { _connectionWarning } } } |