Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simple support for end to end encryption for go #2

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.idea

23 changes: 10 additions & 13 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 20 additions & 14 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -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)
// }
//}
82 changes: 82 additions & 0 deletions decrypt.go
Original file line number Diff line number Diff line change
@@ -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
}
92 changes: 54 additions & 38 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
36 changes: 36 additions & 0 deletions sugar.go
Original file line number Diff line number Diff line change
@@ -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
}