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 _getDbPath { return _createDb } function _getKeyFile { $path = Split-Path -Path (_getDbPath) -Parent return (Join-Path -Path $path -ChildPath "private.key") } 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 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 ) process { _isHacked $Value if (_isNameExists $Name) { Write-Warning "Given name '$Name' already exists; Pass different name and try again." } else { if (!($PSBoundParameters.ContainsKey('Key'))) { $Key = Get-PSKey } $encryptedValue = _encrypt -plainText $Value -key $Key # create the database and save the KV pair $null = _createDb _unhideFile (_getDbPath) Invoke-SqliteQuery ` -DataSource (_getDbPath) ` -Query "INSERT INTO _ (Name, Value, Metadata) VALUES (@N, @V, @M)" ` -SqlParameters @{ N = $Name V = $encryptedValue M = $Metadata } _hideFile (_getDbPath) # cleaning up _clearHistory $MyInvocation.MyCommand.Name } } } function Get-PSArchivedKey { [CmdletBinding()] [OutputType([object[]])] param ( [ValidateNotNullOrEmpty()] [datetime] $DateModified ) process { if (Test-Path "$(Split-Path -Path (_getKeyFile) -Parent)\archive") { $results = @() $archivedFiles = Get-ChildItem -Path "$(Split-Path -Path (_getKeyFile) -Parent)\archive" | 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 } } } function Get-PSKey { [CmdletBinding()] [OutputType([string])] param ( [switch] $Force ) process { 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 } } function Get-PSSecret { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ArgumentCompleter([NameCompleter])] [ValidateNotNullOrEmpty()] [string] $Name, [string] $Key = (Get-PSKey), [switch] $AsPlainText ) process { _unhideFile (_getDbPath) $res = Invoke-SqliteQuery -DataSource (_getDbPath) -Query "SELECT * FROM _" _hideFile (_getDbPath) 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 (!([string]::IsNullOrEmpty($result.Value))) { return $result } } else { return $res } } } function Remove-PSPersonalVault { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [switch] $Force ) process { if ($Force.IsPresent -or $PSCmdlet.ShouldProcess("Peronal Vault", "Remove-PSPersonalVault")) { _clearPersonalVault } } } 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 (!(_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'" } } } } 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, [switch] $Force ) process { _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")) { if (!($PSBoundParameters.ContainsKey('Key'))) { $Key = Get-PSKey } _unhideFile (_getDbPath) $encryptedValue = _encrypt -plainText $Value -key $Key Invoke-SqliteQuery ` -DataSource (_getDbPath) ` -Query "UPDATE _ SET Value = '$encryptedValue' WHERE Name = '$Name'" _hideFile (_getDbPath) # cleaning up _clearHistory $MyInvocation.MyCommand.Name } } } } |