diff --git a/go-libs/github/github.go b/go-libs/github/github.go index 8bcc5d68..c0752183 100644 --- a/go-libs/github/github.go +++ b/go-libs/github/github.go @@ -204,6 +204,49 @@ func (c *GitHubClient) ListWorkflowJobs(ctx context.Context, org, repo string, r }) } +func (c *GitHubClient) GetRepositoryPublicKey(ctx context.Context, org, repo string) (*RepositoryPublicKey, error) { + path := fmt.Sprintf("%s/repos/%s/%s/actions/secrets/public-key", gitHubAPI, org, repo) + var res RepositoryPublicKey + err := c.api.Do(ctx, "GET", path, httpclient.WithResponseUnmarshal(&res)) + return &res, err +} + +func (c *GitHubClient) OverrideRepositorySecret(ctx context.Context, org, repo, secretName, secretText string) error { + pk, err := c.GetRepositoryPublicKey(ctx, org, repo) + if err != nil { + return fmt.Errorf("public key: %w", err) + } + encryptedValue, err := c.encryptSecretWithSodium(pk.Key, secretText) + if err != nil { + return fmt.Errorf("encrypt: %w", err) + } + path := fmt.Sprintf("%s/repos/%s/%s/actions/secrets/%s", gitHubAPI, org, repo, secretName) + return c.api.Do(ctx, "PUT", path, + httpclient.WithRequestData(OverrideRepositorySecret{ + EncryptedValue: encryptedValue, + KeyID: pk.KeyID, + })) +} + +func (c *GitHubClient) ListRepositorySecrets(ctx context.Context, org, repo string) (*RepositorySecrets, error) { + path := fmt.Sprintf("%s/repos/%s/%s/actions/secrets", gitHubAPI, org, repo) + var res RepositorySecrets + err := c.api.Do(ctx, "GET", path, httpclient.WithResponseUnmarshal(&res)) + return &res, err +} + +func (c *GitHubClient) GetRepositorySecret(ctx context.Context, org, repo, secretName string) (*RepositorySecret, error) { + path := fmt.Sprintf("%s/repos/%s/%s/actions/secrets/%s", gitHubAPI, org, repo, secretName) + var res RepositorySecret + err := c.api.Do(ctx, "GET", path, httpclient.WithResponseUnmarshal(&res)) + return &res, err +} + +func (c *GitHubClient) DeleteRepositorySecret(ctx context.Context, org, repo, secretName string) error { + path := fmt.Sprintf("%s/repos/%s/%s/actions/secrets/%s", gitHubAPI, org, repo, secretName) + return c.api.Do(ctx, "DELETE", path) +} + func (c *GitHubClient) ListCommits(ctx context.Context, org, repo string, req *ListCommits) listing.Iterator[RepositoryCommit] { path := fmt.Sprintf("%s/repos/%s/%s/commits", gitHubAPI, org, repo) return paginator[RepositoryCommit, string](ctx, c, path, req, func(rc RepositoryCommit) string { diff --git a/go-libs/github/secrets.go b/go-libs/github/secrets.go new file mode 100644 index 00000000..1507f815 --- /dev/null +++ b/go-libs/github/secrets.go @@ -0,0 +1,49 @@ +package github + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "time" + + "golang.org/x/crypto/nacl/box" +) + +type RepositorySecrets struct { + TotalCount int `json:"total_count"` + Secrets []RepositorySecret `json:"secrets"` +} + +type RepositorySecret struct { + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type RepositoryPublicKey struct { + KeyID string `json:"key_id"` + Key string `json:"key"` +} + +type OverrideRepositorySecret struct { + EncryptedValue string `json:"encrypted_value"` + KeyID string `json:"key_id"` +} + +func (c *GitHubClient) encryptSecretWithSodium(pkBase64, clearText string) (string, error) { + // adapted from implementation by Sterling Hanenkamp (c) 2022 + // See https://zostay.com/posts/2022/05/04/do-not-use-libsodium-with-go/ + var pkBytes [32]byte + _, err := base64.StdEncoding.Decode(pkBytes[:], []byte(pkBase64)) + if err != nil { + return "", fmt.Errorf("public key: %w", err) + } + raw := []byte(clearText) + out := make([]byte, 0, len(raw)+box.Overhead+len(pkBytes)) + encryptedRaw, err := box.SealAnonymous(out, raw, &pkBytes, rand.Reader) + if err != nil { + return "", err + } + encoded := base64.StdEncoding.EncodeToString(encryptedRaw) + return encoded, nil +} diff --git a/go-libs/go.mod b/go-libs/go.mod index 1c099c0c..25b9977a 100644 --- a/go-libs/go.mod +++ b/go-libs/go.mod @@ -13,6 +13,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.21.0 golang.org/x/oauth2 v0.19.0 ) @@ -48,7 +49,6 @@ require ( go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.22.0 // indirect