Skip to content

Commit

Permalink
Initial implementation of codespaces API (#2803)
Browse files Browse the repository at this point in the history
  • Loading branch information
artificial-aidan authored Jun 19, 2023
1 parent 4f74d0c commit 56c2c3b
Show file tree
Hide file tree
Showing 10 changed files with 3,028 additions and 2 deletions.
164 changes: 164 additions & 0 deletions example/codespaces/newreposecretwithxcrypto/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright 2023 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// newreposecretwithxcrypto creates a new secret in GitHub for a given owner/repo.
// newreposecretwithxcrypto uses x/crypto/nacl/box instead of sodium.
// It does not depend on any native libraries and is easier to cross-compile for different platforms.
// Quite possibly there is a performance penalty due to this.
//
// newreposecretwithxcrypto has two required flags for owner and repo, and takes in one argument for the name of the secret to add.
// The secret value is pulled from an environment variable based on the secret name.
// To authenticate with GitHub, provide your token via an environment variable GITHUB_AUTH_TOKEN.
//
// To verify the new secret, navigate to GitHub Repository > Settings > left side options bar > Secrets.
//
// Usage:
//
// export GITHUB_AUTH_TOKEN=<auth token from github that has secret create rights>
// export SECRET_VARIABLE=<secret value of the secret variable>
// go run main.go -owner <owner name> -repo <repository name> SECRET_VARIABLE
//
// Example:
//
// export GITHUB_AUTH_TOKEN=0000000000000000
// export SECRET_VARIABLE="my-secret"
// go run main.go -owner google -repo go-github SECRET_VARIABLE
package main

import (
"context"
crypto_rand "crypto/rand"
"encoding/base64"
"flag"
"fmt"
"log"
"os"

"github.com/google/go-github/v53/github"
"golang.org/x/crypto/nacl/box"
)

var (
repo = flag.String("repo", "", "The repo that the secret should be added to, ex. go-github")
owner = flag.String("owner", "", "The owner of there repo this should be added to, ex. google")
)

func main() {
flag.Parse()

token := os.Getenv("GITHUB_AUTH_TOKEN")
if token == "" {
log.Fatal("please provide a GitHub API token via env variable GITHUB_AUTH_TOKEN")
}

if *repo == "" {
log.Fatal("please provide required flag --repo to specify GitHub repository ")
}

if *owner == "" {
log.Fatal("please provide required flag --owner to specify GitHub user/org owner")
}

secretName, err := getSecretName()
if err != nil {
log.Fatal(err)
}

secretValue, err := getSecretValue(secretName)
if err != nil {
log.Fatal(err)
}

ctx := context.Background()
client := github.NewTokenClient(ctx, token)

if err := addRepoSecret(ctx, client, *owner, *repo, secretName, secretValue); err != nil {
log.Fatal(err)
}

fmt.Printf("Added secret %q to the repo %v/%v\n", secretName, *owner, *repo)
}

func getSecretName() (string, error) {
secretName := flag.Arg(0)
if secretName == "" {
return "", fmt.Errorf("missing argument secret name")
}
return secretName, nil
}

func getSecretValue(secretName string) (string, error) {
secretValue := os.Getenv(secretName)
if secretValue == "" {
return "", fmt.Errorf("secret value not found under env variable %q", secretName)
}
return secretValue, nil
}

// addRepoSecret will add a secret to a GitHub repo for use in GitHub Codespaces.
//
// The secretName and secretValue will determine the name of the secret added and it's corresponding value.
//
// The actual transmission of the secret value to GitHub using the api requires that the secret value is encrypted
// using the public key of the target repo. This encryption is done using x/crypto/nacl/box.
//
// First, the public key of the repo is retrieved. The public key comes base64
// encoded, so it must be decoded prior to use.
//
// Second, the decode key is converted into a fixed size byte array.
//
// Third, the secret value is converted into a slice of bytes.
//
// Fourth, the secret is encrypted with box.SealAnonymous using the repo's decoded public key.
//
// Fifth, the encrypted secret is encoded as a base64 string to be used in a github.EncodedSecret type.
//
// Sixth, The other two properties of the github.EncodedSecret type are determined. The name of the secret to be added
// (string not base64), and the KeyID of the public key used to encrypt the secret.
// This can be retrieved via the public key's GetKeyID method.
//
// Finally, the github.EncodedSecret is passed into the GitHub client.Codespaces.CreateOrUpdateRepoSecret method to
// populate the secret in the GitHub repo.
func addRepoSecret(ctx context.Context, client *github.Client, owner string, repo, secretName string, secretValue string) error {
publicKey, _, err := client.Codespaces.GetRepoPublicKey(ctx, owner, repo)
if err != nil {
return err
}

encryptedSecret, err := encryptSecretWithPublicKey(publicKey, secretName, secretValue)
if err != nil {
return err
}

if _, err := client.Codespaces.CreateOrUpdateRepoSecret(ctx, owner, repo, encryptedSecret); err != nil {
return fmt.Errorf("Codespaces.CreateOrUpdateRepoSecret returned error: %v", err)
}

return nil
}

func encryptSecretWithPublicKey(publicKey *github.PublicKey, secretName string, secretValue string) (*github.EncryptedSecret, error) {
decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey.GetKey())
if err != nil {
return nil, fmt.Errorf("base64.StdEncoding.DecodeString was unable to decode public key: %v", err)
}

var boxKey [32]byte
copy(boxKey[:], decodedPublicKey)
secretBytes := []byte(secretValue)
encryptedBytes, err := box.SealAnonymous([]byte{}, secretBytes, &boxKey, crypto_rand.Reader)
if err != nil {
return nil, fmt.Errorf("box.SealAnonymous failed with error %w", err)
}

encryptedString := base64.StdEncoding.EncodeToString(encryptedBytes)
keyID := publicKey.GetKeyID()
encryptedSecret := &github.EncryptedSecret{
Name: secretName,
KeyID: keyID,
EncryptedValue: encryptedString,
}
return encryptedSecret, nil
}
171 changes: 171 additions & 0 deletions example/codespaces/newusersecretwithxcrypto/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright 2023 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// newusersecretwithxcrypto creates a new secret in GitHub for a given user.
// newusersecretwithxcrypto uses x/crypto/nacl/box instead of sodium.
// It does not depend on any native libraries and is easier to cross-compile for different platforms.
// Quite possibly there is a performance penalty due to this.
//
// newusersecretwithxcrypto takes in one argument for the name of the secret to add, and 2 flags owner, repo.
// If owner/repo are defined then it adds the secret to that repository
// The secret value is pulled from an environment variable based on the secret name.
// To authenticate with GitHub, provide your token via an environment variable GITHUB_AUTH_TOKEN.
//
// To verify the new secret, navigate to GitHub User > Settings > left side options bar > Codespaces > Secrets.
//
// Usage:
//
// export GITHUB_AUTH_TOKEN=<auth token from github that has secret create rights>
// export SECRET_VARIABLE=<secret value of the secret variable>
// go run main.go SECRET_VARIABLE
//
// Example:
//
// export GITHUB_AUTH_TOKEN=0000000000000000
// export SECRET_VARIABLE="my-secret"
// go run main.go SECRET_VARIABLE
package main

import (
"context"
crypto_rand "crypto/rand"
"encoding/base64"
"flag"
"fmt"
"log"
"os"

"github.com/google/go-github/v53/github"
"golang.org/x/crypto/nacl/box"
)

var (
repo = flag.String("repo", "", "The repo that the secret should be added to, ex. go-github")
owner = flag.String("owner", "", "The owner of there repo this should be added to, ex. google")
)

func main() {
flag.Parse()

token := os.Getenv("GITHUB_AUTH_TOKEN")
if token == "" {
log.Fatal("please provide a GitHub API token via env variable GITHUB_AUTH_TOKEN")
}

secretName, err := getSecretName()
if err != nil {
log.Fatal(err)
}

secretValue, err := getSecretValue(secretName)
if err != nil {
log.Fatal(err)
}

ctx := context.Background()
client := github.NewTokenClient(ctx, token)

if err := addUserSecret(ctx, client, secretName, secretValue, *owner, *repo); err != nil {
log.Fatal(err)
}

fmt.Printf("Added secret %q to the authenticated user\n", secretName)
}

func getSecretName() (string, error) {
secretName := flag.Arg(0)
if secretName == "" {
return "", fmt.Errorf("missing argument secret name")
}
return secretName, nil
}

func getSecretValue(secretName string) (string, error) {
secretValue := os.Getenv(secretName)
if secretValue == "" {
return "", fmt.Errorf("secret value not found under env variable %q", secretName)
}
return secretValue, nil
}

// addUserSecret will add a secret to a GitHub user for use in GitHub Codespaces.
//
// The secretName and secretValue will determine the name of the secret added and it's corresponding value.
//
// The actual transmission of the secret value to GitHub using the api requires that the secret value is encrypted
// using the public key of the target user. This encryption is done using x/crypto/nacl/box.
//
// First, the public key of the user is retrieved. The public key comes base64
// encoded, so it must be decoded prior to use.
//
// Second, the decode key is converted into a fixed size byte array.
//
// Third, the secret value is converted into a slice of bytes.
//
// Fourth, the secret is encrypted with box.SealAnonymous using the user's decoded public key.
//
// Fifth, the encrypted secret is encoded as a base64 string to be used in a github.EncodedSecret type.
//
// Sixth, The other two properties of the github.EncodedSecret type are determined. The name of the secret to be added
// (string not base64), and the KeyID of the public key used to encrypt the secret.
// This can be retrieved via the public key's GetKeyID method.
//
// Seventh, the github.EncodedSecret is passed into the GitHub client.Codespaces.CreateOrUpdateUserSecret method to
// populate the secret in the GitHub user.
//
// Finally, if a repo and owner are passed in, it adds the repo to the user secret.
func addUserSecret(ctx context.Context, client *github.Client, secretName, secretValue, owner, repo string) error {
publicKey, _, err := client.Codespaces.GetUserPublicKey(ctx)
if err != nil {
return err
}

encryptedSecret, err := encryptSecretWithPublicKey(publicKey, secretName, secretValue)
if err != nil {
return err
}

if _, err := client.Codespaces.CreateOrUpdateUserSecret(ctx, encryptedSecret); err != nil {
return fmt.Errorf("Codespaces.CreateOrUpdateUserSecret returned error: %v", err)
}

if owner != "" && repo != "" {
r, _, err := client.Repositories.Get(ctx, owner, repo)
if err != nil {
return fmt.Errorf("Repositories.Get returned error: %v", err)
}
_, err = client.Codespaces.AddSelectedRepoToUserSecret(ctx, encryptedSecret.Name, r)
if err != nil {
return fmt.Errorf("Codespaces.AddSelectedRepoToUserSecret returned error: %v", err)
}
fmt.Printf("Added secret %q to %v/%v\n", secretName, owner, repo)
}

return nil
}

func encryptSecretWithPublicKey(publicKey *github.PublicKey, secretName string, secretValue string) (*github.EncryptedSecret, error) {
decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey.GetKey())
if err != nil {
return nil, fmt.Errorf("base64.StdEncoding.DecodeString was unable to decode public key: %v", err)
}

var boxKey [32]byte
copy(boxKey[:], decodedPublicKey)
secretBytes := []byte(secretValue)
encryptedBytes, err := box.SealAnonymous([]byte{}, secretBytes, &boxKey, crypto_rand.Reader)
if err != nil {
return nil, fmt.Errorf("box.SealAnonymous failed with error %w", err)
}

encryptedString := base64.StdEncoding.EncodeToString(encryptedBytes)
keyID := publicKey.GetKeyID()
encryptedSecret := &github.EncryptedSecret{
Name: secretName,
KeyID: keyID,
EncryptedValue: encryptedString,
}
return encryptedSecret, nil
}
4 changes: 2 additions & 2 deletions example/newreposecretwithxcrypto/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func getSecretValue(secretName string) (string, error) {

// addRepoSecret will add a secret to a GitHub repo for use in GitHub Actions.
//
// Finally, the secretName and secretValue will determine the name of the secret added and it's corresponding value.
// The secretName and secretValue will determine the name of the secret added and it's corresponding value.
//
// The actual transmission of the secret value to GitHub using the api requires that the secret value is encrypted
// using the public key of the target repo. This encryption is done using x/crypto/nacl/box.
Expand All @@ -115,7 +115,7 @@ func getSecretValue(secretName string) (string, error) {
//
// Fifth, the encrypted secret is encoded as a base64 string to be used in a github.EncodedSecret type.
//
// Sixt, The other two properties of the github.EncodedSecret type are determined. The name of the secret to be added
// Sixth, The other two properties of the github.EncodedSecret type are determined. The name of the secret to be added
// (string not base64), and the KeyID of the public key used to encrypt the secret.
// This can be retrieved via the public key's GetKeyID method.
//
Expand Down
Loading

0 comments on commit 56c2c3b

Please sign in to comment.