diff --git a/README.md b/README.md index ac2f2a5b5..b3bcd7ac6 100644 --- a/README.md +++ b/README.md @@ -270,13 +270,13 @@ We're currently setting the foundation and building [integrations](https://infis - - ✔️ Ruby on Rails + + ✔️ Vue - - ✔️ Vue + + ✔️ Ruby on Rails @@ -292,6 +292,16 @@ We're currently setting the foundation and building [integrations](https://infis + + + + ✔️ .NET + + + + And more... + + diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index d36e32b91..bbaaff8c3 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -33,7 +33,8 @@ const secretSchema = new Schema( { version: { type: Number, - required: true + required: true, + default: 1 }, workspace: { type: Schema.Types.ObjectId, diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 17a91d39c..fb2a23f42 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,4 +1,137 @@ -import express from 'express'; +import express, { Request, Response } from 'express'; +import { requireAuth, validateRequest } from '../../middleware'; +import { ISecret, Secret } from '../../models'; +import { decryptSymmetric } from '../../utils/crypto'; +import { getLogger } from '../../utils/logger'; +import { body, param, query, check } from 'express-validator'; +import { BadRequestError } from '../../utils/errors'; const router = express.Router(); +/** + * Create a single secret for a given workspace and environment + */ +router.post( + '/', requireAuth, + body('secret').exists().isObject(), + async (req: Request, res: Response) => { + try { + const { secret }: { secret: ISecret[] } = req.body; + const newlyCreatedSecret = await Secret.create(secret) + res.status(200).json(newlyCreatedSecret) + } catch { + throw BadRequestError({ message: "Unable to create the secret" }) + } + } +); + +/** + * Create many secrets + */ +router.post( + '/bulk-create', requireAuth, + body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + async (req: Request, res: Response) => { + try { + const { secrets }: { secrets: ISecret[] } = req.body; + const newlyCreatedSecrets = await Secret.insertMany(secrets) + res.status(200).json(newlyCreatedSecrets) + } catch { + throw BadRequestError({ message: "Unable to create the secret" }) + } + } +); + +/** + * Get a single secret by secret id + */ +router.get( + '/:secretId', requireAuth, param('secretId').exists().trim(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.findById(req.params.secretId) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } + } +); + +/** + * Get a single secret by secret id + */ +router.get( + '/:bulk', requireAuth, param('secretId').exists().trim(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.findById(req.params.secretId) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } + } +); + +/** + * Delete a single secret by secret id + */ +router.delete( + '/:secretId', + requireAuth, + param('secretId').exists().trim(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.deleteOne({ + _id: req.params.secretId + }) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } + } +); + +/** + * Delete many secrets by secret ids + */ +router.delete( + '/batch', + requireAuth, + body('secretIds').exists().isArray(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretIdsToDelete: string[] = req.body.secretIds + const secretFromDB = await Secret.deleteMany({ + _id: { $in: secretIdsToDelete } + }) + return res.status(200).send(secretFromDB); + } catch (error) { + throw BadRequestError({ message: `Unable to delete the requested secrets by ids [${req.body.secretIds}]` }) + } + } +); + +/** + * Apply modifications to many existing secrets + */ +router.patch( + '/bulk-update', + requireAuth, + body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + validateRequest, async (req: Request, res: Response) => { + try { + const { secrets }: { secrets: ISecret[] } = req.body; + + const operations = secrets.map((secretToUpdate: ISecret) => ({ + updateOne: { filter: { _id: secretToUpdate._id }, update: secretToUpdate }, + })); + + const bulkModificationInfo = await Secret.bulkWrite(operations); + + return res.status(200).json(bulkModificationInfo) + } catch (error) { + throw BadRequestError({ message: `Unable to process the bulk update. Double check the ids of the secrets` }) + } + } +); + export default router; diff --git a/cli/go.mod b/cli/go.mod index c48e3e2f9..956e9bb29 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -13,6 +13,7 @@ require ( require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/Luzifer/go-openssl/v4 v4.1.0 // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/danieljoos/wincred v1.1.2 // indirect @@ -34,6 +35,7 @@ require ( ) require ( + github.com/Luzifer/go-openssl v2.0.0+incompatible github.com/go-resty/resty/v2 v2.7.0 github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible diff --git a/cli/go.sum b/cli/go.sum index d169d7b89..2b8836515 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -2,6 +2,10 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/Luzifer/go-openssl v2.0.0+incompatible h1:EpNNxrPDji4rRzE0KeOeIeV7pHyKe8zF9oNnAXy4mBY= +github.com/Luzifer/go-openssl v2.0.0+incompatible/go.mod h1:t2qnLjT8WQ3usGU1R8uAqjY4T7CK7eMg9vhQ3l9Ue/Y= +github.com/Luzifer/go-openssl/v4 v4.1.0 h1:8qi3Z6f8Aflwub/Cs4FVSmKUEg/lC8GlODbR2TyZ+nM= +github.com/Luzifer/go-openssl/v4 v4.1.0/go.mod h1:3i1T3Pe6eQK19d86WhuQzjLyMwBaNmGmt3ZceWpWVa4= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -96,15 +100,20 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -116,6 +125,7 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go new file mode 100644 index 000000000..f8efd6724 --- /dev/null +++ b/cli/packages/cmd/secrets.go @@ -0,0 +1,103 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/models" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/Infisical/infisical-merge/packages/visualize" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var secretsCmd = &cobra.Command{ + Example: `infisical secrets"`, + Short: "Used to create, read update and delete secrets", + Use: "secrets", + DisableFlagsInUseLine: true, + 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) + }, +} + +var secretsGetCmd = &cobra.Command{ + Example: `secrets get ..."`, + Short: "Used to retrieve secrets by name", + Use: "get [secrets]", + DisableFlagsInUseLine: true, + Args: cobra.MinimumNArgs(1), + PreRun: toggleDebug, + Run: getSecretsByNames, +} + +var secretsSetCmd = &cobra.Command{ + Example: `secrets set ..."`, + Short: "Used update retrieve secrets by name", + Use: "set [secrets]", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("set secret") + }, +} + +var secretsDeleteCmd = &cobra.Command{ + Example: `secrets delete ..."`, + Short: "Used to delete secrets by name", + Use: "delete [secrets]", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Delete secret") + }, +} + +func init() { + secretsCmd.AddCommand(secretsGetCmd) + secretsCmd.AddCommand(secretsSetCmd) + secretsCmd.AddCommand(secretsDeleteCmd) + rootCmd.AddCommand(secretsCmd) +} + +func getSecretsByNames(cmd *cobra.Command, args []string) { + secrets, err := util.GetAllEnvironmentVariables("", "dev") + if err != nil { + log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Debug(err) + } + + requestedSecrets := []models.SingleEnvironmentVariable{} + + secretsMap := make(map[string]models.SingleEnvironmentVariable) + for _, secret := range secrets { + secretsMap[secret.Key] = secret + } + + for _, secretKeyFromArg := range args { + if value, ok := secretsMap[secretKeyFromArg]; ok { + requestedSecrets = append(requestedSecrets, value) + } else { + requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ + Key: secretKeyFromArg, + Type: "NOT FOUND", + Value: "NOT FOUND", + }) + } + } + + visualize.PrintAllSecretDetails(requestedSecrets) +} diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go index c6eee2d0c..0431989de 100644 --- a/cli/packages/util/crypto.go +++ b/cli/packages/util/crypto.go @@ -3,21 +3,26 @@ package util import ( "crypto/aes" "crypto/cipher" + "crypto/rand" + "io" + + "golang.org/x/crypto/nacl/box" ) -func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) { +// will decrypt cipher text to plain text using iv and tag +func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } - aesgcm, err := cipher.NewGCMWithNonceSize(block, len(IV)) + aesgcm, err := cipher.NewGCMWithNonceSize(block, len(iv)) if err != nil { return nil, err } - var nonce = IV - var ciphertext = append(encryptedPrivateKey, tag...) + var nonce = iv + var ciphertext = append(cipherText, tag...) // the aesgcm open method expects auth tag at the end of the cipher text plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) if err != nil { @@ -26,3 +31,46 @@ func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []b return plaintext, nil } + +func GenerateNewKey() (newKey []byte, keyErr error) { + key := make([]byte, 16) // block size defaults to 16 so this is fine + _, err := rand.Read(key) + return key, err +} + +// Will encrypt a plain text with the provided key +func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byte, tag []byte, err error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, nil, nil, 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 + } + + // create a nonce + nonce := make([]byte, aesgcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + panic(err) + } + + ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) + + ciphertextOnly := ciphertext[:len(ciphertext)-16] // combines the auth tag with the cipher text so we need to extract it + + authTag := ciphertext[len(ciphertext)-16:] + + return ciphertextOnly, nonce, authTag, nil +} + +func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) { + plainTextToReturn, _ := box.Open(nil, ciphertext, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey)) + return plainTextToReturn +} + +func EncryptAssymmetric(message []byte, nonce []byte, publicKey []byte, privateKey []byte) (encryptedMessage []byte) { + encryptedPlainText := box.Seal(nil, message, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey)) + return encryptedPlainText +} diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index f88aed96b..f41e73a0f 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -11,7 +11,6 @@ import ( "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" - "golang.org/x/crypto/nacl/box" ) const PERSONAL_SECRET_TYPE_NAME = "personal" @@ -56,7 +55,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, } // log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey) - workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey) var listOfEnv []models.SingleEnvironmentVariable for _, secret := range pullSecretsRequestResponse.Secrets { @@ -166,7 +165,8 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, return nil, err } - workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + // workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey) var listOfEnv []models.SingleEnvironmentVariable for _, secret := range pullSecretsByInfisicalTokenResponse.Secrets { @@ -223,6 +223,7 @@ func GetAllEnvironmentVariables(projectId string, envName string) ([]models.Sing return nil, err } + // TODO: Should be based on flag. I.e only get all workspaces if desired, otherwise only get the one in the current root of project workspaceConfigs, err := GetAllWorkSpaceConfigsStartingFromCurrentPath() if err != nil { return nil, fmt.Errorf("unable to check if you have a %s file in your current directory", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go new file mode 100644 index 000000000..e732d06af --- /dev/null +++ b/cli/packages/visualize/secrets.go @@ -0,0 +1,14 @@ +package visualize + +import "github.com/Infisical/infisical-merge/packages/models" + +func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) { + rows := [][]string{} + for _, secret := range secrets { + rows = append(rows, []string{secret.Key, secret.Value, secret.Type}) + } + + headers := []string{"Secret name", "Secret vaule", "Secret type"} + + Table(headers, rows) +} diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go index 85d946a32..5658197ee 100644 --- a/cli/packages/visualize/visualize.go +++ b/cli/packages/visualize/visualize.go @@ -6,13 +6,23 @@ import ( "github.com/jedib0t/go-pretty/table" ) +type TableOptions struct { + Title string +} + +// func GetDefaultTableOptions() TableOptions{ +// return TableOptions{ +// Title: "", +// } +// } + // Given headers and rows, this function will print out a table func Table(headers []string, rows [][]string) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) - // t.SetTitle("Title") + // t.SetTitle(tableOptions.Title) t.Style().Options.DrawBorder = true t.Style().Options.SeparateHeader = true t.Style().Options.SeparateColumns = true diff --git a/docs/mint.json b/docs/mint.json index cbac56d5a..e94b70a6b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -166,7 +166,8 @@ "integrations/frameworks/django", "integrations/frameworks/flask", "integrations/frameworks/laravel", - "integrations/frameworks/rails" + "integrations/frameworks/rails", + "integrations/frameworks/dotnet" ] }, {