Secretify.psm1

Import-Module $PSScriptRoot\Crypto.psm1 -Force

# consts
$AUTH_GRANT_CLIENTCREDENTIALS = "client_credentials"


Export-ModuleMember -Variable AUTH_GRANT_CLIENTCREDENTIALS

function New-SecretAPI {
    param (
        [string] $apiURL,
        [int]$typeID,
        [PSCustomObject]$cipher,
        [string]$expiresAt,
        [int]$views,
        [bool]$isDestroyable,
        [bool]$isRequest,
        [bool]$hasPassphrase,
        [string]$fileIdentifiers,
        [string]$token,
        [string]$provider
    )

    $headers = @{}
    if ($token) {
        $headers.Add("Authorization", "Bearer $token")
    }

    $body = @{
        type_id        = $typeID
        cipher         = $cipher
        expires_at     = $expiresAt
        views          = $views
        is_destroyable = $isDestroyable
        is_request     = $isRequest
        has_passphrase = $hasPassphrase
    } | ConvertTo-Json

    Write-Debug "Trying with url $apiURL"

    try {
        $response = Invoke-RestMethod -Uri $apiURL -Method Post -Headers $headers -Body $body -ContentType "application/json" -SkipCertificateCheck | ConvertTo-Json -Depth 10
    }
    catch {
        throw "An error occurred during the REST API call for secret creation. Details: $_"
    }
    return $response
}

function Get-Secret-API {
    param (
        [string] $apiURL,
        [string]$identifier
    )

    $headers = @{}
    if ($token) {
        $headers.Add("Authorization", "Bearer $token")
    }

    $url = "$($apiURL)/secret/$($identifier)/_cipher"

    $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers -ContentType "application/json" -SkipCertificateCheck  | ConvertTo-Json -Depth 10
    return $response
}

# Class

class Secretify {
    [string]$APIURL
    [string]$UIURL
    [string]$AccessToken
    [array]$Types

    Secretify([string]$URL) {
        $this.APIURL = "$($URL)/api/v1"
        $this.UIURL = $URL
        $this.Types = $null  # Initialize Types as null
        $this.InitializeTypes()
    }

    [void]InitializeTypes() {
        $typesUrl = "$($this.APIURL)/type"
        try {
            $response = Invoke-RestMethod -Uri $typesUrl -Method Get -ContentType "application/json" -SkipCertificateCheck -ErrorAction Stop
            $this.Types = $response.data.types
        }
        catch {
            throw "Failed to retrieve types. Error: $_"
        }
    }

    [void]Authenticate([string]$grantType, [hashtable]$credentials) {
        $authUrl = "$($this.APIURL)/auth/microsoftonline"
        $authBody = @{
            grant_type    = $grantType
            client_id     = $credentials.ClientID
            client_secret = $credentials.ClientSecret
        } | ConvertTo-Json

        try {
            $authResponse = Invoke-RestMethod -Uri $authUrl -Method Post -Body $authBody -ContentType "application/json" -SkipCertificateCheck -ErrorAction Stop  | ConvertTo-Json -Depth 10
            $authResponseObject = $authResponse | ConvertFrom-Json

            if ($authResponseObject -and $authResponseObject.data.access_token) {
                # Store the access token
                $this.AccessToken = $authResponseObject.data.access_token
            }
            else {
                throw "Failed to authenticate. Unexpected response format."
            }
        }
        catch {
            throw "Failed to authenticate. Error: $_"
        }
    }
    
    [System.Collections.Hashtable]Create([string]$typeIdentifier, [hashtable]$attributes, [hashtable]$options) {
        # Retrieve type ID based on type identifier
        $type = $this.Types | Where-Object { $_.identifier -eq $typeIdentifier }

        if ($type -eq $null) {
            throw "Invalid type identifier: $typeIdentifier"
        }
        Write-Debug "Type is $type"

        # Options
        $typeID = $type.id
        $isDestroyable = $true
        $isRequest = $false
        $hasPassphrase = $false
        $createUrl = "$($this.APIURL)/secret"
        $views = $options.views
        $expiresAt = $options.expiresAt
    
        # Generate key
        $key = New-EncryptionKey
        
        # Loop through attributes and encrypt each one
        $cipherParams = @{}
        foreach ($attributeName in $attributes.Keys) {
            $textToEncrypt = $attributes[$attributeName]
            $encryptedData = Protect-String -textToEncrypt $textToEncrypt -encryptionKey (ConvertFrom-Base64Url $key)
            $cipherParams[$attributeName] = $encryptedData
        }
    
        # Create secret via API
        $response = New-SecretAPI -apiURL $createUrl -typeID $typeID -cipher $cipherParams -expiresAt $expiresAt -views $views -isDestroyable $isDestroyable -isRequest $isRequest -hasPassphrase $hasPassphrase | ConvertFrom-Json

        $identifier = $response.data.identifier
        
        return @{
            Link       = "$($this.UIURL)/s/$identifier#$key"
            Identifier = $identifier
            Key        = $key
        }
    }

    [object]RevealLink([string]$secretLink) {
        # Extract identifier and key from the secret link
        $secretLinkParts = $secretLink -split '#'
        $identifier = $secretLinkParts[0] -replace '^.*\/s\/'
        $key = $secretLinkParts[1]

        Write-Debug "Extracted Identifier: $identifier"
        Write-Debug "Extracted Key: $key"

        return $this.InvokeRevealLogic($identifier, $key)
    }

    [object]Reveal([string]$identifier, [string]$key) {
        return $this.InvokeRevealLogic($identifier, $key)
    }

    [object]InvokeRevealLogic([string]$Identifier, [string]$Key) {
        $reponse = Get-Secret-API -apiURL $this.APIURL -identifier $Identifier | ConvertFrom-Json

        $cipher = $reponse.data.cipher | ConvertFrom-Json

        try {
            $decryptedAttributes = @{}
            foreach ($attributeName in $cipher.PSObject.Properties.Name) {
                $encryptedData = $cipher.$attributeName
                $decryptedData = Unprotect-String -encryptedData $encryptedData -encryptionKey (ConvertFrom-Base64Url $Key)
                $decryptedAttributes[$attributeName] = $decryptedData
            }
        }
        catch {
            throw 'Could not decrypt cipher attribute'
        }

        return [PSCustomObject]$decryptedAttributes
    }

}