// Copyright 2016-2018, Pulumi Corporation.
//
// 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.

// Package cloud implements support for a generic cloud secret manager.
package cloud

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	netUrl "net/url"
	"os"
	"strings"

	gosecrets "gocloud.dev/secrets"
	_ "gocloud.dev/secrets/awskms"        // support for awskms://
	_ "gocloud.dev/secrets/azurekeyvault" // support for azurekeyvault://
	"gocloud.dev/secrets/gcpkms"          // support for gcpkms://
	_ "gocloud.dev/secrets/hashivault"    // support for hashivault://
	"google.golang.org/api/cloudkms/v1"

	"github.com/pulumi/pulumi/pkg/v3/authhelpers"
	"github.com/pulumi/pulumi/pkg/v3/secrets"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)

// Type is the type of secrets managed by this secrets provider
const Type = "cloud"

type cloudSecretsManagerState struct {
	URL          string `json:"url"`
	EncryptedKey []byte `json:"encryptedkey"`
}

// openKeeper opens the keeper, handling pulumi-specifc cases in the URL.
func openKeeper(ctx context.Context, url string) (*gosecrets.Keeper, error) {
	u, err := netUrl.Parse(url)
	if err != nil {
		return nil, fmt.Errorf("unable to parse the secrets provider URL: %w", err)
	}

	switch u.Scheme {
	case gcpkms.Scheme:
		credentials, err := authhelpers.ResolveGoogleCredentials(ctx, cloudkms.CloudkmsScope)
		if err != nil {
			return nil, fmt.Errorf("missing google credentials: %w", err)
		}

		kmsClient, _, err := gcpkms.Dial(ctx, credentials.TokenSource)
		if err != nil {
			return nil, fmt.Errorf("failed to connect to gcpkms: %w", err)
		}
		opener := gcpkms.URLOpener{
			Client: kmsClient,
		}

		return opener.OpenKeeperURL(ctx, u)
	default:
		return gosecrets.OpenKeeper(ctx, url)
	}
}

// generateNewDataKey generates a new DataKey seeded by a fresh random 32-byte key and encrypted
// using the target cloud key management service.
func generateNewDataKey(url string) ([]byte, error) {
	plaintextDataKey := make([]byte, 32)
	_, err := rand.Read(plaintextDataKey)
	if err != nil {
		return nil, err
	}
	keeper, err := openKeeper(context.Background(), url)
	if err != nil {
		return nil, err
	}
	return keeper.Encrypt(context.Background(), plaintextDataKey)
}

// newCloudSecretsManager returns a secrets manager that uses the target cloud key management
// service to encrypt/decrypt a data key used for envelope encryption of secrets values.
func newCloudSecretsManager(url string, encryptedDataKey []byte) (*Manager, error) {
	keeper, err := openKeeper(context.Background(), url)
	if err != nil {
		return nil, err
	}
	// We're emulating gocloud.dev's old behaviour here. Pre v0.28.0 it used to have an inner wrapping, which
	// we keep for compatibility (see above). However the newer version expects this to be unwrapped, before
	// it's used, so let's do that here.
	ciphertext := encryptedDataKey
	if strings.HasPrefix(url, "azurekeyvault://") {
		ciphertext, err = base64.RawURLEncoding.DecodeString(string(encryptedDataKey))
		if err != nil {
			return nil, err
		}
	}
	plaintextDataKey, err := keeper.Decrypt(context.Background(), ciphertext)
	if err != nil {
		return nil, err
	}
	state, err := json.Marshal(cloudSecretsManagerState{
		URL:          url,
		EncryptedKey: encryptedDataKey,
	})
	if err != nil {
		return nil, fmt.Errorf("marshalling state: %w", err)
	}
	crypter := config.NewSymmetricCrypter(plaintextDataKey)
	return &Manager{
		crypter: crypter,
		state:   state,
	}, nil
}

// Manager is the secrets.Manager implementation for cloud key management services
type Manager struct {
	state   json.RawMessage
	crypter config.Crypter
}

func (m *Manager) Type() string                         { return Type }
func (m *Manager) State() json.RawMessage               { return m.state }
func (m *Manager) Encrypter() (config.Encrypter, error) { return m.crypter, nil }
func (m *Manager) Decrypter() (config.Decrypter, error) { return m.crypter, nil }

func EditProjectStack(info *workspace.ProjectStack, state json.RawMessage) error {
	info.EncryptionSalt = ""

	var s cloudSecretsManagerState
	err := json.Unmarshal(state, &s)
	if err != nil {
		return fmt.Errorf("unmarshalling cloud state: %w", err)
	}

	info.SecretsProvider = s.URL
	info.EncryptedKey = base64.StdEncoding.EncodeToString(s.EncryptedKey)
	return nil
}

// NewCloudSecretsManagerFromState deserialize configuration from state and returns a secrets
// manager that uses the target cloud key management service to encrypt/decrypt a data key used for
// envelope encryption of secrets values.
func NewCloudSecretsManagerFromState(state json.RawMessage) (secrets.Manager, error) {
	var s cloudSecretsManagerState
	err := json.Unmarshal(state, &s)
	if err != nil {
		return nil, fmt.Errorf("unmarshalling state: %w", err)
	}

	// We're emulating gocloud.dev's old behaviour here.  Pre v0.28.0 it used to have an inner wrapping, which
	// we keep for compatibility (see above). However the newer version expects this to be unwrapped, before
	// it's used. newCloudSecretsManager will manage that but we need to check here as well to handle the
	// #15329 regression.
	dataKey := s.EncryptedKey
	if strings.HasPrefix(s.URL, "azurekeyvault://") {
		wrappedKey, err := base64.RawURLEncoding.DecodeString(string(dataKey))
		if err != nil {
			// https://github.com/pulumi/pulumi/issues/15329 resulted in some non-encoded keys being written
			// to state. This checks that case to see if there valid base64 data.
			firstErr := err
			_, err := base64.StdEncoding.DecodeString(string(wrappedKey))
			if err != nil {
				// Wasn't valid base64 so probably just gibberish, return the first error we saw.
				return nil, firstErr
			}
			// This is a valid base64 string so it's probably just a bad encoding, wrap it as expected and
			// pass it on to newCloudSecretsManager
			dataKey = []byte(base64.RawURLEncoding.EncodeToString(dataKey))
		}
	}

	return newCloudSecretsManager(s.URL, dataKey)
}

func NewCloudSecretsManager(info *workspace.ProjectStack,
	secretsProvider string, rotateSecretsProvider bool,
) (secrets.Manager, error) {
	// Only a passphrase provider has an encryption salt. So changing a secrets provider
	// from passphrase to a cloud secrets provider should ensure that we remove the enryptionsalt
	// as it's a legacy artifact and needs to be removed
	info.EncryptionSalt = ""

	var secretsManager *Manager

	// Allow per-execution override of the secrets provider via an environment
	// variable. This allows a temporary replacement without updating the stack
	// config, such a during CI.
	if override := os.Getenv("PULUMI_CLOUD_SECRET_OVERRIDE"); override != "" {
		secretsProvider = override
	}

	// If we're rotating then just clear the key so we create a fresh one below
	if rotateSecretsProvider {
		info.EncryptedKey = ""
	}

	// if there is no key OR the secrets provider is changing
	// then we need to generate the new key based on the new secrets provider
	if info.EncryptedKey == "" || info.SecretsProvider != secretsProvider {
		dataKey, err := generateNewDataKey(secretsProvider)
		if err != nil {
			return nil, err
		}
		// gocloud.dev versions before v0.28.0 wrapped the
		// data key in a base64.RawURLEncoding before wrapping
		// it again in base64.StdEncoding.  We keep emulating
		// this here for compatibility.
		if strings.HasPrefix(secretsProvider, "azurekeyvault://") {
			dataKey = []byte(base64.RawURLEncoding.EncodeToString(dataKey))
		}
		info.EncryptedKey = base64.StdEncoding.EncodeToString(dataKey)
	}
	info.SecretsProvider = secretsProvider

	dataKey, err := base64.StdEncoding.DecodeString(info.EncryptedKey)
	if err != nil {
		return nil, err
	}

	secretsManager, err = newCloudSecretsManager(secretsProvider, dataKey)
	if err != nil {
		return nil, err
	}

	return secretsManager, nil
}