src/common/Secret.ps1

# Copyright 2019, 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.

enum SecretType {
    Certificate
    Password
}

ScriptClass Secret {
    $data = $null
    $type = $null
    $certificatePath = $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] ) {
                $certPath = if ( split-path -isabsolute $secret ) {
                    if ( ! ( test-path $secret ) ) {
                        throw [ArgumentException]::new("The specified path '$secret' is not a valid file system path or cert store path")
                    }
                    if ( (split-path -qualifier $secret ) -eq 'cert:' ) {
                        $secret
                    } else {
                        $this.certificatePath = $secret
                    }
                } else {
                    join-path 'cert:\currentuser\my' $secret
                }

                if ( $certPath ) {
                    get-item $certPath
                }
            } 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 .pfx file in a file system drive" -f $secret.gettype())
            }

            if ( $certificate ) {
                __ValidateCertificate $certificate
            }

            $this.data = $certificate
        }
    }

    function GetSecretData([securestring] $secretPassword) {
        switch ( $this.type ) {
            ([SecretType]::Certificate) {
                if ( ! $this.data -and $this.certificatePath ) {
                    $this.data = if ( $secretPassword ) {
                        [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($this.certificatePath, $secretPassword)
                    } else {
                        [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($this.certificatePath)
                    }
                }

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