diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index ff39791ac..7267ae50c 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -452,7 +452,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretValueTag, tags, ...(( - secretCommentCiphertext && + secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag ) ? { diff --git a/backend/src/templates/organizationInvitation.handlebars b/backend/src/templates/organizationInvitation.handlebars index 49cc96f38..c8d0a9116 100644 --- a/backend/src/templates/organizationInvitation.handlebars +++ b/backend/src/templates/organizationInvitation.handlebars @@ -12,7 +12,6 @@

{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}

Join now

What is Infisical?

-

Infisical is a simple end-to-end encrypted solution that enables teams to sync and manage their environment - variables.

+

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

\ No newline at end of file diff --git a/backend/src/templates/workspaceInvitation.handlebars b/backend/src/templates/workspaceInvitation.handlebars index 252452ce5..501d72c8d 100644 --- a/backend/src/templates/workspaceInvitation.handlebars +++ b/backend/src/templates/workspaceInvitation.handlebars @@ -8,9 +8,9 @@

Infisical

Join your team on Infisical

-

{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical workspace — {{workspaceName}}

+

{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}

Join now

What is Infisical?

-

Infisical is a simple end-to-end encrypted solution that enables teams to sync and manage their environment variables.

+

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

\ No newline at end of file diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 698d41408..8f929bf69 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -201,21 +201,30 @@ type GetEncryptedSecretsV2Request struct { type GetEncryptedSecretsV2Response struct { Secrets []struct { - ID string `json:"_id"` - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - User string `json:"user,omitempty"` + ID string `json:"_id"` + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + User string `json:"user,omitempty"` + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } `json:"tags"` } `json:"secrets"` } diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index c5fb11c1e..a00cacaea 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -6,6 +6,8 @@ package cmd import ( "encoding/base64" "fmt" + "regexp" + "sort" "strings" "unicode" @@ -22,7 +24,7 @@ import ( ) var secretsCmd = &cobra.Command{ - Example: `infisical secrets"`, + Example: `infisical secrets`, Short: "Used to create, read update and delete secrets", Use: "secrets", DisableFlagsInUseLine: true, @@ -67,6 +69,16 @@ var secretsGetCmd = &cobra.Command{ Run: getSecretsByNames, } +var secretsGenerateExampleEnvCmd = &cobra.Command{ + Example: `secrets generate-example-env > .example-env`, + Short: "Used to generate a example .env file", + Use: "generate-example-env", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + PreRun: toggleDebug, + Run: generateExampleEnv, +} + var secretsSetCmd = &cobra.Command{ Example: `secrets set ..."`, Short: "Used set secrets", @@ -357,6 +369,171 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { visualize.PrintAllSecretDetails(requestedSecrets) } +func generateExampleEnv(cmd *cobra.Command, args []string) { + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + util.HandleError(err, "Unable to parse flag") + } + + infisicalToken, err := cmd.Flags().GetString("token") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken}) + if err != nil { + util.HandleError(err, "To fetch all secrets") + } + + tagsHashToSecretKey := make(map[string]int) + + type TagsAndSecrets struct { + Secrets []models.SingleEnvironmentVariable + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } + } + + // sort secrets by associated tags (most number of tags to least tags) + sort.Slice(secrets, func(i, j int) bool { + return len(secrets[i].Tags) > len(secrets[j].Tags) + }) + + for _, secret := range secrets { + listOfTagSlugs := []string{} + + for _, tag := range secret.Tags { + listOfTagSlugs = append(listOfTagSlugs, tag.Slug) + } + sort.Strings(listOfTagSlugs) + + tagsHash := util.GetHashFromStringList(listOfTagSlugs) + + tagsHashToSecretKey[tagsHash] += 1 + } + + finalTagHashToSecretKey := make(map[string]TagsAndSecrets) + + for _, secret := range secrets { + listOfTagSlugs := []string{} + for _, tag := range secret.Tags { + listOfTagSlugs = append(listOfTagSlugs, tag.Slug) + } + + // sort the slug so we get the same hash each time + sort.Strings(listOfTagSlugs) + + tagsHash := util.GetHashFromStringList(listOfTagSlugs) + occurrence, exists := tagsHashToSecretKey[tagsHash] + if exists && occurrence > 0 { + + value, exists2 := finalTagHashToSecretKey[tagsHash] + allSecretsForTags := append(value.Secrets, secret) + + // sort the the secrets by keys so that they can later be sorted by the first item in the secrets array + sort.Slice(allSecretsForTags, func(i, j int) bool { + return allSecretsForTags[i].Key < allSecretsForTags[j].Key + }) + + if exists2 { + finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{ + Tags: secret.Tags, + Secrets: allSecretsForTags, + } + } else { + finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{ + Tags: secret.Tags, + Secrets: []models.SingleEnvironmentVariable{secret}, + } + } + + tagsHashToSecretKey[tagsHash] -= 1 + } + } + + // sort the fianl result by secret key fo consistent print order + listOfsecretDetails := make([]TagsAndSecrets, 0, len(finalTagHashToSecretKey)) + for _, secretDetails := range finalTagHashToSecretKey { + listOfsecretDetails = append(listOfsecretDetails, secretDetails) + } + + // sort the order of the headings by the order of the secrets + sort.Slice(listOfsecretDetails, func(i, j int) bool { + return len(listOfsecretDetails[i].Tags) < len(listOfsecretDetails[j].Tags) + }) + + for _, secretDetails := range listOfsecretDetails { + listOfKeyValue := []string{} + + for _, secret := range secretDetails.Secrets { + re := regexp.MustCompile(`(?s)(.*)DEFAULT:(.*)`) + match := re.FindStringSubmatch(secret.Comment) + defaultValue := "" + comment := secret.Comment + + // Case: Only has default value + if len(match) == 2 { + defaultValue = strings.TrimSpace(match[1]) + } + + // Case: has a comment and a default value + if len(match) == 3 { + comment = match[1] + defaultValue = match[2] + } + + row := "" + if comment != "" { + comment = addHash(comment) + row = fmt.Sprintf("%s \n%s=%s", strings.TrimSpace(comment), strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue)) + } else { + row = fmt.Sprintf("%s=%s", strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue)) + } + + // each secret row to be added to the file + listOfKeyValue = append(listOfKeyValue, row) + } + + listOfTagNames := []string{} + for _, tag := range secretDetails.Tags { + listOfTagNames = append(listOfTagNames, tag.Name) + } + + heading := CenterString(strings.Join(listOfTagNames, " & "), 80) + + if len(listOfTagNames) == 0 { + fmt.Printf("\n%s \n", strings.Join(listOfKeyValue, "\n \n")) + } else { + fmt.Printf("\n\n\n%s\n \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n")) + } + } +} + +func CenterString(s string, numStars int) string { + stars := strings.Repeat("*", numStars) + padding := (numStars - len(s)) / 2 + cenetredTextWithStar := stars[:padding] + " " + strings.ToUpper(s) + " " + stars[padding:] + + hashes := strings.Repeat("#", len(cenetredTextWithStar)+2) + return fmt.Sprintf("%s \n# %s \n%s", hashes, cenetredTextWithStar, hashes) +} + +func addHash(input string) string { + lines := strings.Split(input, "\n") + for i, line := range lines { + lines[i] = "# " + line + } + return strings.Join(lines, "\n") +} + func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable { secretMapByName := make(map[string]models.SingleEnvironmentVariable) @@ -368,6 +545,10 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod } func init() { + + secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") + secretsCmd.AddCommand(secretsGenerateExampleEnvCmd) + secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") secretsCmd.AddCommand(secretsGetCmd) diff --git a/cli/packages/crypto/crypto.go b/cli/packages/crypto/crypto.go index f9589347a..2f507ed4c 100644 --- a/cli/packages/crypto/crypto.go +++ b/cli/packages/crypto/crypto.go @@ -12,6 +12,11 @@ import ( // will decrypt cipher text to plain text using iv and tag func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) { + // Case: empty string + if len(cipherText) == 0 && len(tag) == 0 && len(iv) == 0 { + return []byte{}, nil + } + block, err := aes.NewCipher(key) if err != nil { return nil, err diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 67b89b27f..3ffc1cc20 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -1,6 +1,8 @@ package models -import "github.com/99designs/keyring" +import ( + "github.com/99designs/keyring" +) type UserCredentials struct { Email string `json:"email"` @@ -19,6 +21,13 @@ type SingleEnvironmentVariable struct { Value string `json:"value"` Type string `json:"type"` ID string `json:"_id"` + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } `json:"tags"` + Comment string `json:"comment"` } type Workspace struct { diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index a6e7e35fc..3fd8853c4 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -1,6 +1,7 @@ package util import ( + "crypto/sha256" "encoding/base64" "fmt" "os" @@ -98,3 +99,14 @@ func RequireLocalWorkspaceFile() { PrintMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]") } } + +func GetHashFromStringList(list []string) string { + hash := sha256.New() + + for _, item := range list { + hash.Write([]byte(item)) + } + + sum := sha256.Sum256(hash.Sum(nil)) + return fmt.Sprintf("%x", sum) +} diff --git a/cli/packages/util/log.go b/cli/packages/util/log.go index a701987fc..b191f33c8 100644 --- a/cli/packages/util/log.go +++ b/cli/packages/util/log.go @@ -24,7 +24,7 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) { } func PrintWarning(message string) { - color.Yellow("Warning: %v", message) + color.New(color.FgYellow).Fprintf(os.Stderr, "Warning: %v \n", message) } func PrintMessageAndExit(messages ...string) { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 86f4a5e05..7a4bc09db 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -315,11 +315,34 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R return nil, fmt.Errorf("unable to symmetrically decrypt secret value") } + // Decrypt comment + comment_iv, err := base64.StdEncoding.DecodeString(secret.SecretCommentIV) + if err != nil { + return nil, fmt.Errorf("unable to decode secret IV for secret value") + } + + comment_tag, err := base64.StdEncoding.DecodeString(secret.SecretCommentTag) + if err != nil { + return nil, fmt.Errorf("unable to decode secret authentication tag for secret value") + } + + comment_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretCommentCiphertext) + if err != nil { + return nil, fmt.Errorf("unable to decode secret cipher text for secret key") + } + + plainTextComment, err := crypto.DecryptSymmetric(key, comment_ciphertext, comment_tag, comment_iv) + if err != nil { + return nil, fmt.Errorf("unable to symmetrically decrypt secret comment") + } + plainTextSecret := models.SingleEnvironmentVariable{ - Key: string(plainTextKey), - Value: string(plainTextValue), - Type: string(secret.Type), - ID: secret.ID, + Key: string(plainTextKey), + Value: string(plainTextValue), + Type: string(secret.Type), + ID: secret.ID, + Tags: secret.Tags, + Comment: string(plainTextComment), } plainTextSecrets = append(plainTextSecrets, plainTextSecret) diff --git a/frontend/src/components/basic/Layout.tsx b/frontend/src/components/basic/Layout.tsx index c6e234058..a6632b556 100644 --- a/frontend/src/components/basic/Layout.tsx +++ b/frontend/src/components/basic/Layout.tsx @@ -246,9 +246,9 @@ const Layout = ({ children }: LayoutProps) => { return ( <> -
+
-
+
-
+

{` ${t('common:no-mobile')} `} diff --git a/frontend/src/components/basic/popups/BottomRightPopup.tsx b/frontend/src/components/basic/popups/BottomRightPopup.tsx index 91427a3a8..58d8a498e 100644 --- a/frontend/src/components/basic/popups/BottomRightPopup.tsx +++ b/frontend/src/components/basic/popups/BottomRightPopup.tsx @@ -34,7 +34,7 @@ const BottonRightPopup = ({ }: PopupProps): JSX.Element => { return (

diff --git a/frontend/src/components/dashboard/AddTagsMenu.tsx b/frontend/src/components/dashboard/AddTagsMenu.tsx index 1b0790079..b0f6b1a22 100644 --- a/frontend/src/components/dashboard/AddTagsMenu.tsx +++ b/frontend/src/components/dashboard/AddTagsMenu.tsx @@ -6,10 +6,12 @@ import { Menu, Transition } from '@headlessui/react'; import { Tag } from 'public/data/frequentInterfaces'; /** - * This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options) + * This is the menu that is used to add more tags to a secret * @param {object} obj - * @param {SecretDataProps[]} obj.data - - * + * @param {Tag[]} obj.allTags - all available tags for a vertain project + * @param {Tag[]} obj.currentTags - currently selected tags for a certain secret + * @param {function} obj.modifyTags - modify tags for a certain secret + * @param {Tag[]} obj.position - currently selected tags for a certain secret */ const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], position: number) => void; position: number; }) => { const router = useRouter(); @@ -21,7 +23,7 @@ const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: >
- {currentTags.length > 2 && {currentTags.length - 2}} + {currentTags?.length > 2 && {currentTags.length - 2}}
)})} diff --git a/frontend/src/components/dashboard/CommentField.tsx b/frontend/src/components/dashboard/CommentField.tsx index bbd77cea0..c6552bb8f 100644 --- a/frontend/src/components/dashboard/CommentField.tsx +++ b/frontend/src/components/dashboard/CommentField.tsx @@ -18,7 +18,7 @@ const CommentField = ({

{t('dashboard:sidebar.comments')}