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
            }
        }
    }
}