From ac4b67d98ea3c9ddb099352dfa15ef47fcedcf7a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 19:22:09 -0500 Subject: [PATCH] delete, get and create via cli --- backend/src/routes/v2/secret.ts | 4 +- cli/packages/cmd/root.go | 2 +- cli/packages/cmd/secrets.go | 173 +++++++++++++++++++++++++++++-- cli/packages/http/api.go | 82 +++++++++++++++ cli/packages/models/api.go | 63 +++++++++++ cli/packages/models/cli.go | 7 ++ cli/packages/util/credentials.go | 53 ++++++++++ cli/packages/util/crypto.go | 13 ++- cli/packages/util/secrets.go | 2 + 9 files changed, 385 insertions(+), 14 deletions(-) create mode 100644 cli/packages/http/api.go diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 5691dda49..69b70c37e 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -24,7 +24,7 @@ router.post( }), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), - body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')), validateRequest, async (req: Request, res: Response) => { const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; @@ -94,7 +94,7 @@ router.delete( requireAuth, param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), - body('secretIds').exists().isArray(), + body('secretIds').exists().isArray().custom(array => array.length > 0), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index f09f08800..0d505e968 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -30,7 +30,7 @@ func Execute() { func init() { rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging") - rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend") + rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "http://localhost:8080/api", "Point the CLI to your own backend") // rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { // } } diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index f8efd6724..a926a4293 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -4,11 +4,17 @@ Copyright © 2022 NAME HERE package cmd import ( + "encoding/base64" "fmt" + "strings" + "crypto/sha256" + + "github.com/Infisical/infisical-merge/packages/http" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/visualize" + "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -21,14 +27,15 @@ var secretsCmd = &cobra.Command{ PreRun: toggleDebug, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - secrets, err := util.GetAllEnvironmentVariables("", "dev") secrets = util.SubstituteSecrets(secrets) if err != nil { log.Debugln(err) return } + visualize.PrintAllSecretDetails(secrets) + }, } @@ -48,9 +55,95 @@ var secretsSetCmd = &cobra.Command{ Use: "set [secrets]", DisableFlagsInUseLine: true, PreRun: toggleDebug, - Args: cobra.NoArgs, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - fmt.Println("set secret") + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + if err != nil { + log.Error(err) + return + } + + if !loggedInUserDetails.IsUserLoggedIn { + log.Error("You are not logged in yet. Please run [infisical login] then try again") + return + } + + if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired { + log.Error("Your login has expired. Please run [infisical login] then try again") + return + } + + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + request := models.GetEncryptedWorkspaceKeyRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + } + + workspaceKeyResponse, err := http.CallGetEncryptedWorkspaceKey(httpClient, request) + if err != nil { + log.Errorf("unable to get your encrypted workspace key. [err=%v]", err) + return + } + + encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.EncryptedKey) + encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Sender.PublicKey) + encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Nonce) + currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey) + + // decrypt workspace key + plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) + secretsToUpload := []models.Secret{} + for _, arg := range args { + splitKeyValueFromArg := strings.SplitN(arg, "=", 2) + if len(splitKeyValueFromArg) < 2 { + splitKeyValueFromArg[1] = "" + } + + key := splitKeyValueFromArg[0] + value := splitKeyValueFromArg[1] + + encryptedKey, err := util.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) + if err != nil { + log.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) + + encryptedValue, err := util.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) + if err != nil { + log.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) + + fullEncryptedSecret := models.Secret{ + SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), + SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), + SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), + SecretKeyHash: hashedKey, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + Type: "shared", + } + secretsToUpload = append(secretsToUpload, fullEncryptedSecret) + } + + batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + Secrets: secretsToUpload, + } + err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) + if err != nil { + log.Errorf("Unable to complete your request because %v", err) + return + } + + log.Infof("secret name(s) [%v] have been created", strings.Join(args, ", ")) }, } @@ -60,9 +153,65 @@ var secretsDeleteCmd = &cobra.Command{ Use: "delete [secrets]", DisableFlagsInUseLine: true, PreRun: toggleDebug, - Args: cobra.NoArgs, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Delete secret") + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + if err != nil { + log.Error(err) + return + } + + if !loggedInUserDetails.IsUserLoggedIn { + log.Error("You are not logged in yet. Please run [infisical login] then try again") + return + } + + if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired { + log.Error("Your login has expired. Please run [infisical login] then try again") + return + } + + secrets, err := util.GetAllEnvironmentVariables("", "dev") + if err != nil { + log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Debug(err) + } + + secretByKey := getSecretsByKeys(secrets) + validSecretIdsToDelete := []string{} + invalidSecretNamesThatDoNotExist := []string{} + + for _, secretKeyFromArg := range args { + if value, ok := secretByKey[secretKeyFromArg]; ok { + validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID) + } else { + invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg) + } + } + + if len(invalidSecretNamesThatDoNotExist) != 0 { + log.Errorf("secret name(s) [%v] does not exist in your project. Please remove and re-run the command", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) + return + } + + request := models.BatchDeleteSecretsBySecretIdsRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + SecretIds: validSecretIdsToDelete, + } + + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + err = http.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request) + if err != nil { + log.Errorf("Unable to complete your request because %v", err) + return + } + + log.Infof("secret name(s) [%v] have been deleted from your project", strings.Join(args, ", ")) + }, } @@ -93,11 +242,21 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { } else { requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ Key: secretKeyFromArg, - Type: "NOT FOUND", - Value: "NOT FOUND", + Type: "*not found*", + Value: "*not found*", }) } } visualize.PrintAllSecretDetails(requestedSecrets) } + +func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable { + secretMapByName := make(map[string]models.SingleEnvironmentVariable) + + for _, secret := range secrets { + secretMapByName[secret.Key] = secret + } + + return secretMapByName +} diff --git a/cli/packages/http/api.go b/cli/packages/http/api.go new file mode 100644 index 000000000..67f8042c2 --- /dev/null +++ b/cli/packages/http/api.go @@ -0,0 +1,82 @@ +package http + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/models" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/go-resty/resty/v2" +) + +func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchModifySecretsByWorkspaceAndEnvRequest) error { + endpoint := fmt.Sprintf("%v/v2/secret/batch-modify/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName) + response, err := httpClient. + R(). + SetBody(request). + Patch(endpoint) + + if err != nil { + return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) + } + + return nil +} + +func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchCreateSecretsByWorkspaceAndEnvRequest) error { + endpoint := fmt.Sprintf("%v/v2/secret/batch-create/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName) + response, err := httpClient. + R(). + SetBody(request). + Post(endpoint) + + if err != nil { + return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) + } + + return nil +} + +func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchDeleteSecretsBySecretIdsRequest) error { + endpoint := fmt.Sprintf("%v/v2/secret/batch/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName) + response, err := httpClient. + R(). + SetBody(request). + Delete(endpoint) + + if err != nil { + return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) + } + + return nil +} + +func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request models.GetEncryptedWorkspaceKeyRequest) (models.GetEncryptedWorkspaceKeyResponse, error) { + endpoint := fmt.Sprintf("%v/v1/key/%v/latest", util.INFISICAL_URL, request.WorkspaceId) + var result models.GetEncryptedWorkspaceKeyResponse + response, err := httpClient. + R(). + SetResult(&result). + Get(endpoint) + + if err != nil { + return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response) + } + + return result, nil +} diff --git a/cli/packages/models/api.go b/cli/packages/models/api.go index 552c7b9bc..5566cdb8b 100644 --- a/cli/packages/models/api.go +++ b/cli/packages/models/api.go @@ -128,3 +128,66 @@ type Workspace struct { V int `json:"__v"` Organization string `json:"organization,omitempty"` } + +type Secret struct { + SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"` + SecretKeyIV string `json:"secretKeyIV,omitempty"` + SecretKeyTag string `json:"secretKeyTag,omitempty"` + SecretKeyHash string `json:"secretKeyHash,omitempty"` + SecretValueCiphertext string `json:"secretValueCiphertext,omitempty"` + SecretValueIV string `json:"secretValueIV,omitempty"` + SecretValueTag string `json:"secretValueTag,omitempty"` + SecretValueHash string `json:"secretValueHash,omitempty"` + SecretCommentCiphertext string `json:"secretCommentCiphertext,omitempty"` + SecretCommentIV string `json:"secretCommentIV,omitempty"` + SecretCommentTag string `json:"secretCommentTag,omitempty"` + SecretCommentHash string `json:"secretCommentHash,omitempty"` + Type string `json:"type,omitempty"` + ID string `json:"_id,omitempty"` +} + +type BatchCreateSecretsByWorkspaceAndEnvRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` + Secrets []Secret `json:"secrets"` +} + +type BatchModifySecretsByWorkspaceAndEnvRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` + Secrets []Secret `json:"secrets"` +} + +type BatchDeleteSecretsBySecretIdsRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` + SecretIds []string `json:"secretIds"` +} + +type GetEncryptedWorkspaceKeyRequest struct { + WorkspaceId string `json:"workspaceId"` +} + +type GetEncryptedWorkspaceKeyResponse struct { + LatestKey struct { + ID string `json:"_id"` + EncryptedKey string `json:"encryptedKey"` + Nonce string `json:"nonce"` + Sender struct { + ID string `json:"_id"` + Email string `json:"email"` + RefreshVersion int `json:"refreshVersion"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + V int `json:"__v"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + PublicKey string `json:"publicKey"` + } `json:"sender"` + Receiver string `json:"receiver"` + Workspace string `json:"workspace"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"latestKey"` +} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 8ba1c4627..70484de81 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -18,8 +18,15 @@ type SingleEnvironmentVariable struct { Key string `json:"key"` Value string `json:"value"` Type string `json:"type"` + ID string `json:"_id"` } type WorkspaceConfigFile struct { WorkspaceId string `json:"workspaceId"` } + +type SymmetricEncryptionResult struct { + CipherText []byte + Nonce []byte + AuthTag []byte +} diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 80c98fa9a..bd0e67deb 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -12,6 +12,12 @@ import ( const SERVICE_NAME = "infisical" +type LoggedInUserDetails struct { + IsUserLoggedIn bool + LoginExpired bool + UserCredentials models.UserCredentials +} + // To do: what happens if the user doesn't have a keyring in their system? func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error { userCredMarshalled, err := json.Marshal(userCred) @@ -102,3 +108,50 @@ func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) { return false, "", nil } } + +func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { + if ConfigFileExists() { + configFile, err := GetConfigFile() + if err != nil { + return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to get logged in user from config file [err=%s]", err) + } + + if configFile.LoggedInUserEmail == "" { + return LoggedInUserDetails{}, nil + } + + userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail) + if err != nil { + return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to your credentials from Keyring [err=%s]", err) + } + + // check to to see if the JWT is still valid + httpClient := resty.New(). + SetAuthToken(userCreds.JTWToken). + SetHeader("Accept", "application/json") + + response, err := httpClient. + R(). + Post(fmt.Sprintf("%v/v1/auth/checkAuth", INFISICAL_URL)) + + if err != nil { + return LoggedInUserDetails{}, err + } + + if response.StatusCode() > 299 { + return LoggedInUserDetails{ + IsUserLoggedIn: true, + LoginExpired: true, + UserCredentials: userCreds, + }, nil + } + + return LoggedInUserDetails{ + IsUserLoggedIn: true, + LoginExpired: false, + UserCredentials: userCreds, + }, nil + } else { + return LoggedInUserDetails{}, nil + } +} diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go index 0431989de..23e117f4b 100644 --- a/cli/packages/util/crypto.go +++ b/cli/packages/util/crypto.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "io" + "github.com/Infisical/infisical-merge/packages/models" "golang.org/x/crypto/nacl/box" ) @@ -39,15 +40,15 @@ func GenerateNewKey() (newKey []byte, keyErr error) { } // Will encrypt a plain text with the provided key -func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byte, tag []byte, err error) { +func EncryptSymmetric(plaintext []byte, key []byte) (result models.SymmetricEncryptionResult, err error) { block, err := aes.NewCipher(key) if err != nil { - return nil, nil, nil, err + return models.SymmetricEncryptionResult{}, err } aesgcm, err := cipher.NewGCMWithNonceSize(block, 16) // default is 12, 16 because https://github.com/Infisical/infisical/blob/bea0ff6e05a4de73a5db625d4ae181a015b50855/backend/src/utils/aes-gcm.ts#L4 if err != nil { - return nil, nil, nil, err + return models.SymmetricEncryptionResult{}, err } // create a nonce @@ -62,7 +63,11 @@ func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byt authTag := ciphertext[len(ciphertext)-16:] - return ciphertextOnly, nonce, authTag, nil + return models.SymmetricEncryptionResult{ + CipherText: ciphertextOnly, + AuthTag: authTag, + Nonce: nonce, + }, nil } func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index f41e73a0f..8d9719698 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -81,6 +81,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, Key: string(plainTextKey), Value: string(plainTextValue), Type: string(secret.Type), + ID: secret.ID, } listOfEnv = append(listOfEnv, env) @@ -192,6 +193,7 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, Key: string(plainTextKey), Value: string(plainTextValue), Type: string(secret.Type), + ID: secret.ID, } listOfEnv = append(listOfEnv, env)