diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e69e07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea + diff --git a/api.go b/api.go index 3cbed23..72865d4 100644 --- a/api.go +++ b/api.go @@ -2,18 +2,13 @@ package infisicalclient import "fmt" -const USER_AGENT = "terraform" - func (client Client) CallGetServiceTokenDetailsV2() (GetServiceTokenDetailsResponse, error) { var tokenDetailsResponse GetServiceTokenDetailsResponse response, err := client.cnf.HttpClient. R(). SetResult(&tokenDetailsResponse). - SetHeader("User-Agent", USER_AGENT). Get("api/v2/service-token") - fmt.Println("response===>", response.Request) - if err != nil { return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unable to complete api request [err=%s]", err) } @@ -25,29 +20,31 @@ func (client Client) CallGetServiceTokenDetailsV2() (GetServiceTokenDetailsRespo return tokenDetailsResponse, nil } -func (client Client) CallGetSecretsV2(request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) { - var secretsResponse GetEncryptedSecretsV2Response +func (client Client) CallGetSecretsV3(request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) { + var secretsResponse GetEncryptedSecretsV3Response requestToBeMade := client.cnf.HttpClient. R(). SetResult(&secretsResponse). - SetHeader("User-Agent", USER_AGENT). + SetHeader("Authorization", "Bearer "+client.cnf.ServiceToken). SetQueryParam("environment", request.Environment). - SetQueryParam("workspaceId", request.WorkspaceId). - SetQueryParam("tagSlugs", request.TagSlugs) + SetQueryParam("workspaceId", request.WorkspaceId) if request.SecretPath != "" { requestToBeMade.SetQueryParam("secretsPath", request.SecretPath) } + if !request.IncludeImports { + requestToBeMade.SetQueryParam("include_imports", "true") + } response, err := requestToBeMade. - Get("api/v2/secrets") + Get("api/v3/secrets") if err != nil { - return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err) + return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err) } if response.IsError() { - return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%v]", response.RawResponse) + return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%v]", response.RawResponse) } return secretsResponse, nil diff --git a/client.go b/client.go index c39e413..4a4ae97 100644 --- a/client.go +++ b/client.go @@ -19,7 +19,7 @@ type Config struct { func NewClient(cnf Config) (*Client, error) { if cnf.ApiKey == "" && cnf.ServiceToken == "" { - return nil, fmt.Errorf("You must enter either a API Key or Service token for authentication with Infisical API") + return nil, fmt.Errorf("you must enter either a API Key or Service token for authentication with Infisical API") } if cnf.HttpClient == nil { diff --git a/client_test.go b/client_test.go index 6a9dd42..859e5d5 100644 --- a/client_test.go +++ b/client_test.go @@ -1,16 +1,22 @@ package infisicalclient -// func TestNewClient(t *testing.T) { - -// client, err := NewClient(Config{HostURL: "http://localhost:8080", serviceToken: ""}) -// fmt.Println(err) - -// res, err := client.CallGetSecretsV2(GetEncryptedSecretsV2Request{Environment: "dev", WorkspaceId: "63cefb15c8d3175601cfa989"}) - -// fmt.Println(err) -// fmt.Println(res) - -// // res, err := client.CallGetServiceTokenDetailsV2() -// // fmt.Println(res, err) -// // write assertions here based on your client's behavior -// } +//func TestNewClient(t *testing.T) { +// serviceToken := "xxxx" +// client, err := NewClient(Config{HostURL: "https://app.infisical.com", ServiceToken: serviceToken}) +// if err != nil { +// t.Error(err) +// } +// response, err := client.CallGetServiceTokenDetailsV2() +// +// if err != nil { +// t.Error(err) +// } +// +// println(response.Workspace) +// +// secrets, err := client.FetchAndDecodeSecrets("65006xxxxxxxxx", "dev") +// +// for k, v := range secrets { +// println(k + ":" + v) +// } +//} diff --git a/decrypt.go b/decrypt.go new file mode 100644 index 0000000..3d44a03 --- /dev/null +++ b/decrypt.go @@ -0,0 +1,82 @@ +package infisicalclient + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "fmt" + "strings" +) + +func DecryptProjectKey(serviceToken string, tokenDetail GetServiceTokenDetailsResponse) (string, error) { + lastDot := strings.LastIndex(serviceToken, ".") + if lastDot == -1 { + return "", fmt.Errorf("invalid service token") + } + serviceTokenSecret := serviceToken[lastDot+1:] + + encryptedKey := tokenDetail.EncryptedKey + iv := tokenDetail.Iv + tag := tokenDetail.Tag + + projectKey, err := decrypt(encryptedKey, iv, tag, serviceTokenSecret) + if err != nil { + return "", fmt.Errorf("error while decrypting project key: %w", err) + } + + return projectKey, nil +} + +func DecryptSecret(secret EncryptedSecret, projectKey string) (key string, value string, err error) { + keyCiphertext := secret.SecretKeyCiphertext + keyIV := secret.SecretKeyIV + keyTag := secret.SecretKeyTag + + secretKey, err := decrypt(keyCiphertext, keyIV, keyTag, projectKey) + if err != nil { + return "", "", fmt.Errorf("error while decrypting secret key: %w", err) + } + + valueCiphertext := secret.SecretValueCiphertext + valueIV := secret.SecretValueIV + valueTag := secret.SecretValueTag + secretValue, err := decrypt(valueCiphertext, valueIV, valueTag, projectKey) + + if err != nil { + return "", "", fmt.Errorf("error while decrypting secret value: %w", err) + } + + return secretKey, secretValue, nil +} + +func decrypt(ciphertext string, iv string, authTag string, key string) (string, error) { + nonceBytes, err := base64.StdEncoding.DecodeString(iv) + if err != nil { + return "", fmt.Errorf("error while decoding iv: %w", err) + } + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", fmt.Errorf("error while creating new cipher: %w", err) + } + ciphertextBytes, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("error while decoding ciphertext: %w", err) + } + tagBytes, err := base64.StdEncoding.DecodeString(authTag) + if err != nil { + return "", fmt.Errorf("error while decoding authTag: %w", err) + } + + gcm, err := cipher.NewGCMWithNonceSize(block, len(nonceBytes)) + if err != nil { + return "", fmt.Errorf("error while creating new GCM: %w", err) + } + + // We are using nonceBytes in Open function directly replacing previously used nonce variable + plainBytes, err := gcm.Open(nil, nonceBytes, append(ciphertextBytes, tagBytes...), nil) + if err != nil { + panic(err) + } + + return string(plainBytes), nil +} diff --git a/model.go b/model.go index 0c0acb1..715e8e0 100644 --- a/model.go +++ b/model.go @@ -2,52 +2,68 @@ package infisicalclient import "time" -type GetEncryptedSecretsV2Request struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - TagSlugs string `json:"tagSlugs"` - SecretPath string `json:"secretPath"` +type GetEncryptedSecretsV3Request struct { + Environment string + WorkspaceId string + SecretPath string + IncludeImports bool } - -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"` - 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"` +type GetEncryptedSecretsV3Response struct { + Secrets []EncryptedSecret `json:"secrets"` + Imports []interface{} `json:"imports"` } type GetServiceTokenDetailsResponse struct { - ID string `json:"_id"` - Name string `json:"name"` - Workspace string `json:"workspace"` - Environment string `json:"environment"` - ExpiresAt time.Time `json:"expiresAt"` + Id string `json:"_id"` + Name string `json:"name"` + Workspace string `json:"workspace"` + Scopes []struct { + Environment string `json:"environment"` + SecretPath string `json:"secretPath"` + Id string `json:"_id"` + } `json:"scopes"` + User struct { + Id string `json:"_id"` + AuthMethods []string `json:"authMethods"` + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + IsMfaEnabled bool `json:"isMfaEnabled"` + MfaMethods []interface{} `json:"mfaMethods"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + V int `json:"__v"` + } `json:"user"` + LastUsed time.Time `json:"lastUsed"` EncryptedKey string `json:"encryptedKey"` Iv string `json:"iv"` Tag string `json:"tag"` + Permissions []string `json:"permissions"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` V int `json:"__v"` } + +type EncryptedSecret struct { + Id string `json:"_id"` + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Tags []interface{} `json:"tags"` + 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"` + Algorithm string `json:"algorithm"` + KeyEncoding string `json:"keyEncoding"` + Folder string `json:"folder"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/sugar.go b/sugar.go new file mode 100644 index 0000000..a072a99 --- /dev/null +++ b/sugar.go @@ -0,0 +1,36 @@ +package infisicalclient + +/* +* +Get all secrets for a given environment and workspace, with E2EE supported +*/ +func (client Client) FetchAndDecodeSecrets(workspaceId string, environment string) (map[string]string, error) { + var secrets map[string]string + secrets = make(map[string]string) + + response, err := client.CallGetServiceTokenDetailsV2() + + if err != nil { + return nil, err + } + + projectKey, err := DecryptProjectKey(client.cnf.ServiceToken, response) + + res, err := client.CallGetSecretsV3(GetEncryptedSecretsV3Request{Environment: environment, WorkspaceId: workspaceId}) + + if err != nil { + return nil, err + } + + for _, secret := range res.Secrets { + key, value, err := DecryptSecret(secret, projectKey) + + if err != nil { + return nil, err + } + + secrets[key] = value + } + + return secrets, nil +}