diff --git a/.gitignore b/.gitignore index 5941a17..c365c78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.json charts bin +.idea diff --git a/app/cli/cli.go b/app/cli/cli.go index 52f5dae..47a500a 100644 --- a/app/cli/cli.go +++ b/app/cli/cli.go @@ -5,6 +5,7 @@ import ( "github.com/dfernandezm/myiac/app/cluster" "github.com/dfernandezm/myiac/app/deploy" "github.com/dfernandezm/myiac/app/docker" + "github.com/dfernandezm/myiac/app/encryption" "github.com/dfernandezm/myiac/app/gcp" props "github.com/dfernandezm/myiac/app/properties" "github.com/dfernandezm/myiac/app/util" @@ -42,6 +43,8 @@ func BuildCli() { deployApp := deployAppSetup(projectFlag, environmentFlag, propertiesFlag) resizeClusterCmd := resizeClusterCmd(projectFlag, environmentFlag) createSecretCmd := createSecretCmd(projectFlag, environmentFlag) + cryptCmd := cryptCmd(projectFlag) + app.Commands = []cli.Command{ setupEnvironment, dockerSetup, @@ -52,6 +55,7 @@ func BuildCli() { installHelmCmd, resizeClusterCmd, createSecretCmd, + cryptCmd, } err := app.Run(os.Args) @@ -60,6 +64,63 @@ func BuildCli() { } } +func cryptCmd(projectFlag *cli.StringFlag) cli.Command { + modeFlag := &cli.StringFlag{ + Name: "mode, m", + Usage: "encrypt or decrypt", + } + + filenameWithTextFlag := &cli.StringFlag{ + Name: "filename, f", + Usage: "Location of file with plainText to encrypt or cipherText to decrypt. " + + "The CipherText will be written in a file with the " + + "same name ended with .enc, the plainText file will be written with same filename ending .dec", + } + + return cli.Command{ + Name: "crypt", + Usage: "Encrypt or decrypt file contents", + Flags: []cli.Flag{ + projectFlag, + modeFlag, + filenameWithTextFlag, + }, + Action: func(c *cli.Context) error { + fmt.Printf("Validating flags for crypt \n") + + _ = validateStringFlagPresence("project", c) + _ = validateStringFlagPresence("mode", c) + _ = validateStringFlagPresence("filename", c) + + project := c.String("project") + mode := c.String("mode") + filename := c.String("filename") + + gcp.SetupEnvironment(project) + + keyRingName := fmt.Sprintf("%s-keyring", project) + keyName := fmt.Sprintf("%s-infra-key", project) + locationId := "global" + kmsEncrypter := gcp.NewKmsEncrypter(project, locationId, keyRingName, keyName) + encrypter := encryption.NewEncrypter(kmsEncrypter) + + if mode != "encrypt" && mode != "decrypt" { + return cli.NewExitError("mode can only be 'encrypt' or 'decrypt'",-1) + } + + if mode == "encrypt" { + encrypter.EncryptFileContents(filename) + } + + if mode == "decrypt" { + encrypter.DecryptFileContents(filename) + } + + return nil + }, + } +} + func resizeClusterCmd(projectFlag *cli.StringFlag, environmentFlag *cli.StringFlag) cli.Command { nodePoolsSizeFlag := &cli.StringFlag{Name: "nodePoolsSize, bp", Usage: "Target size of all node pools"} @@ -222,7 +283,7 @@ func dockerBuildCmd(projectFlag *cli.StringFlag) cli.Command { appNameFlag := &cli.StringFlag{ Name: "app, a", Usage: "The container to build. Should match a repo name in registry " + - "and a Helm chart folder naming convention (moneycol-server, moneycol-frontend...)" + "and a Helm chart folder naming convention (moneycol-server, moneycol-frontend...)", } buildPathFlag := &cli.StringFlag{Name: "buildPath, bp", Usage: "The location of the Dockerfile"} diff --git a/app/deploy/helm_test.go b/app/deploy/helm_test.go index 362999e..f7f8915 100644 --- a/app/deploy/helm_test.go +++ b/app/deploy/helm_test.go @@ -70,6 +70,10 @@ func (mcr mockCommandRunner) Setup(executable string, args []string) { mcr.arguments = args } +func (mcr mockCommandRunner) IgnoreError(ignoreError bool) { + +} + // https://quii.gitbook.io/learn-go-with-tests/ // To run: go test -v func TestReleaseDeployed(t *testing.T) { diff --git a/app/encryption/encrypter.go b/app/encryption/encrypter.go new file mode 100644 index 0000000..24720bc --- /dev/null +++ b/app/encryption/encrypter.go @@ -0,0 +1,38 @@ +package encryption + +import "github.com/dfernandezm/myiac/app/util" + +type Encrypter interface { + Encrypt(plainText string) (string, error) + Decrypt(cipherText string) (string, error) +} + +type encryptionService struct { + encrypter Encrypter +} +// NewEncryptionService create a new encrypter +func NewEncrypter(encrypter Encrypter) *encryptionService { + enc := new(encryptionService) + enc.encrypter = encrypter + return enc +} + +// EncryptFileContents encrypts the text contained in 'filename' and returns cipherText +// into a file with the same name ended with '.enc' +func (enc encryptionService) EncryptFileContents(filename string) string { + plainText, _ := util.ReadFileToString(filename) + cipherText, _ := enc.encrypter.Encrypt(plainText) + cipherTextFilename := filename + ".enc" + _ = util.WriteStringToFile(cipherText, cipherTextFilename) + return cipherTextFilename +} + +// DecryptFileContents decrypts the ciphertext contained in 'filename' and returns the +// plaintext result into another file with same name ended with '.dec' +func (enc encryptionService) DecryptFileContents(filename string) string { + cipherText, _ := util.ReadFileToString(filename) + plainText, _ := enc.encrypter.Decrypt(cipherText) + plainTextFilename := filename + ".dec" + _ = util.WriteStringToFile(plainText, plainTextFilename) + return plainTextFilename +} diff --git a/app/gcp/dns_test.go b/app/gcp/dns_test.go index 0ff1cc7..4580a7a 100644 --- a/app/gcp/dns_test.go +++ b/app/gcp/dns_test.go @@ -1,9 +1,8 @@ package gcp import ( - //"fmt" - "testing" "github.com/stretchr/testify/assert" + "testing" ) func TestCreateGCPDNSService(t *testing.T) { diff --git a/app/gcp/kms.go b/app/gcp/kms.go new file mode 100644 index 0000000..78f96fe --- /dev/null +++ b/app/gcp/kms.go @@ -0,0 +1,322 @@ +package gcp + +import ( + "context" + "fmt" + "github.com/dfernandezm/myiac/app/util" + "google.golang.org/api/iterator" + "log" + + kms "cloud.google.com/go/kms/apiv1" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" +) + +type keyRing struct { + name string +} + +type key struct { + name string +} + +type kmsEncrypter struct { + projectId string + locationId string + defaultKeyRingName string + defaultKeyName string + keyRingHolder *keyRing + keyHolder *key + kmsClient *kms.KeyManagementClient +} + +// Creates a new encrypter using GCP KMS service +// +// See: https://cloud.google.com/kms/docs/creating-keys +// +func NewKmsEncrypter(projectId string, locationId string, defaultKeyRingName string, + defaultKeyName string) *kmsEncrypter { + kenc := new(kmsEncrypter) + kenc.projectId = projectId + kenc.locationId = locationId + kenc.defaultKeyName = defaultKeyName + kenc.defaultKeyRingName = defaultKeyRingName + return kenc +} + +func (kenc *kmsEncrypter) Encrypt(plainText string) (string, error) { + ctx := context.Background() + client, _ := kenc.createKmsClient(ctx) + + // Convert the message into bytes. Cryptographic plaintext and + // ciphertext are always byte arrays. + plaintext := []byte(plainText) + + // Get or create a keyRing + if kenc.keyRingHolder == nil { + keyRing, err := kenc.getOrCreateKeyRing() + if err != nil { + //TODO: should use stacktraces + log.Fatalf("Error creating keyRing %v" , err) + } + kenc.keyRingHolder = keyRing + } + + // Get or create a key + if kenc.keyHolder == nil { + key, err := kenc.getOrCreateCryptoKey(kenc.keyRingHolder.name, kenc.defaultKeyName) + if err != nil { + log.Fatalf("Error creating key %v" , err) + } + kenc.keyHolder = key + } + + // Build the encrypt request. + req := &kmspb.EncryptRequest{ + Name: kenc.keyHolder.name, + Plaintext: plaintext, + } + + result, err := client.Encrypt(ctx, req) + if err != nil { + log.Fatalf("failed to encrypt: %v" , err) + } + + cipherText := string(result.Ciphertext) + fmt.Printf("Encrypted ciphertext: %s\n", cipherText) + + //TODO: convert to base64? before returning? + return cipherText, nil +} + +func (kenc *kmsEncrypter) Decrypt(cipherText string) (string, error) { + ctx := context.Background() + client, _ := kenc.createKmsClient(ctx) + + // Convert the message into bytes. Cryptographic plaintext and + // ciphertext are always byte arrays. + ciphertext := []byte(cipherText) + + // Obtain the key + kenc.getEncryptionKey() + + // Build the encrypt request. + req := &kmspb.DecryptRequest{ + Name: kenc.keyHolder.name, + Ciphertext: ciphertext, + } + + result, err := client.Decrypt(ctx, req) + if err != nil { + log.Fatalf("failed to decrypt: %v" , err) + } + + plainText := string(result.Plaintext) + fmt.Printf("Encrypted plainText: %s\n", plainText) + + //TODO: convert back from base64? before returning? + return plainText, nil +} + +func (kenc *kmsEncrypter) getEncryptionKey() { + // Get or create a keyRing + if kenc.keyRingHolder == nil { + keyRing, err := kenc.getOrCreateKeyRing() + if err != nil { + //TODO: should use stacktraces + log.Fatalf("Error creating keyRing %v" , err) + } + kenc.keyRingHolder = keyRing + } + + // Get or create a key + if kenc.keyHolder == nil { + key, err := kenc.getOrCreateCryptoKey(kenc.keyRingHolder.name, kenc.defaultKeyName) + if err != nil { + log.Fatalf("Error creating key %v" , err) + } + kenc.keyHolder = key + } +} + +// createKeyRing creates a new ring to store keys on KMS. +// parent := "projects/PROJECT_ID/locations/global" +// id := "my-key-ring" +func (kenc *kmsEncrypter) createKeyRing(id string, parent string) (*keyRing, error) { + + // Create the client. + ctx := context.Background() + client, _ := kenc.createKmsClient(ctx) + + // Build the request. + req := &kmspb.CreateKeyRingRequest{ + Parent: parent, + KeyRingId: id, + } + + result, err := client.CreateKeyRing(ctx, req) + + if err != nil { + return nil, fmt.Errorf("failed to create key ring: %v", err) + } + + fmt.Printf("Created key ring: %s\n", result.Name) + fullKeyRingName := fmt.Sprintf("%s/keyRings/%s", parent, result.Name) + keyRing := &keyRing{name: fullKeyRingName} + return keyRing, nil +} + +func (kenc *kmsEncrypter) listKeyRings(parent string) ([]string, error) { + // Create the client. + ctx := context.Background() + client, _ := kenc.createKmsClient(ctx) + + listReq := &kmspb.ListKeyRingsRequest{ + Parent: parent, + } + + result := client.ListKeyRings(ctx, listReq) + var keyRings []string + for { + resp, err := result.Next() + if err == iterator.Done { + break + } + if err != nil { + log.Fatalf("Failed to list key rings: %v", err) + } + + fmt.Printf("key ring: %s\n", resp.Name) + keyRings = append(keyRings, resp.Name) + } + + return keyRings, nil +} + +func (kenc *kmsEncrypter) getOrCreateKeyRing() (*keyRing, error) { + parent := fmt.Sprintf("projects/%s/locations/%s", + kenc.projectId, + kenc.locationId) + + id := kenc.defaultKeyRingName + keyRings, _ := kenc.listKeyRings(parent) + + fmt.Printf("Check if keyRing with id %s exists\n", id) + fullKeyRingName := fmt.Sprintf("%s/keyRings/%s", parent, id) + + if util.ArrayContains(keyRings, fullKeyRingName) { + fmt.Printf("Keyring exists with id %s, using it\n", id) + existingKeyRing := &keyRing{name: fullKeyRingName} + return existingKeyRing, nil + } else { + fmt.Printf("Keyring with id %s does not exist, creating now\n", id) + newKeyRing, err := kenc.createKeyRing(id, parent) + + if err != nil { + return nil, fmt.Errorf("failed to create keyring: %v\n", err) + } + + return newKeyRing, nil + } +} + +// Creates or retrieves an existing symmetric encryption key +func (kenc *kmsEncrypter) getOrCreateCryptoKey(parentKeyRing string, keyName string) (*key, error) { + + fullKeyName := fmt.Sprintf("%s/cryptoKeys/%s", + parentKeyRing, + keyName) + keys, _ := kenc.listCryptoKeys(parentKeyRing) + + fmt.Printf("Found keys %v\n", keys) + fmt.Printf("Check if key with id %s exists\n", keyName) + + if util.ArrayContains(keys, fullKeyName) { + fmt.Printf("Keyring exists with id %s, using it\n", keyName) + existingKeyRing := &key{name: fullKeyName} + return existingKeyRing, nil + } else { + createdKeyName, err := kenc.createKeySymmetricEncryptDecrypt(parentKeyRing, keyName) + + if err != nil { + return nil, fmt.Errorf("failed to create key: %v\n", err) + } + + fmt.Printf("Created key with name %s", createdKeyName) + newKey := &key{name: createdKeyName} + return newKey, nil + } +} + +// createKeySymmetricEncryptDecrypt creates a new symmetric encrypt/decrypt key +// on Cloud KMS. +// +// parentKeyRing := "projects/my-project/locations/us-east1/keyRings/my-key-ring" +// id := "my-symmetric-encryption-key" +func (kenc *kmsEncrypter) createKeySymmetricEncryptDecrypt(parentKeyRing string, id string) (string, error) { + + // Create the client. + ctx := context.Background() + client, _ := kenc.createKmsClient(ctx) + + // Build the request. + req := &kmspb.CreateCryptoKeyRequest{ + Parent: parentKeyRing, + CryptoKeyId: id, + CryptoKey: &kmspb.CryptoKey{ + Purpose: kmspb.CryptoKey_ENCRYPT_DECRYPT, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{ + Algorithm: kmspb.CryptoKeyVersion_GOOGLE_SYMMETRIC_ENCRYPTION, + }, + }, + } + + // Call the API. + result, err := client.CreateCryptoKey(ctx, req) + if err != nil { + return "", fmt.Errorf("failed to create key: %v", err) + } + fmt.Printf("Created key: %s\n", result.Name) + return result.Name, nil +} + +func (kenc *kmsEncrypter) listCryptoKeys(parent string) ([]string, error) { + // Create the client. + ctx := context.Background() + client, err := kenc.createKmsClient(ctx) + + if err != nil { + return nil, fmt.Errorf("failed to list keys: %v", err) + } + + listReq := &kmspb.ListCryptoKeysRequest{ + Parent: parent, + } + + result := client.ListCryptoKeys(ctx, listReq) + var keys []string + for { + resp, err := result.Next() + if err == iterator.Done { + break + } + if err != nil { + log.Fatalf("Failed to list keys: %v", err) + } + + fmt.Printf("crypto key: %s\n", resp.Name) + keys = append(keys, resp.Name) + } + + return keys, nil +} + +func (kenc *kmsEncrypter) createKmsClient(ctx context.Context) (*kms.KeyManagementClient, error) { + + client, err := kms.NewKeyManagementClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create kms client: %v", err) + } + return client, nil +} + +//TODO: add a key rotation period diff --git a/app/gcp/kms_test.go b/app/gcp/kms_test.go new file mode 100644 index 0000000..e155dde --- /dev/null +++ b/app/gcp/kms_test.go @@ -0,0 +1,38 @@ +package gcp + +import ( + "fmt" + "github.com/dfernandezm/myiac/app/util" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestCreateGCPKMSService(t *testing.T) { + gcpClient := NewKmsEncrypter("moneycol","moneycol-keyring", + "moneycol-keyring", "moneycol-infra-key") + assert.Equal(t, "moneycol", gcpClient.projectId) + assert.Equal(t, "moneycol-keyring", gcpClient.defaultKeyRingName) +} + +func TestEncrypts(t *testing.T) { + homeDir, _ := os.UserHomeDir() + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", homeDir + "/moneycol_account.json") + gcpClient := NewKmsEncrypter("moneycol", "global", "moneycol-keyring", + "moneycol-infra-key") + cipherText, err := gcpClient.Encrypt("a very sensitive value") + fmt.Printf("Test ciphertext: %s\n", cipherText) + _ = util.WriteStringToFile(cipherText, "/tmp/encrypted-value-2.txt") + assert.Equal(t, nil, err) +} + +func TestDecrypts(t *testing.T) { + homeDir, _ := os.UserHomeDir() + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", homeDir + "/moneycol_account.json") + gcpClient := NewKmsEncrypter("moneycol", "global", "moneycol-keyring", + "moneycol-infra-key") + cipherToDecrypt, _ := util.ReadFileToString("/tmp/encrypted-value-2.txt") + plainText, err := gcpClient.Decrypt(cipherToDecrypt) + fmt.Printf("Test plainText: %s\n", plainText) + assert.Equal(t, nil, err) +} diff --git a/app/util/util.go b/app/util/util.go index e4eacc2..8799e8d 100644 --- a/app/util/util.go +++ b/app/util/util.go @@ -3,6 +3,7 @@ package util import ( b64 "encoding/base64" "fmt" + "io/ioutil" "log" "os" "os/exec" @@ -86,4 +87,29 @@ func WriteStringToFile(content string, filePath string) error { return nil } +func ArrayContains(array []string, value string) bool { + for _, val:= range array { + if val == value { + return true + } + } + return false +} + +func ReadFileToString(filename string) (string, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return "", fmt.Errorf("error reading file %v", err) + } + return string(bytes), nil +} + +func ReadFileToBytes(filename string) ([]byte, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading file %v", err) + } + return bytes, nil +} + diff --git a/go.mod b/go.mod index 615bcb9..d2f32cf 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/urfave/cli v1.22.4 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e // indirect google.golang.org/api v0.21.0 + google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 4486f0a..4d9a56d 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -48,28 +49,23 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -82,10 +78,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -102,13 +96,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74 h1:4cFkmztxtMslUX2SctSl+blCyXfpzhGOy9LhKAqSMA4= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e h1:3Dzrrxi54Io7Aoyb0PYLsI47K2TxkRQg+cqUn+m04do= -golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.21.0 h1:zS+Q/CJJnVlXpXQVIz+lH0ZT2lBuT2ac7XD8Y/3w6hY= google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=