src/common/Secret.ps1

# Copyright 2021, Adam Edwards
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

. (import-script LocalCertificate)

enum SecretType {
    Certificate
    Password
}

ScriptClass Secret {
    $data = $null
    $type = $null
    $certificateFilePath = $null

    function __initialize($secret) {

        if ( ! $secret ) {
            throw [ArgumentException]::new('Secret was null or an empty string')
        }

        if ( $secret -is [SecureString] ) {
            $this.type = ([SecretType]::Password)
            $this.data = $secret
        } else {
            $this.type = ([SecretType]::Certificate)
            $certificate = if ( $secret -is [System.Security.Cryptography.X509Certificates.X509Certificate2] ) {
                $secret
            } elseif ( $secret -is [string] ) {
                $certItem = $::.LocalCertificate |=> GetCertificateItemFromPath $secret 'Cert:\CurrentUser\my'

                if ( $certItem -is [System.Security.Cryptography.X509Certificates.X509Certificate2] ) {
                    $certItem
                } elseif ( $certItem -is [System.IO.FileInfo] ) {
                    # Don't return anything, but save the path for later so that it can be
                    # accessed when it's used, which may be never -- this matters because
                    # for a file system cert, we will likely need to prompt a user for a password,
                    # and that should only be done at the point it is used.
                    $this.certificateFilePath = $certItem.FullName
                }
            } else {
                throw [ArgumentException]::new("Secret was of invalid type '{0}', it must be a [SecureString], [X509Certificate2], or [String] path to a certificate in the PowerShell certificate drive or path to a certificate file with a private key in a file system drive" -f $secret.gettype())
            }

            if ( $certificate ) {
                __ValidateCertificate $certificate
            }

            $this.data = $certificate
        }
    }

    function GetCertificatePath {
        $this.certificateFilePath
    }

    function GetSecretData([securestring] $secretPassword) {
        switch ( $this.type ) {
            ([SecretType]::Certificate) {
                if ( ! $this.data -and $this.certificateFilePath ) {
                    $this.data = $::.LocalCertificate |=> GetCertificateFromPath $this.certificateFilePath $null $true $secretPassword
                }

                $this.data
            }
            ([SecretType]::Password) {
                __DecryptSecureString $this.data
            }
            default {
                throw [ArgumentException]::("Unknown secret type '{0}'" -f $this.tostring())
            }
        }
    }

    function __DecryptSecureString( $secureString ) {
        $pscred = new-object PSCredential '.', $SecureString
        $pscred.GetNetworkCredential().Password
    }

    function __ValidateCertificate( $certificate ) {
        $certDescription = "thumbprint = {0}, subject = {1}" -f $certificate.thumbprint, $certificate.subject
        if ( ! $certificate.hasprivatekey ) {
            throw [ArgumentException]::new("The specified certificate '$certDescription' does not have a private key")
        }

        if ( ! $certificate.privatekey ) {
            $knownGoodProviders = @('Microsoft Enhanced RSA and AES Cryptographic Provider', 'Microsoft RSA SChannel Cryptographic Provider') -join "`n"
            throw [ArgumentException]::new("The specified certificate '$certDescription' is marked as having a private key, but no private key data is available through the [X509Certificate2] type. Try with a new certificate with a different cryptographic provider. If you use the 'New-SelfSignedCertificate' cmdlet, you can specify the provider through the '-provider' option. Providers known to be compatible with the [X509Certificate2] type include and are not limited to the following:`n$knownGoodProviders")
        }

        $currentTime = [DateTime]::Now

        if ( $certificate.NotAfter.CompareTo($currentTime) -lt 0 ) {
            throw [ArgumentException]::new(("The specified certificate '$certDescription' is expired -- current time is '{0}' and the certificate expiration time is '{1}'" -f $currentTime, $certificate.NotAfter))
        }

        if ( $certificate.NotBefore.CompareTo($currentTime) -gt 0 ) {
            write-warning ("Certificate '$certDescription': Current time is {0}, specified certificate is not valid before {1}" -f $currentTime, $certificate.NotBefore)
        }
    }

    static {
        function ToDisplayableSecretInfo($secretType, $displayName, $subject, $graphKeyId, $appId, $appObjectId, $notBefore, $notAfter, $customKeyId, $localPath, $typeName, $exportedCertificatePath) {

            $certificatePath = if ( $secretType -eq 'Certificate' ) {
                $localPath
                if ( $localPath ) {
                }
            }

            # TODO: Make this generic, right now it assumes
            # this is a certificate
            $remappedObject = [PSCustomObject] @{
                Thumbprint = $customKeyId
                AppId = $appId
                KeyId = $graphKeyId
                FriendlyName = $displayName
                Subject = $subject
                NotAfter = $notAfter
                NotBefore = $notBefore
                CertificatePath = $certificatePath
                ExportedCertificatePath = $exportedCertificatePath
                AppObjectId = $appObjectId
            }

            if ( $typeName ) {
                $remappedObject.pstypenames.insert(0, $typeName)
            }

            $remappedObject
        }
    }
}