Posh-HIBP.psm1

#region Setup


function Get-HibpCredential {

    [CmdletBinding()]
    Param ()

    $aKeyPtr = [IntPtr]::Zero

    try {

        $secureKey = $env:HibpApiKey | ConvertTo-SecureString -ErrorAction SilentlyContinue
        if (-not $secureKey) { return $null }

        $aKeyPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($secureKey)

        $aKey = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($aKeyPtr)
    }
    catch {

        return $null
    }
    finally {

        # Always free the unmanaged memory to reduce exposure of sensitive data.
        if ($aKeyPtr -ne [IntPtr]::Zero) {
            [System.Runtime.InteropServices.Marshal]::ZeroFreeCoTaskMemUnicode($aKeyPtr)
        }
    }

    return $aKey
}

function Remove-HibpCredential {

    [CmdletBinding()]
    Param ()

    $apiKeyVarName = 'HibpApiKey'

    if (Test-Path -Path ('env:{0}' -f $apiKeyVarName)) {
        Remove-Item -Path ('env:{0}' -f $apiKeyVarName)
    }

    if (Test-Path "HKCU:\Environment\$apiKeyVarName") {
        Remove-ItemProperty -Path "HKCU:\Environment" -Name $apiKeyVarName
    }
}

function Save-HibpCredential {

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]$ApiKey
    )

    $apiKeyVarName = 'HibpApiKey'

    # Securely stores the API key as an encrypted string in a user-level environment variable.
    $secureKey = ConvertTo-SecureString -String $ApiKey -AsPlainText -Force
    $encryptedKey = ConvertFrom-SecureString -SecureString $secureKey

    # Set for future sessions for the current user
    [System.Environment]::SetEnvironmentVariable($apiKeyVarName, $encryptedKey, 'User')

    # Setting for use in the current process, as it is necessary to reload the profile to make environmental variables available.
    $env:HibpApiKey = $encryptedKey
}


#endregion

#region Private Functions


function Initialize-HibpRequirements {
    # Ensures TLS 1.2 is enabled for web requests.
    # This relies on the 'core' module being available.
    if (-not ([System.Net.ServicePointManager]::SecurityProtocol.HasFlag([System.Net.SecurityProtocolType]::Tls12))) {
        try {

            Set-WebSecurityProtocol -Protocols 'TLS1.2' -Append -Quiet
        }
        catch {

            Write-Warning "Failed to set TLS 1.2. API calls may fail. Error: $_"
        }
    }
}

function Invoke-HibpRequest {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [string]$Endpoint,

        [System.Collections.IDictionary]$QueryParameter,

        [string]$ApiKey = (Get-HibpCredential)
    )

    Initialize-HibpRequirements

    if (-not $ApiKey) {
        throw 'HIBP API key not found. Provide the key using the -ApiKey parameter or by running Save-HibpCredential.'
    }

    $headers = @{
        'hibp-api-key' = $ApiKey
        'user-agent'   = 'Posh-HIBP-PowerShell-Module'
    }

    $baseUri = 'https://haveibeenpwned.com/api/v3'
    $uri = '{0}/{1}' -f $baseUri, $Endpoint

    if ($QueryParameter) {
        $query = $QueryParameter.GetEnumerator() | ForEach-Object {
            '{0}={1}' -f $_.Key, ([System.Web.HttpUtility]::UrlEncode($_.Value))
        }

        $uri = '{0}?{1}' -f $uri, ($query -join '&')
    }

    try {
        Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -ErrorAction Stop
    }
    catch {
        $response = $_.Exception.Response
        if ($null -ne $response) {
            $statusCode = $response.StatusCode.value__
            Write-Error ('API request failed with status code {0}. Message: {1}' -f $statusCode, $_.Exception.Message)
        }
        else {
            Write-Error ('An unknown error occurred: {0}' -f $_.Exception.Message)
        }
    }
}


#endregion

#region Public Functions


$publicFunctionPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public'
$publicFunctions = @()

if (Test-Path -Path $publicFunctionPath) {

    $functionFiles = Get-ChildItem -Path $publicFunctionPath -Filter *.ps1

    foreach ($file in $functionFiles) {
        try {
            . $file.FullName
            $publicFunctions += $file.BaseName
            Write-Verbose "Imported function $($file.BaseName)"
        }
        catch {
            Write-Error "Failed to import function $($file.FullName): $_"
        }
    }
}
else {
    Write-Warning "No Public functions directory found at $publicFunctionPath"
}

Export-ModuleMember -Function $publicFunctions


#endregion