├── .circleci └── config.yml ├── .github └── workflows │ └── bump-version.yml ├── .golangci.yml ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── go.mod ├── go.sum ├── internals ├── api │ ├── account.go │ ├── account_key.go │ ├── account_test.go │ ├── acl.go │ ├── audit.go │ ├── auth.go │ ├── auth_aws.go │ ├── auth_gcp.go │ ├── auth_test.go │ ├── ciphertext.go │ ├── ciphertext_test.go │ ├── credential.go │ ├── credential_test.go │ ├── dir.go │ ├── dir_test.go │ ├── docs.go │ ├── encrypted_data.go │ ├── encrypted_data_test.go │ ├── encryption_key.go │ ├── encryption_key_test.go │ ├── encryption_metadata.go │ ├── encryption_parameters.go │ ├── idp_link.go │ ├── name.go │ ├── namespace.go │ ├── org.go │ ├── path_test.go │ ├── paths.go │ ├── paths_test.go │ ├── patterns.go │ ├── patterns_test.go │ ├── permission.go │ ├── permission_test.go │ ├── repo.go │ ├── repo_test.go │ ├── revoke.go │ ├── secret.go │ ├── secret_key.go │ ├── secret_test.go │ ├── secret_version.go │ ├── secret_version_test.go │ ├── server_errors.go │ ├── service.go │ ├── service_test.go │ ├── tree.go │ ├── tree_test.go │ ├── user.go │ ├── user_test.go │ ├── uuid │ │ └── uuid.go │ └── values.go ├── assert │ └── asserts.go ├── auth │ ├── docs.go │ ├── nop.go │ ├── session.go │ ├── signature.go │ ├── signature_internal_test.go │ └── signature_test.go ├── aws │ ├── docs.go │ ├── errors.go │ ├── kms_decrypter.go │ ├── kms_decrypter_test.go │ ├── service_creator.go │ └── service_creator_test.go ├── crypto │ ├── ciphertext.go │ ├── ciphertext_test.go │ ├── docs.go │ ├── hash.go │ ├── pem.go │ ├── pem_test.go │ ├── rsa.go │ ├── rsa_test.go │ ├── salt.go │ ├── salt_test.go │ ├── scrypt.go │ ├── scrypt_test.go │ ├── symmetric.go │ └── symmetric_test.go ├── errio │ ├── errors.go │ └── errors_test.go ├── gcp │ ├── errors.go │ ├── kms_decrypter.go │ ├── kms_decrypter_test.go │ ├── service_creator.go │ └── service_creator_test.go └── oauthorizer │ ├── authorizer.go │ └── callback_handler.go ├── pkg ├── randchar │ ├── example_test.go │ ├── fakes │ │ └── generator.go │ ├── generator.go │ └── generator_test.go ├── secrethub │ ├── account.go │ ├── account_key.go │ ├── acl.go │ ├── acl_test.go │ ├── audit.go │ ├── audit_test.go │ ├── client.go │ ├── client_options.go │ ├── client_test.go │ ├── client_version.go │ ├── configdir │ │ └── dir.go │ ├── credentials.go │ ├── credentials │ │ ├── aws.go │ │ ├── bootstrap_code.go │ │ ├── creators.go │ │ ├── creators_test.go │ │ ├── credentials.go │ │ ├── encoding.go │ │ ├── encoding_test.go │ │ ├── gcp.go │ │ ├── key.go │ │ ├── pass_based_encryption.go │ │ ├── providers.go │ │ ├── readers.go │ │ ├── rsa.go │ │ ├── sessions │ │ │ ├── refresher.go │ │ │ ├── session.go │ │ │ ├── session_aws.go │ │ │ ├── session_gcp.go │ │ │ └── session_test.go │ │ └── setup_code.go │ ├── crypto.go │ ├── dir.go │ ├── example_test.go │ ├── fakeclient │ │ ├── accessrule.go │ │ ├── account.go │ │ ├── audit.go │ │ ├── client.go │ │ ├── credentials.go │ │ ├── dir.go │ │ ├── idp_link.go │ │ ├── me.go │ │ ├── org.go │ │ ├── org_member.go │ │ ├── repo.go │ │ ├── repo_service.go │ │ ├── repo_user.go │ │ ├── secret.go │ │ ├── secret_version.go │ │ ├── service.go │ │ ├── service_aws.go │ │ └── user.go │ ├── idp_link.go │ ├── internals │ │ └── http │ │ │ ├── client.go │ │ │ ├── encoding.go │ │ │ └── options.go │ ├── iterator │ │ ├── iterator.go │ │ ├── iterator_test.go │ │ └── paginator.go │ ├── main_test.go │ ├── me.go │ ├── org.go │ ├── org_member.go │ ├── org_member_test.go │ ├── org_test.go │ ├── repo.go │ ├── repo_service.go │ ├── repo_user.go │ ├── secret.go │ ├── secret_key.go │ ├── secret_version.go │ ├── service.go │ ├── user.go │ └── user_test.go └── secretpath │ ├── path.go │ └── path_test.go └── scripts └── check-version ├── check-version.sh └── main.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | lint: 4 | docker: 5 | - image: golangci/golangci-lint:v1.23.8-alpine 6 | steps: 7 | - checkout 8 | - run: golangci-lint run 9 | test: 10 | docker: 11 | - image: circleci/golang:1.13 12 | steps: 13 | - checkout 14 | - restore_cache: 15 | keys: 16 | - go-modules-{{ checksum "go.mod" }} 17 | - run: go mod download 18 | - save_cache: 19 | key: go-modules-{{ checksum "go.mod" }} 20 | paths: 21 | - /go/pkg/mod 22 | - run: make test 23 | verify-version: 24 | docker: 25 | - image: circleci/golang:1.13 26 | steps: 27 | - checkout 28 | - restore_cache: 29 | keys: 30 | - go-modules-{{ checksum "go.mod" }} 31 | - run: go mod download 32 | - save_cache: 33 | key: go-modules-{{ checksum "go.mod" }} 34 | paths: 35 | - /go/pkg/mod 36 | - run: make check-version 37 | workflows: 38 | version: 2 39 | pipeline: 40 | jobs: 41 | - lint 42 | - test 43 | - verify-version: 44 | filters: 45 | branches: 46 | only: 47 | - /release/v[0-9]*\.[0-9]*\.[0-9]*/ 48 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - release/v* 5 | 6 | jobs: 7 | bump-version: 8 | name: Bump secrethub.ClientVersion 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Bump version 14 | uses: florisvdg/action-version-bump@v0.1.0 15 | with: 16 | sed: 's/^\(const ClientVersion = "v\).*\("\)$/\1$VERSION\2/g' 17 | file: pkg/secrethub/client_version.go 18 | author_email: bender.github@secrethub.io 19 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - errcheck 4 | - goimports 5 | - golint 6 | - staticcheck 7 | - vet 8 | - unused 9 | - gosimple 10 | - stylecheck 11 | - structcheck 12 | - varcheck 13 | - interfacer 14 | - unconvert 15 | - ineffassign 16 | - deadcode 17 | - gocyclo 18 | - typecheck 19 | - depguard 20 | - unparam 21 | - nakedret 22 | - gofmt 23 | - misspell 24 | - prealloc 25 | - goconst 26 | disable-all: true 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contibuting to SecretHub 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | ### Topics 6 | 7 | - [Reporting Security Issues](#reporting-security-issues) 8 | - [Reporting Issues](#reporting-other-issues) 9 | - [Requesting Features](#requesting-features) 10 | - [Submitting Pull Requests](#submitting-pull-requests) 11 | 12 | ### Reporting Security Issues 13 | 14 | At SecretHub, we consider the security of our systems a top priority. 15 | But no matter how much effort we put into system security, there can still be vulnerabilities present. 16 | 17 | Please follow our [Responsible Disclosure Policy][disclosure-policy] when reporting security issues. 18 | 19 | ### Reporting Other Issues 20 | 21 | When submitting bug reports, please include the version of the client(s) on which you encountered the issue. 22 | 23 | ### Requesting Features 24 | 25 | SecretHub is in active development. When there is something in particular you would like us to focus on, please [create an issue][issues]. 26 | If you'd like to implement the feature yourself, please checkout [Submitting Pull Requests](#submitting-pull-requests). 27 | 28 | ### Submitting Pull Requests 29 | 30 | We are always thrilled to receive pull requests. We do our best to process them quickly. 31 | 32 | [disclosure-policy]: https://secrethub.io/security/ 33 | [issues]: https://github.com/secrethub/secrethub-go/issues/new 34 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors by first contribution 2 | 3 | - Marc Mackenbach [@mackenbach](https://github.com/mackenbach) 4 | - Steffan Norberhuis [@snorberhuis](https://github.com/snorberhuis) 5 | - Joris Coenen [@jpcoenen](https://github.com/jpcoenen) 6 | - Remco Verhoef [@nl5887](https://github.com/nl5887) 7 | - Aaron Ang [@aaronang](https://github.com/aaronang) 8 | - Marc Zwalua [@marczwalua](https://github.com/marczwalua) 9 | - Joey van Rijn [@jmsvanrijn](https://github.com/jmsvanrijn) 10 | - Simon Barendse [@simonbarendse](https://github.com/simonbarendse) 11 | - Valerio Barrila [@ninjatux](https://github.com/ninjatux) 12 | - Floris van der Grinten [@florisvdg](https://github.com/florisvdg) 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | commit: format lint test 2 | 3 | format: 4 | @goimports -w $(find . -type f -name '*.go') 5 | 6 | GOLANGCI_VERSION=v1.23.8 7 | lint: 8 | @docker run --rm -t --user $$(id -u):$$(id -g) -v $$(go env GOCACHE):/cache/go -e GOCACHE=/cache/go -e GOLANGCI_LINT_CACHE=/cache/go -v $$(go env GOPATH)/pkg:/go/pkg -v ${PWD}:/app -w /app golangci/golangci-lint:${GOLANGCI_VERSION}-alpine golangci-lint run ./... 9 | 10 | test: 11 | @go test ./... 12 | 13 | tools: format-tools lint-tools 14 | 15 | format-tools: 16 | @go get -u golang.org/x/tools/cmd/goimports 17 | 18 | check-version: 19 | ./scripts/check-version/check-version.sh 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | # Security Policy 3 | 4 | ## Reporting a Vulnerability 5 | 6 | At SecretHub, we consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present. 7 | 8 | If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our clients and our systems. Please check out the [responsible disclosure policy](https://secrethub.io/security/responsible-disclosure/). 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/secrethub/secrethub-go 2 | 3 | require ( 4 | bitbucket.org/zombiezen/cardcpx v0.0.0-20150417151802-902f68ff43ef 5 | cloud.google.com/go v0.56.0 6 | github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf 7 | github.com/aws/aws-sdk-go v1.25.49 8 | github.com/docker/docker v1.13.1 9 | github.com/docker/go-units v0.3.3 10 | github.com/go-chi/chi v4.0.1+incompatible 11 | github.com/gofrs/uuid v3.2.0+incompatible 12 | github.com/google/go-querystring v1.0.0 13 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 14 | github.com/mattn/go-shellwords v1.0.6 // indirect 15 | github.com/mitchellh/go-homedir v1.1.0 16 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 17 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 18 | google.golang.org/api v0.26.0 19 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940 20 | google.golang.org/grpc v1.28.0 21 | ) 22 | 23 | go 1.13 24 | -------------------------------------------------------------------------------- /internals/api/account.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "time" 5 | 6 | "strings" 7 | 8 | "github.com/secrethub/secrethub-go/internals/api/uuid" 9 | ) 10 | 11 | // Errors 12 | var ( 13 | ErrInvalidAccountName = errAPI.Code("invalid_account_name").Error("An account name either needs to be an username or a servicename") 14 | ErrInvalidKeyID = errAPI.Code("invalid_key_id").Error("id of the provided account key is invalid") 15 | 16 | ServiceNamePrefix = "s-" 17 | ) 18 | 19 | // Account represents an account on SecretHub. 20 | type Account struct { 21 | AccountID uuid.UUID `json:"account_id"` 22 | Name AccountName `json:"name"` 23 | PublicKey []byte `json:"public_key"` 24 | AccountType string `json:"account_type"` 25 | CreatedAt time.Time `json:"created_at"` 26 | } 27 | 28 | // AccountName represents the name of either a user or a service. 29 | type AccountName string 30 | 31 | // NewAccountName validates an account's name and returns it as a typed AccountName when valid. 32 | func NewAccountName(name string) (AccountName, error) { 33 | err := ValidateAccountName(name) 34 | if err != nil { 35 | return "", err 36 | } 37 | return AccountName(name), err 38 | } 39 | 40 | // IsService returns true if the AccountName contains the name of a service. 41 | func (n AccountName) IsService() bool { 42 | return strings.HasPrefix(strings.ToLower(string(n)), ServiceNamePrefix) 43 | } 44 | 45 | // IsUser returns true if the AccountName contains the name of a user. 46 | func (n AccountName) IsUser() bool { 47 | return !n.IsService() 48 | } 49 | 50 | // Validate checks whether an AccountName is valid. 51 | func (n AccountName) Validate() error { 52 | return ValidateAccountName(string(n)) 53 | } 54 | 55 | // Set sets the AccountName to the value. 56 | func (n *AccountName) Set(value string) error { 57 | accountName, err := NewAccountName(value) 58 | if err != nil { 59 | return err 60 | } 61 | *n = accountName 62 | return nil 63 | } 64 | 65 | // String returns the account's name as a string to be used for printing. 66 | func (n AccountName) String() string { 67 | return string(n) 68 | } 69 | 70 | // Value returns the account's name as a string to be used in communication 71 | // with the client and in transportation to the server. 72 | func (n AccountName) Value() string { 73 | return string(n) 74 | } 75 | -------------------------------------------------------------------------------- /internals/api/account_key.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Errors 8 | var ( 9 | ErrAccountNotKeyed = errAPI.Code("account_not_keyed").StatusError("User has not yet keyed their account", http.StatusBadRequest) 10 | ErrAccountKeyNotFound = errAPI.Code("account_key_not_found").StatusError("User has not yet keyed their account", http.StatusNotFound) 11 | ErrIllegalKeyVersion = errHub.Code("illegal_key_version").StatusError("key_version should be either v1 or v2", http.StatusBadRequest) 12 | ) 13 | 14 | // EncryptedAccountKey represents an account key encrypted with a credential. 15 | type EncryptedAccountKey struct { 16 | Account *Account `json:"account"` 17 | PublicKey []byte `json:"public_key"` 18 | EncryptedPrivateKey *EncryptedData `json:"encrypted_private_key"` 19 | Credential *Credential `json:"credential"` 20 | } 21 | 22 | // CreateAccountKeyRequest contains the fields to add an account_key encrypted for a credential. 23 | type CreateAccountKeyRequest struct { 24 | EncryptedPrivateKey *EncryptedData `json:"encrypted_private_key"` 25 | PublicKey []byte `json:"public_key"` 26 | } 27 | 28 | // Validate checks whether the request is valid. 29 | func (req CreateAccountKeyRequest) Validate() error { 30 | if len(req.PublicKey) == 0 { 31 | return ErrInvalidPublicKey 32 | } 33 | if req.EncryptedPrivateKey == nil { 34 | return ErrMissingField("encrypted_private_key") 35 | } 36 | if err := req.EncryptedPrivateKey.Validate(); err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /internals/api/account_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | ) 8 | 9 | func TestAccountName(t *testing.T) { 10 | 11 | tests := []struct { 12 | input string 13 | isService bool 14 | }{ 15 | { 16 | input: "user1", 17 | isService: false, 18 | }, 19 | { 20 | input: "USER1", 21 | isService: false, 22 | }, 23 | { 24 | input: "user-s-1", 25 | isService: false, 26 | }, 27 | { 28 | input: "s-service1", 29 | isService: true, 30 | }, 31 | { 32 | input: "S-SERVICE1", 33 | isService: true, 34 | }, 35 | { 36 | input: "s--service1", 37 | isService: true, 38 | }, 39 | } 40 | 41 | for _, test := range tests { 42 | an := api.AccountName(test.input) 43 | 44 | if an.IsService() != test.isService { 45 | t.Errorf("unexpected output AccountName(\"%s\").Service(): %v (actual) != %v (expected)", test.input, an.IsService(), test.isService) 46 | } 47 | if an.IsUser() != !test.isService { 48 | t.Errorf("unexpected output AccountName(\"%s\").User(): %v (actual) != %v (expected)", test.input, an.IsUser(), !test.isService) 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /internals/api/acl.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "bitbucket.org/zombiezen/cardcpx/natsort" 8 | "github.com/secrethub/secrethub-go/internals/api/uuid" 9 | ) 10 | 11 | // Errors 12 | var ( 13 | ErrInvalidSecretID = errAPI.Code("invalid_secret_id").StatusError("invalid secret id", http.StatusBadRequest) 14 | ErrInvalidDirID = errAPI.Code("invalid_dir_id").StatusError("invalid directory id", http.StatusBadRequest) 15 | ErrAccessRuleAlreadyExists = errAPI.Code("access_rule_already_exists").StatusError("access rule already exists", http.StatusConflict) 16 | ErrAccessRuleNotFound = errAPI.Code("access_rule_not_found").StatusError("access rule not found", http.StatusNotFound) 17 | ) 18 | 19 | // AccessRule defines the permission of an account on 20 | // a directory and its children. 21 | type AccessRule struct { 22 | Account *Account `json:"account"` 23 | AccountID uuid.UUID `json:"account_id"` 24 | DirID uuid.UUID `json:"dir_id"` 25 | RepoID uuid.UUID `json:"repo_id"` 26 | Permission Permission `json:"permission"` 27 | CreatedAt time.Time `json:"created_at"` 28 | LastChangedAt time.Time `json:"last_changed_at"` 29 | } 30 | 31 | // CreateAccessRuleRequest contains the request fields for creating 32 | // an AccessRule. 33 | type CreateAccessRuleRequest struct { 34 | Permission Permission `json:"permission"` 35 | EncryptedDirs []EncryptedNameForNodeRequest `json:"encrypted_dirs"` 36 | EncryptedSecrets []SecretAccessRequest `json:"encrypted_secrets"` 37 | } 38 | 39 | // Validate validates the request fields. 40 | func (car *CreateAccessRuleRequest) Validate() error { 41 | for _, encryptedDir := range car.EncryptedDirs { 42 | err := encryptedDir.Validate() 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | 48 | for _, encryptedSecret := range car.EncryptedSecrets { 49 | err := encryptedSecret.Validate() 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // AccessLevel defines the permissions of an account on a directory and is the 59 | // effect of one or more access rules on the directory itself or its parent(s). 60 | type AccessLevel struct { 61 | Account *Account `json:"account"` 62 | AccountID uuid.UUID `json:"account_id"` 63 | DirID uuid.UUID `json:"dir_id"` 64 | Permission Permission `json:"permission"` 65 | } 66 | 67 | // UpdateAccessRuleRequest contains the request fields for updating 68 | // an AccessRule. 69 | type UpdateAccessRuleRequest struct { 70 | Permission Permission `json:"permission"` 71 | } 72 | 73 | // Validate validates the request fields. 74 | func (uar *UpdateAccessRuleRequest) Validate() error { 75 | return nil 76 | } 77 | 78 | // SortAccessLevels sorts a list of AccessLevels first by the permission and then by the account name. 79 | type SortAccessLevels []*AccessLevel 80 | 81 | func (s SortAccessLevels) Len() int { 82 | return len(s) 83 | } 84 | func (s SortAccessLevels) Swap(i, j int) { 85 | s[i], s[j] = s[j], s[i] 86 | } 87 | func (s SortAccessLevels) Less(i, j int) bool { 88 | if s[i].Permission > s[j].Permission { 89 | return true 90 | } 91 | 92 | if s[i].Permission < s[j].Permission { 93 | return false 94 | } 95 | 96 | return natsort.Less(string(s[i].Account.Name), string(s[j].Account.Name)) 97 | } 98 | 99 | // SortAccessRules makes a list of AccessRules sortable. 100 | // Sort order: Permission (high to low), AccountName (natural) 101 | type SortAccessRules []*AccessRule 102 | 103 | func (s SortAccessRules) Len() int { 104 | return len(s) 105 | } 106 | 107 | func (s SortAccessRules) Swap(i, j int) { 108 | s[i], s[j] = s[j], s[i] 109 | } 110 | 111 | func (s SortAccessRules) Less(i, j int) bool { 112 | if s[i].Permission > s[j].Permission { 113 | return true 114 | } 115 | if s[i].Permission < s[j].Permission { 116 | return false 117 | } 118 | return natsort.Less(string(s[i].Account.Name), string(s[j].Account.Name)) 119 | } 120 | -------------------------------------------------------------------------------- /internals/api/audit.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/secrethub/secrethub-go/internals/api/uuid" 7 | ) 8 | 9 | // AuditAction values. 10 | const ( 11 | AuditActionUnknown AuditAction = "unknown" 12 | AuditActionCreate AuditAction = "create" 13 | AuditActionRead AuditAction = "read" 14 | AuditActionUpdate AuditAction = "update" 15 | AuditActionDelete AuditAction = "delete" 16 | ) 17 | 18 | // Audit represents an AuditEvent in SecretHub. 19 | type Audit struct { 20 | EventID uuid.UUID `json:"event_id"` 21 | Action AuditAction `json:"action"` 22 | IPAddress string `json:"ip_address"` 23 | LoggedAt time.Time `json:"logged_at"` 24 | Repo Repo `json:"repo"` 25 | Actor AuditActor `json:"actor"` 26 | Subject AuditSubject `json:"subject"` 27 | } 28 | 29 | // AuditAction represents the action that was performed to create this audit event. 30 | type AuditAction string 31 | 32 | // AuditActor represents the Account of an AuditEvent 33 | type AuditActor struct { 34 | ActorID uuid.UUID `json:"id,omitempty"` 35 | Deleted bool `json:"deleted,omitempty"` 36 | // Type is `user` or `service`. When actor is deleted, type is always `account` 37 | Type string `json:"type"` 38 | User *User `json:"user,omitempty"` 39 | Service *Service `json:"service,omitempty"` 40 | } 41 | 42 | // AuditSubjectType represents the type of an audit subject. 43 | type AuditSubjectType string 44 | 45 | // AuditSubjectTypeList represents a list of AuditSubjectTypes. 46 | type AuditSubjectTypeList []AuditSubjectType 47 | 48 | // The different options for an AuditSubjectType. 49 | const ( 50 | AuditSubjectAccount = "account" 51 | AuditSubjectUser = "user" 52 | AuditSubjectService = "service" 53 | AuditSubjectSecret = "secret" 54 | AuditSubjectSecretVersion = "secret_version" 55 | AuditSubjectSecretKey = "secret_key" 56 | AuditSubjectSecretMember = "permission" 57 | AuditSubjectRepo = "repo" 58 | AuditSubjectRepoMember = "repo_member" 59 | AuditSubjectRepoKey = "repo_key" 60 | ) 61 | 62 | // AuditSubject represents the Subject of an AuditEvent 63 | type AuditSubject struct { 64 | SubjectID uuid.UUID `json:"id,omitempty"` 65 | Deleted bool `json:"deleted,omitempty"` 66 | // Type is `user`, `service`, `repo`, `secret`, `secret_version` or `secret_key`. When subject is deleted, user and service are indicated with type `account` 67 | Type AuditSubjectType `json:"type"` 68 | User *User `json:"user,omitempty"` 69 | Service *Service `json:"service,omitempty"` 70 | Repo *Repo `json:"repo,omitempty"` 71 | EncryptedSecret *EncryptedSecret `json:"encrypted_secret,omitempty"` // This is converted to a Secret by the Client. 72 | Secret *Secret `json:"secret,omitempty"` 73 | EncryptedSecretVersion *EncryptedSecretVersion `json:"encrypted_secret_version,omitempty"` // This is converted to a SecretVersion by the Client. 74 | SecretVersion *SecretVersion `json:"secret_version,omitempty"` 75 | } 76 | 77 | // Join converts an AuditSubjectTypeList to a string where each AuditSubjectType is separated by separator. 78 | func (l AuditSubjectTypeList) Join(separator string) string { 79 | output := "" 80 | for i, t := range l { 81 | output += string(t) 82 | if i < len(l)-1 { 83 | output += separator 84 | } 85 | } 86 | return output 87 | } 88 | -------------------------------------------------------------------------------- /internals/api/auth_aws.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "net/http" 4 | 5 | // Errors 6 | var ( 7 | ErrCouldNotGetEndpoint = errAPI.Code("aws_endpoint_not_found").StatusError("could not find an AWS endpoint for the provided region", http.StatusBadRequest) 8 | ErrAWSException = errAPI.Code("aws_exception").StatusError("encountered an unexpected problem while verifying your identity on AWS. Please try again later.", http.StatusFailedDependency) 9 | ErrNoServiceWithRole = errAPI.Code("no_service_with_role").StatusErrorPref("no service account found that is linked to the IAM role '%s'", http.StatusNotFound) 10 | ErrNoAWSCredentials = errAPI.Code("missing_aws_credentials").StatusError("request was not signed with AWS credentials", http.StatusUnauthorized) 11 | ErrInvalidAWSCredentials = errAPI.Code("invalid_aws_credentials").StatusError("credentials were not accepted by AWS", http.StatusUnauthorized) 12 | ) 13 | 14 | // AuthPayloadAWSSTS is the authentication payload used for authenticating with AWS STS. 15 | type AuthPayloadAWSSTS struct { 16 | Region string `json:"region"` 17 | Request []byte `json:"request"` 18 | } 19 | 20 | // NewAuthRequestAWSSTS returns a new AuthRequest for authentication using AWS STS. 21 | func NewAuthRequestAWSSTS(sessionType SessionType, region string, stsRequest []byte) AuthRequest { 22 | return AuthRequest{ 23 | Method: AuthMethodAWSSTS, 24 | SessionType: sessionType, 25 | Payload: &AuthPayloadAWSSTS{ 26 | Region: region, 27 | Request: stsRequest, 28 | }, 29 | } 30 | } 31 | 32 | // Validate whether the AuthPayloadAWSSTS is valid. 33 | func (pl AuthPayloadAWSSTS) Validate() error { 34 | if pl.Region == "" { 35 | return ErrMissingField("region") 36 | } 37 | if pl.Request == nil { 38 | return ErrMissingField("request") 39 | } 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internals/api/auth_gcp.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "net/http" 4 | 5 | // Errors 6 | var ( 7 | ErrInvalidGCPIDToken = errAPI.Code("invalid_id_token").StatusError("provided id_token is invalid", http.StatusBadRequest) 8 | ErrNoGCPServiceWithEmail = errAPI.Code("no_service_with_email").StatusErrorPref("no service account found that is linked to the GCP Service Account %s'", http.StatusUnauthorized) 9 | ) 10 | 11 | // AuthPayloadGCPServiceAccount is the authentication payload used for authenticating with a GCP Service Account. 12 | type AuthPayloadGCPServiceAccount struct { 13 | IDToken string `json:"id_token"` 14 | } 15 | 16 | // NewAuthRequestGCPServiceAccount returns a new AuthRequest for authentication using a GCP Service Account. 17 | func NewAuthRequestGCPServiceAccount(sessionType SessionType, idToken string) AuthRequest { 18 | return AuthRequest{ 19 | Method: AuthMethodGCPServiceAccount, 20 | SessionType: sessionType, 21 | Payload: &AuthPayloadGCPServiceAccount{ 22 | IDToken: idToken, 23 | }, 24 | } 25 | } 26 | 27 | func (pl AuthPayloadGCPServiceAccount) Validate() error { 28 | if pl.IDToken == "" { 29 | return ErrMissingField("id_token") 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internals/api/auth_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "testing" 4 | 5 | func TestSession_UnmarshalJSON(t *testing.T) { 6 | // TODO 7 | } 8 | 9 | func TestAuthRequest_UnmarshalJSON(t *testing.T) { 10 | // TODO 11 | } 12 | -------------------------------------------------------------------------------- /internals/api/ciphertext.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Errors 4 | // These will be removed after the next server-release, 5 | // as they are then no longer returned from the server. 6 | var ( 7 | ErrUnknownAlgorithm = errAPI.Code("unknown_algorithm").Error("algorithm of the encoded ciphertext is invalid") 8 | ErrInvalidCiphertext = errAPI.Code("invalid_ciphertext").Error("cannot encode invalid ciphertext") 9 | ErrInvalidMetadata = errAPI.Code("invalid_metadata").Error("metadata of encrypted key is invalid") 10 | ) 11 | -------------------------------------------------------------------------------- /internals/api/ciphertext_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import "github.com/secrethub/secrethub-go/internals/crypto" 4 | 5 | var ( 6 | testCiphertextRSA = crypto.CiphertextRSA{ 7 | Data: []byte("VGh/cyBpcyBhIHRlc3Qgc3RyaW5n"), 8 | } 9 | testCiphertextAES = crypto.CiphertextAES{ 10 | Data: []byte("Lwi6p9ofYSs+FeCHkmt/aacN3A8="), 11 | Nonce: []byte("DeLt3C9ZWZ1I4P+H"), 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /internals/api/dir.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "bitbucket.org/zombiezen/cardcpx/natsort" 8 | "github.com/secrethub/secrethub-go/internals/api/uuid" 9 | "github.com/secrethub/secrethub-go/internals/crypto" 10 | ) 11 | 12 | // Errors 13 | var ( 14 | ErrInvalidDirName = errAPI.Code("invalid_dir_name").StatusError( 15 | "directory names must be between 2 and 32 characters long and "+ 16 | "may only contain letters, numbers, dashes (-), underscores (_), and dots (.)", 17 | http.StatusBadRequest, 18 | ) 19 | ErrInvalidDirBlindName = errAPI.Code("invalid_dir_blind_name").StatusErrorf("directory blind name is invalid: %s", http.StatusBadRequest, ErrInvalidBlindName) 20 | ErrInvalidParentBlindName = errAPI.Code("invalid_parent_blind_name").StatusErrorf("directory parent blind name is invalid: %s", http.StatusBadRequest, ErrInvalidBlindName) 21 | ) 22 | 23 | // EncryptedDir represents an encrypted Dir. 24 | // The names are encrypted and so are the names of SubDirs and Secrets. 25 | // The secrets contain no encrypted data, only the encrypted name. 26 | type EncryptedDir struct { 27 | DirID uuid.UUID `json:"dir_id"` 28 | BlindName string `json:"blind_name"` 29 | EncryptedName crypto.CiphertextRSA `json:"encrypted_name"` 30 | ParentID *uuid.UUID `json:"parent_id"` 31 | Status string `json:"status"` 32 | CreatedAt time.Time `json:"created_at"` 33 | LastModifiedAt time.Time `json:"last_modified_at"` 34 | } 35 | 36 | // Decrypt decrypts an EncryptedDir into a Dir. 37 | func (ed *EncryptedDir) Decrypt(accountKey *crypto.RSAPrivateKey) (*Dir, error) { 38 | name, err := accountKey.Unwrap(ed.EncryptedName) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | result := &Dir{ 44 | DirID: ed.DirID, 45 | BlindName: ed.BlindName, 46 | Name: string(name), 47 | ParentID: ed.ParentID, 48 | Status: ed.Status, 49 | CreatedAt: ed.CreatedAt, 50 | LastModifiedAt: ed.LastModifiedAt, 51 | } 52 | 53 | return result, nil 54 | } 55 | 56 | // Dir represents an directory. 57 | // A dir belongs to a repo and contains other dirs and secrets. 58 | type Dir struct { 59 | DirID uuid.UUID `json:"dir_id"` 60 | BlindName string `json:"blind_name"` 61 | Name string `json:"name"` 62 | ParentID *uuid.UUID `json:"parent_id"` 63 | Status string `json:"status"` 64 | CreatedAt time.Time `json:"created_at"` 65 | LastModifiedAt time.Time `json:"last_modified_at"` 66 | SubDirs []*Dir `json:"sub_dirs"` 67 | Secrets []*Secret `json:"secrets"` 68 | } 69 | 70 | // CreateDirRequest contains the request fields for creating a new directory. 71 | type CreateDirRequest struct { 72 | BlindName string `json:"blind_name"` 73 | ParentBlindName string `json:"parent_blind_name"` 74 | 75 | EncryptedNames []EncryptedNameRequest `json:"encrypted_names"` 76 | } 77 | 78 | // Validate validates the CreateDirRequest to be valid. 79 | func (cdr *CreateDirRequest) Validate() error { 80 | err := ValidateBlindName(cdr.BlindName) 81 | if err != nil { 82 | return ErrInvalidDirBlindName 83 | } 84 | 85 | err = ValidateBlindName(cdr.ParentBlindName) 86 | if err != nil { 87 | return ErrInvalidParentBlindName 88 | } 89 | 90 | if len(cdr.EncryptedNames) < 1 { 91 | return ErrNotEncryptedForAccounts 92 | } 93 | 94 | unique := make(map[uuid.UUID]int) 95 | for _, encryptedName := range cdr.EncryptedNames { 96 | err := encryptedName.Validate() 97 | if err != nil { 98 | return err 99 | } 100 | 101 | unique[encryptedName.AccountID]++ 102 | } 103 | 104 | for _, count := range unique { 105 | if count != 1 { 106 | return ErrNotUniquelyEncryptedForAccounts 107 | } 108 | } 109 | 110 | return nil 111 | } 112 | 113 | // SortDirByName makes a list of Dir sortable. 114 | type SortDirByName []*Dir 115 | 116 | func (d SortDirByName) Len() int { 117 | return len(d) 118 | } 119 | func (d SortDirByName) Swap(i, j int) { 120 | d[i], d[j] = d[j], d[i] 121 | } 122 | func (d SortDirByName) Less(i, j int) bool { 123 | return natsort.Less(d[i].Name, d[j].Name) 124 | } 125 | -------------------------------------------------------------------------------- /internals/api/dir_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/internals/api/uuid" 8 | "github.com/secrethub/secrethub-go/internals/assert" 9 | "github.com/secrethub/secrethub-go/internals/crypto" 10 | ) 11 | 12 | func TestCreateDirRequest_Validate(t *testing.T) { 13 | blindKey, err := crypto.GenerateSymmetricKey() 14 | assert.OK(t, err) 15 | 16 | dirPath := api.DirPath("owner/repo/dir") 17 | parentPath := api.DirPath("owner/repo/parent/dir") 18 | 19 | dirPathBlindName, err := dirPath.BlindName(blindKey) 20 | assert.OK(t, err) 21 | parentPathBlindName, err := parentPath.BlindName(blindKey) 22 | assert.OK(t, err) 23 | 24 | tests := []struct { 25 | createDirRequest *api.CreateDirRequest 26 | expected error 27 | }{ 28 | { 29 | createDirRequest: getTestCreateDirRequest(t), 30 | expected: nil, 31 | }, 32 | { 33 | createDirRequest: &api.CreateDirRequest{ 34 | ParentBlindName: parentPathBlindName, 35 | 36 | EncryptedNames: []api.EncryptedNameRequest{{ 37 | AccountID: uuid.New(), 38 | EncryptedName: testCiphertextRSA, 39 | }, 40 | }, 41 | }, 42 | expected: api.ErrInvalidDirBlindName, 43 | }, 44 | { 45 | createDirRequest: &api.CreateDirRequest{ 46 | BlindName: dirPathBlindName, 47 | EncryptedNames: []api.EncryptedNameRequest{{ 48 | AccountID: uuid.New(), 49 | EncryptedName: testCiphertextRSA, 50 | }, 51 | }, 52 | }, 53 | expected: api.ErrInvalidParentBlindName, 54 | }, 55 | } 56 | 57 | for _, test := range tests { 58 | result := test.createDirRequest.Validate() 59 | assert.Equal(t, result, test.expected) 60 | } 61 | 62 | } 63 | 64 | func TestCreateDirRequest_Validate_UniqueEncryptedFor(t *testing.T) { 65 | accountID := uuid.New() 66 | blindKey, err := crypto.GenerateSymmetricKey() 67 | assert.OK(t, err) 68 | 69 | dirPath := api.DirPath("owner/repo/dir") 70 | parentPath := api.DirPath("owner/repo/parent/dir") 71 | 72 | dirPathBlindName, err := dirPath.BlindName(blindKey) 73 | assert.OK(t, err) 74 | parentPathBlindName, err := parentPath.BlindName(blindKey) 75 | assert.OK(t, err) 76 | 77 | cdr := api.CreateDirRequest{ 78 | BlindName: dirPathBlindName, 79 | ParentBlindName: parentPathBlindName, 80 | 81 | EncryptedNames: []api.EncryptedNameRequest{ 82 | { 83 | AccountID: accountID, 84 | EncryptedName: testCiphertextRSA, 85 | }, 86 | { 87 | AccountID: accountID, 88 | EncryptedName: testCiphertextRSA, 89 | }, 90 | }, 91 | } 92 | 93 | result := cdr.Validate() 94 | assert.Equal(t, result, api.ErrNotUniquelyEncryptedForAccounts) 95 | } 96 | 97 | func getTestCreateDirRequest(t *testing.T) *api.CreateDirRequest { 98 | blindKey, err := crypto.GenerateSymmetricKey() 99 | assert.OK(t, err) 100 | 101 | dirPath := api.DirPath("owner/repo/dir") 102 | parentPath := api.DirPath("owner/repo/parent/dir") 103 | 104 | dirPathBlindName, err := dirPath.BlindName(blindKey) 105 | assert.OK(t, err) 106 | parentPathBlindName, err := parentPath.BlindName(blindKey) 107 | assert.OK(t, err) 108 | 109 | return &api.CreateDirRequest{ 110 | BlindName: dirPathBlindName, 111 | ParentBlindName: parentPathBlindName, 112 | 113 | EncryptedNames: []api.EncryptedNameRequest{{ 114 | AccountID: uuid.New(), 115 | EncryptedName: testCiphertextRSA, 116 | }, 117 | }, 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /internals/api/docs.go: -------------------------------------------------------------------------------- 1 | // Package api provides request and response types for 2 | // interacting with the SecretHub API. 3 | package api 4 | -------------------------------------------------------------------------------- /internals/api/encrypted_data_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/secrethub/secrethub-go/internals/api/uuid" 8 | 9 | "github.com/secrethub/secrethub-go/internals/assert" 10 | ) 11 | 12 | func TestEncryptedData_MarshalUnmarshalValidate(t *testing.T) { 13 | encryptedDataRSAAccountKey := NewEncryptedDataRSAOAEP([]byte("rsa-ciphertext"), HashingAlgorithmSHA256, NewEncryptionKeyAccountKey(4096, uuid.New())) 14 | 15 | cases := map[string]struct { 16 | in *EncryptedData 17 | expectedErr error 18 | validateErr error 19 | }{ 20 | "aes with rsa account key": { 21 | in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, NewEncryptionKeyEncrypted(256, encryptedDataRSAAccountKey)), 22 | }, 23 | "aes with rsa local key": { 24 | in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, NewEncryptionKeyLocal(256)), 25 | }, 26 | "aes with secret key": { 27 | in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, NewEncryptionKeySecretKey(256, uuid.New())), 28 | }, 29 | "rsa account key": { 30 | in: encryptedDataRSAAccountKey, 31 | }, 32 | "aws kms": { 33 | in: NewEncryptedDataAWSKMS([]byte("ciphertext"), NewEncryptionKeyAWS("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab")), 34 | }, 35 | "gcp kms": { 36 | in: NewEncryptedDataGCPKMS([]byte("ciphertext"), NewEncryptionKeyGCP("projects/secrethub-test-1234567890.iam/locations/global/keyRings/test/cryptoKeys/test")), 37 | }, 38 | "aes with scrypt": { 39 | in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, NewEncryptionKeyDerivedScrypt(256, 1, 2, 3, []byte("just-a-salt"))), 40 | }, 41 | "aes with bootstrap code": { 42 | in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, NewEncryptionKeyBootstrapCode(256)), 43 | }, 44 | "rsa with missing key": { 45 | in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, nil), 46 | expectedErr: ErrInvalidKeyType, 47 | }, 48 | "rsa with empty local key": { 49 | in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, &EncryptionKey{KeyTypeLocal}), 50 | validateErr: ErrMissingField("length"), 51 | }, 52 | "empty encrypted data": { 53 | in: &EncryptedData{Key: &EncryptionKey{KeyTypeLocal}}, 54 | expectedErr: ErrInvalidEncryptionAlgorithm, 55 | }, 56 | } 57 | 58 | for name, tc := range cases { 59 | t.Run(name, func(t *testing.T) { 60 | bytes, err := json.Marshal(tc.in) 61 | assert.OK(t, err) 62 | 63 | var res EncryptedData 64 | err = json.Unmarshal(bytes, &res) 65 | 66 | assert.Equal(t, err, tc.expectedErr) 67 | if tc.expectedErr == nil { 68 | assert.Equal(t, res.Validate(), tc.validateErr) 69 | if tc.validateErr == nil { 70 | assert.Equal(t, res, tc.in) 71 | } 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internals/api/encryption_key_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/secrethub/secrethub-go/internals/assert" 10 | ) 11 | 12 | func TestEncryptionKeyDerived_UnmarshalJSON(t *testing.T) { 13 | salt := []byte(strings.Repeat("1", 96)) 14 | parameters := KeyDerivationParametersScrypt{ 15 | P: 1, 16 | N: 1, 17 | R: 1, 18 | } 19 | metadata := KeyDerivationMetadataScrypt{ 20 | Salt: salt, 21 | } 22 | 23 | cases := map[string]struct { 24 | in *EncryptionKeyDerived 25 | expectedErr error 26 | validateErr error 27 | }{ 28 | "success": { 29 | in: NewEncryptionKeyDerivedScrypt(128, 1, 1, 1, salt), 30 | }, 31 | "missing-parameters": { 32 | in: &EncryptionKeyDerived{ 33 | EncryptionKey: EncryptionKey{ 34 | Type: KeyTypeDerived, 35 | }, 36 | Length: 128, 37 | Algorithm: KeyDerivationAlgorithmScrypt, 38 | Parameters: nil, 39 | Metadata: metadata, 40 | }, 41 | validateErr: ErrMissingField("parameters"), 42 | }, 43 | "missing-metadata": { 44 | in: &EncryptionKeyDerived{ 45 | EncryptionKey: EncryptionKey{ 46 | Type: KeyTypeDerived, 47 | }, 48 | Length: 128, 49 | Algorithm: KeyDerivationAlgorithmScrypt, 50 | Parameters: parameters, 51 | Metadata: nil, 52 | }, 53 | validateErr: ErrMissingField("metadata"), 54 | }, 55 | "invalid-algorithm": { 56 | in: &EncryptionKeyDerived{ 57 | EncryptionKey: EncryptionKey{ 58 | Type: KeyTypeDerived, 59 | }, 60 | Length: 128, 61 | Algorithm: KeyDerivationAlgorithm("invalid"), 62 | Parameters: parameters, 63 | Metadata: metadata, 64 | }, 65 | expectedErr: ErrInvalidKeyDerivationAlgorithm, 66 | }, 67 | } 68 | 69 | for name, tc := range cases { 70 | t.Run(name, func(t *testing.T) { 71 | bytes, err := json.Marshal(tc.in) 72 | assert.OK(t, err) 73 | 74 | fmt.Println(string(bytes)) 75 | 76 | var res EncryptionKeyDerived 77 | err = json.Unmarshal(bytes, &res) 78 | 79 | assert.Equal(t, err, tc.expectedErr) 80 | if tc.expectedErr == nil { 81 | assert.Equal(t, res.Validate(), tc.validateErr) 82 | if tc.validateErr == nil { 83 | assert.Equal(t, res, tc.in) 84 | } 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internals/api/encryption_metadata.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // EncryptionMetadataAESGCM is the metadata used by the AES-GCM encryption algorithm. 4 | type EncryptionMetadataAESGCM struct { 5 | Nonce []byte `json:"nonce"` 6 | } 7 | 8 | // Validate checks whether the EncryptionMetadataAESGCM is valid. 9 | func (m EncryptionMetadataAESGCM) Validate() error { 10 | if m.Nonce == nil { 11 | return ErrMissingField("nonce") 12 | } 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /internals/api/encryption_parameters.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Errors 4 | var ( 5 | ErrInvalidNonceLength = errAPI.Code("invalid_nonce_length").Error("invalid nonce length provided") 6 | ErrInvalidHashingAlgorithm = errAPI.Code("invalid_hashing_algorithm").Error("invalid hashing algorithm provided") 7 | ) 8 | 9 | // EncryptionParametersAESGCM are the parameters used by the AES-GCM encryption algorithm. 10 | type EncryptionParametersAESGCM struct { 11 | NonceLength int `json:"nonce_length"` 12 | } 13 | 14 | // Validate checks whether the EncryptionParametersAESGCM is valid. 15 | func (p EncryptionParametersAESGCM) Validate() error { 16 | if p.NonceLength == 0 { 17 | return ErrMissingField("nonce_length") 18 | } 19 | if p.NonceLength < 96 { 20 | return ErrInvalidNonceLength 21 | } 22 | return nil 23 | } 24 | 25 | // EncryptionParametersRSAOAEP are the parameters used by the RSA-OAEP encryption algorithm. 26 | type EncryptionParametersRSAOAEP struct { 27 | HashingAlgorithm HashingAlgorithm `json:"hashing_algorithm"` 28 | } 29 | 30 | // Validate checks whether the EncryptionParametersRSAOAEP is valid. 31 | func (p EncryptionParametersRSAOAEP) Validate() error { 32 | if p.HashingAlgorithm == "" { 33 | return ErrMissingField("hashing_algorithm") 34 | } 35 | if p.HashingAlgorithm != HashingAlgorithmSHA256 { 36 | return ErrInvalidHashingAlgorithm 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internals/api/idp_link.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "regexp" 7 | "time" 8 | ) 9 | 10 | var ( 11 | ErrInvalidIDPLinkType = errAPI.Code("invalid_idp_link_type").StatusError("invalid IDP link type", http.StatusBadRequest) 12 | ErrInvalidGCPProjectID = errAPI.Code("invalid_gcp_project_id").StatusErrorPref("invalid GCP project ID: %s", http.StatusBadRequest) 13 | ErrVerifyingGCPAccessProof = errAPI.Code("gcp_verification_error").StatusError("could not verify GCP authorization", http.StatusInternalServerError) 14 | ErrInvalidGCPAuthorizationCode = errAPI.Code("invalid_authorization_code").StatusError("authorization code was not accepted by GCP", http.StatusPreconditionFailed) 15 | ErrGCPLinkPermissionDenied = errAPI.Code("gcp_permission_denied").StatusError("missing required projects.get permission to create link to GCP project", http.StatusPreconditionFailed) 16 | 17 | gcpProjectIDPattern = regexp.MustCompile("^[a-z][a-z0-9-]*[a-z0-9]$") 18 | ) 19 | 20 | type CreateIdentityProviderLinkGCPRequest struct { 21 | RedirectURL string `json:"redirect_url"` 22 | AuthorizationCode string `json:"authorization_code"` 23 | } 24 | 25 | type IdentityProviderLinkType string 26 | 27 | const ( 28 | IdentityProviderLinkGCP IdentityProviderLinkType = "gcp" 29 | ) 30 | 31 | // IdentityProviderLink is a prerequisite for creating some identity provider backed service accounts. 32 | // These links prove that a namespace's member has access to a resource (identified by the LinkedID) within 33 | // the identity provider. Once a link between a namespace and an identity provider has been created, from then on 34 | // service accounts can be created within the scope described by the LinkedID. For example, after creating a link 35 | // to a GCP Project, GCP service accounts within that project can be used for the GCP Identity Provider. 36 | // 37 | // The meaning of LinkedID depends on the type of the IdentityProviderLink in the following way: 38 | // - GCP: LinkedID is a GCP Project ID. 39 | type IdentityProviderLink struct { 40 | Type IdentityProviderLinkType `json:"type"` 41 | Namespace string `json:"namespace"` 42 | LinkedID string `json:"linked_id"` 43 | CreatedAt time.Time `json:"created_at"` 44 | } 45 | 46 | type OAuthConfig struct { 47 | ClientID string `json:"client_id"` 48 | AuthURI string `json:"auth_uri"` 49 | Scopes []string `json:"scopes"` 50 | ResultURL *url.URL `json:"result_url"` 51 | } 52 | 53 | // ValidateLinkedID calls the validation function corresponding to the link type and returns the corresponding result. 54 | func ValidateLinkedID(linkType IdentityProviderLinkType, linkedID string) error { 55 | switch linkType { 56 | case IdentityProviderLinkGCP: 57 | return ValidateGCPProjectID(linkedID) 58 | default: 59 | return ErrInvalidIDPLinkType 60 | } 61 | } 62 | 63 | // ValidateGCPProjectID returns an error if the provided value is not a valid GCP project ID. 64 | func ValidateGCPProjectID(projectID string) error { 65 | if len(projectID) < 6 || len(projectID) > 30 { 66 | return ErrInvalidGCPProjectID("length must be 6 to 30 character") 67 | } 68 | if !gcpProjectIDPattern.MatchString(projectID) { 69 | return ErrInvalidGCPProjectID("can only contains lowercase letter, digits and hyphens") 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /internals/api/name.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api/uuid" 5 | "github.com/secrethub/secrethub-go/internals/crypto" 6 | ) 7 | 8 | // EncryptedNameRequest contains an EncryptedName for an Account. 9 | type EncryptedNameRequest struct { 10 | AccountID uuid.UUID `json:"account_id"` 11 | EncryptedName crypto.CiphertextRSA `json:"encrypted_name"` 12 | } 13 | 14 | // Validate validates the EncryptedNameRequest to be valid. 15 | func (enr *EncryptedNameRequest) Validate() error { 16 | if enr.AccountID.IsZero() { 17 | return ErrInvalidAccountID 18 | } 19 | 20 | return nil 21 | } 22 | 23 | // EncryptedNameForNodeRequest contains an EncryptedName for an Account and the corresponding NodeID. 24 | type EncryptedNameForNodeRequest struct { 25 | EncryptedNameRequest 26 | NodeID uuid.UUID `json:"node_id"` 27 | } 28 | 29 | // Validate validates the EncryptedNameForNodeRequest. 30 | func (nnr EncryptedNameForNodeRequest) Validate() error { 31 | if nnr.NodeID.IsZero() { 32 | return ErrInvalidNodeID 33 | } 34 | 35 | err := nnr.EncryptedNameRequest.Validate() 36 | if err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internals/api/namespace.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // NamespaceDetails defines a user or organization namespace. 4 | // TODO: rename this to Namespace currently claimed in paths.go 5 | type NamespaceDetails struct { 6 | Name string `json:"name"` 7 | MemberCount int `json:"member_count"` 8 | RepoCount int `json:"repo_count"` 9 | SecretCount int `json:"secret_count"` 10 | } 11 | -------------------------------------------------------------------------------- /internals/api/org.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "bitbucket.org/zombiezen/cardcpx/natsort" 8 | "github.com/secrethub/secrethub-go/internals/api/uuid" 9 | ) 10 | 11 | // Roles 12 | const ( 13 | OrgRoleAdmin = "admin" 14 | OrgRoleMember = "member" 15 | ) 16 | 17 | // Org represents an organization account on SecretHub 18 | type Org struct { 19 | OrgID uuid.UUID `json:"org_id"` 20 | Name string `json:"name"` 21 | Description string `json:"description"` 22 | CreatedAt time.Time `json:"created_at"` 23 | Members []*OrgMember `json:"members,omitempty"` 24 | } 25 | 26 | // SortOrgByName makes a list of orgs sortable. 27 | type SortOrgByName []*Org 28 | 29 | func (s SortOrgByName) Len() int { 30 | return len(s) 31 | } 32 | func (s SortOrgByName) Swap(i, j int) { 33 | s[i], s[j] = s[j], s[i] 34 | } 35 | func (s SortOrgByName) Less(i, j int) bool { 36 | return natsort.Less(s[i].Name, s[j].Name) 37 | } 38 | 39 | // OrgMember represents a user's membership of an organization. 40 | type OrgMember struct { 41 | OrgID uuid.UUID `json:"org_id"` 42 | AccountID uuid.UUID `json:"account_id"` 43 | Role string `json:"role"` 44 | CreatedAt time.Time `json:"created_at"` 45 | LastChangedAt time.Time `json:"last_changed_at"` 46 | User *User `json:"user,omitempty"` 47 | } 48 | 49 | // SortOrgMemberByUsername makes a list of org members sortable. 50 | type SortOrgMemberByUsername []*OrgMember 51 | 52 | func (s SortOrgMemberByUsername) Len() int { 53 | return len(s) 54 | } 55 | func (s SortOrgMemberByUsername) Swap(i, j int) { 56 | s[i], s[j] = s[j], s[i] 57 | } 58 | func (s SortOrgMemberByUsername) Less(i, j int) bool { 59 | if s[i].User == nil || s[j].User == nil { 60 | return false 61 | } 62 | return natsort.Less(s[i].User.Username, s[j].User.Username) 63 | } 64 | 65 | // CreateOrgRequest contains the required fields for creating an organization. 66 | type CreateOrgRequest struct { 67 | Name string `json:"name"` 68 | Description string `json:"description"` 69 | } 70 | 71 | // Validate validates the request fields. 72 | func (req CreateOrgRequest) Validate() error { 73 | err := ValidateOrgName(req.Name) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return ValidateOrgDescription(req.Description) 79 | } 80 | 81 | // CreateOrgMemberRequest contains the required fields for 82 | // creating a user's organization membership. 83 | type CreateOrgMemberRequest struct { 84 | Username string `json:"username"` 85 | Role string `json:"role"` 86 | } 87 | 88 | // Validate validates the request fields. 89 | func (req CreateOrgMemberRequest) Validate() error { 90 | err := ValidateUsername(req.Username) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return ValidateOrgRole(req.Role) 96 | } 97 | 98 | // UpdateOrgMemberRequest contains the required fields for 99 | // updating a user's organization membership. 100 | type UpdateOrgMemberRequest struct { 101 | Role string `json:"role"` 102 | } 103 | 104 | // Validate validates the request fields. 105 | func (req UpdateOrgMemberRequest) Validate() error { 106 | return ValidateOrgRole(req.Role) 107 | } 108 | 109 | // ValidateOrgRole validates an organization role. 110 | func ValidateOrgRole(role string) error { 111 | switch strings.ToLower(role) { 112 | case OrgRoleAdmin, OrgRoleMember: 113 | return nil 114 | default: 115 | return ErrInvalidOrgRole 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /internals/api/paths_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/secrethub/secrethub-go/internals/assert" 7 | ) 8 | 9 | func TestJoinPaths(t *testing.T) { 10 | cases := map[string]struct { 11 | elements []string 12 | expected string 13 | }{ 14 | "one path": { 15 | elements: []string{"namespace/repo"}, 16 | expected: "namespace/repo", 17 | }, 18 | "empty element": { 19 | elements: []string{"namespace/repo", ""}, 20 | expected: "namespace/repo", 21 | }, 22 | "two empty elements": { 23 | elements: []string{"", ""}, 24 | expected: "", 25 | }, 26 | "two paths, without separator": { 27 | elements: []string{"namespace/repo", "dir"}, 28 | expected: "namespace/repo/dir", 29 | }, 30 | "two paths, with separator": { 31 | elements: []string{"namespace/repo", "/dir"}, 32 | expected: "namespace/repo/dir", 33 | }, 34 | "two paths, both with separator": { 35 | elements: []string{"namespace/repo/", "/dir"}, 36 | expected: "namespace/repo/dir", 37 | }, 38 | "no paths": { 39 | elements: []string{}, 40 | expected: "", 41 | }, 42 | } 43 | 44 | for name, tc := range cases { 45 | t.Run(name, func(t *testing.T) { 46 | actual := JoinPaths(tc.elements...) 47 | assert.Equal(t, actual, tc.expected) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internals/api/permission.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Error 4 | var ( 5 | ErrAccessLevelUnknown = errAPI.Code("access_level_unknown").Error("The access level is not known") 6 | ) 7 | 8 | // Permission defines what kind of access an access rule grants or a access level has. 9 | type Permission int 10 | 11 | // The different Permission options. 12 | const ( 13 | PermissionNone Permission = iota 14 | PermissionRead 15 | PermissionWrite 16 | PermissionAdmin 17 | ) 18 | 19 | // Set sets the Permission to the value. 20 | func (al *Permission) Set(value string) error { 21 | switch value { 22 | case "r", "read": 23 | *al = PermissionRead 24 | case "w", "write": 25 | *al = PermissionWrite 26 | case "a", "admin": 27 | *al = PermissionAdmin 28 | case "n", "none": 29 | *al = PermissionNone 30 | default: 31 | return ErrAccessLevelUnknown 32 | } 33 | return nil 34 | } 35 | 36 | func (al Permission) String() string { 37 | switch al { 38 | case PermissionRead: 39 | return "read" 40 | case PermissionWrite: 41 | return "write" 42 | case PermissionAdmin: 43 | return "admin" 44 | default: 45 | return "none" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internals/api/permission_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/internals/assert" 8 | ) 9 | 10 | func TestPermission_Set(t *testing.T) { 11 | testCases := []struct { 12 | input string 13 | value api.Permission 14 | err error 15 | }{ 16 | { 17 | input: "r", 18 | value: api.PermissionRead, 19 | err: nil, 20 | }, 21 | { 22 | input: "read", 23 | value: api.PermissionRead, 24 | err: nil, 25 | }, 26 | { 27 | input: "w", 28 | value: api.PermissionWrite, 29 | err: nil, 30 | }, 31 | { 32 | input: "write", 33 | value: api.PermissionWrite, 34 | err: nil, 35 | }, 36 | { 37 | input: "a", 38 | value: api.PermissionAdmin, 39 | err: nil, 40 | }, 41 | { 42 | input: "admin", 43 | value: api.PermissionAdmin, 44 | err: nil, 45 | }, 46 | { 47 | input: "n", 48 | value: api.PermissionNone, 49 | err: nil, 50 | }, 51 | { 52 | input: "none", 53 | value: api.PermissionNone, 54 | err: nil, 55 | }, 56 | { 57 | input: "unknown", 58 | err: api.ErrAccessLevelUnknown, 59 | }, 60 | { 61 | input: "rw", 62 | err: api.ErrAccessLevelUnknown, 63 | }, 64 | { 65 | input: "rwa", 66 | err: api.ErrAccessLevelUnknown, 67 | }, 68 | { 69 | input: "readwrite", 70 | err: api.ErrAccessLevelUnknown, 71 | }, 72 | { 73 | input: "man", 74 | err: api.ErrAccessLevelUnknown, 75 | }, 76 | } 77 | 78 | for _, testCase := range testCases { 79 | var accessLevel api.Permission 80 | actual := accessLevel.Set(testCase.input) 81 | assert.Equal(t, actual, testCase.err) 82 | assert.Equal(t, accessLevel, testCase.value) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internals/api/repo_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "sort" 7 | 8 | "github.com/secrethub/secrethub-go/internals/api" 9 | "github.com/secrethub/secrethub-go/internals/api/uuid" 10 | "github.com/secrethub/secrethub-go/internals/assert" 11 | ) 12 | 13 | var ( 14 | accountIDUser1 = uuid.New() 15 | 16 | repoMemberRequest1 = &api.CreateRepoMemberRequest{ 17 | RepoEncryptionKey: []byte{1, 2, 3}, 18 | RepoIndexKey: []byte{1, 2, 3}, 19 | } 20 | ) 21 | 22 | func TestCreateRepoRequest_Validate(t *testing.T) { 23 | tests := []struct { 24 | crr api.CreateRepoRequest 25 | expected error 26 | }{ 27 | { 28 | crr: api.CreateRepoRequest{ 29 | Name: "SomeRepoName", 30 | RootDir: nil, 31 | RepoMember: nil, 32 | }, 33 | expected: api.ErrNoRootDir, 34 | }, 35 | { 36 | crr: api.CreateRepoRequest{ 37 | Name: "SomeRepoName", 38 | RootDir: getTestCreateDirRequest(t), 39 | RepoMember: nil, 40 | }, 41 | expected: api.ErrNoRepoMember, 42 | }, 43 | } 44 | 45 | for _, test := range tests { 46 | err := test.crr.Validate() 47 | assert.Equal(t, err, test.expected) 48 | } 49 | } 50 | 51 | func TestInviteUserRequest_Validate_Success(t *testing.T) { 52 | 53 | inviteRequest := &api.InviteUserRequest{ 54 | AccountID: accountIDUser1, 55 | RepoMember: repoMemberRequest1, 56 | } 57 | 58 | err := inviteRequest.Validate() 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | 63 | } 64 | 65 | func TestInviteUserRequest_Validate_InvalidRepoMember(t *testing.T) { 66 | 67 | inviteRequest := &api.InviteUserRequest{ 68 | AccountID: accountIDUser1, 69 | RepoMember: &api.CreateRepoMemberRequest{}, 70 | } 71 | 72 | err := inviteRequest.Validate() 73 | if err == nil { 74 | t.Error("did not throw an error for having an invalid CreateSRepoMemberRequest") 75 | } 76 | 77 | } 78 | 79 | func TestSortRepoByName(t *testing.T) { 80 | listIn := []string{ 81 | "test1", 82 | "test3", 83 | "test10", 84 | "test2", 85 | "test", 86 | "test11", 87 | "test20", 88 | "test_", 89 | "test_1", 90 | "test-", 91 | "test-1", 92 | "test-2", 93 | } 94 | 95 | listOut := []string{ 96 | "test", 97 | "test-", 98 | "test-1", 99 | "test-2", 100 | "test1", 101 | "test2", 102 | "test3", 103 | "test10", 104 | "test11", 105 | "test20", 106 | "test_", 107 | "test_1", 108 | } 109 | 110 | // Test for namespace sorting 111 | byNamespace := make([]*api.Repo, len(listIn)) 112 | for i, name := range listIn { 113 | byNamespace[i] = &api.Repo{Owner: name, Name: "same"} 114 | } 115 | 116 | sort.Sort(api.SortRepoByName(byNamespace)) 117 | for i, repo := range byNamespace { 118 | if repo.Owner != listOut[i] { 119 | t.Errorf("expected %s at position %d, got %s", listOut[i], i, repo.Owner) 120 | } 121 | } 122 | 123 | // Test for name sorting 124 | byName := make([]*api.Repo, len(listIn)) 125 | for i, name := range listIn { 126 | byName[i] = &api.Repo{Owner: "same", Name: name} 127 | } 128 | 129 | sort.Sort(api.SortRepoByName(byName)) 130 | for i, repo := range byName { 131 | if repo.Name != listOut[i] { 132 | t.Errorf("expected %s at position %d, got %s", listOut[i], i, repo.Name) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /internals/api/revoke.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // RevokeResponse is returned when a revoke command is executed. 11 | type RevokeResponse struct { 12 | RevokedSecretVersions []*EncryptedSecretVersion `json:"revoked_secret_versions"` 13 | RevokedSecretKeys []*SecretKey `json:"revoked_secret_keys"` 14 | } 15 | 16 | // RevokeOrgResponse is returned as the effect of revoking an account from a repository. 17 | type RevokeOrgResponse struct { 18 | DryRun bool `json:"dry"` // Dry indicates whether it was a dry run or not. 19 | Repos []*RevokeRepoResponse `json:"repos"` 20 | StatusCounts map[string]int `json:"status_counts"` // StatusCounts contains aggregate counts of the repos the account is revoked from. 21 | } 22 | 23 | // RevokeRepoResponse is returned as the effect of revoking an account from a repo. 24 | type RevokeRepoResponse struct { 25 | Namespace string `json:"namespace"` // Added for display purposes 26 | Name string `json:"name"` // Added for display purposes 27 | Status string `json:"status"` 28 | RevokedSecretVersionCount int `json:"revoked_secret_version_count"` 29 | RevokedSecretKeyCount int `json:"revoked_secret_key_count"` 30 | } 31 | 32 | // RevokeOpts contains optional query parameters for revoke requests. 33 | type RevokeOpts struct { 34 | DryRun bool `url:"dry_run"` // Dry performs a dry run without actually revoking the account. 35 | } 36 | 37 | // Unmarshal decodes url.Values into the options struct, 38 | // setting default values if not present in the query values. 39 | // TODO SHDEV-817: refactor this to a more extendable mechanism. 40 | func (o *RevokeOpts) Unmarshal(values url.Values) { 41 | dry := values.Get("dry_run") 42 | if strings.ToLower(dry) == "true" { 43 | o.DryRun = true 44 | } else { 45 | o.DryRun = false 46 | } 47 | } 48 | 49 | // Values returns the url.Values encoding of the options. 50 | func (o RevokeOpts) Values() (url.Values, error) { 51 | return query.Values(o) 52 | } 53 | -------------------------------------------------------------------------------- /internals/api/secret_key.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api/uuid" 5 | "github.com/secrethub/secrethub-go/internals/crypto" 6 | "github.com/secrethub/secrethub-go/internals/errio" 7 | ) 8 | 9 | // SecretKey represents a secret key that is intended to be used by a specific account. 10 | type SecretKey struct { 11 | SecretKeyID uuid.UUID `json:"secret_key_id"` 12 | AccountID uuid.UUID `json:"account_id"` 13 | Key *crypto.SymmetricKey `json:"key"` 14 | } 15 | 16 | // EncryptedSecretKey represents a secret key, encrypted for a specific account. 17 | type EncryptedSecretKey struct { 18 | SecretKeyID uuid.UUID `json:"secret_key_id"` 19 | AccountID uuid.UUID `json:"account_id"` 20 | EncryptedKey crypto.CiphertextRSA `json:"encrypted_key"` 21 | } 22 | 23 | // Decrypt decrypts an EncryptedSecretKey into a SecretKey. 24 | func (k *EncryptedSecretKey) Decrypt(accountKey *crypto.RSAPrivateKey) (*SecretKey, error) { 25 | keyBytes, err := accountKey.Unwrap(k.EncryptedKey) 26 | if err != nil { 27 | return nil, errio.Error(err) 28 | } 29 | 30 | return &SecretKey{ 31 | SecretKeyID: k.SecretKeyID, 32 | AccountID: k.AccountID, 33 | Key: crypto.NewSymmetricKey(keyBytes), 34 | }, nil 35 | } 36 | 37 | // CreateSecretKeyRequest contains the request fields for creating a new secret key. 38 | type CreateSecretKeyRequest struct { 39 | EncryptedFor []EncryptedKeyRequest `json:"encrypted_for"` 40 | } 41 | 42 | // Validate validates the request fields. 43 | func (r *CreateSecretKeyRequest) Validate() error { 44 | if len(r.EncryptedFor) < 1 { 45 | return ErrNotEncryptedForAccounts 46 | } 47 | 48 | for _, ef := range r.EncryptedFor { 49 | err := ef.Validate() 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // EncryptedKeyRequest contains the request fields for re-encrypted for an account. 59 | type EncryptedKeyRequest struct { 60 | AccountID uuid.UUID `json:"account_id"` 61 | EncryptedKey crypto.CiphertextRSA `json:"encrypted_key"` 62 | } 63 | 64 | // Validate validates the request fields. 65 | func (r *EncryptedKeyRequest) Validate() error { 66 | if r.AccountID.IsZero() { 67 | return ErrInvalidAccountID 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // ToAuditSubject converts a SecretKey to an AuditSubject 74 | func (sk *SecretKey) ToAuditSubject() *AuditSubject { 75 | return &AuditSubject{ 76 | SubjectID: sk.SecretKeyID, 77 | Type: AuditSubjectSecretKey, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internals/api/secret_version_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/secrethub/secrethub-go/internals/api/uuid" 7 | "github.com/secrethub/secrethub-go/internals/crypto" 8 | ) 9 | 10 | func TestCreateSecretVersionRequest_Validate_MaxSize(t *testing.T) { 11 | 12 | tests := []struct { 13 | dataSize int 14 | expectSuccess bool 15 | }{ 16 | { 17 | 10, 18 | true, 19 | }, 20 | { 21 | 512 * 1024, 22 | true, 23 | }, 24 | { 25 | 550 * 1024, 26 | false, 27 | }, 28 | } 29 | 30 | aesKey, err := crypto.GenerateSymmetricKey() 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | for _, test := range tests { 36 | 37 | slice := make([]byte, test.dataSize) 38 | for i := range slice { 39 | slice[i] = 0x1 40 | } 41 | 42 | ciphertext, err := aesKey.Encrypt(slice) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | id := uuid.New() 48 | r := CreateSecretVersionRequest{ 49 | SecretKeyID: id, 50 | EncryptedData: ciphertext, 51 | } 52 | 53 | err = r.Validate() 54 | success := err == nil 55 | if success != test.expectSuccess { 56 | t.Errorf("success (%v) != expectSuccess (%v)", success, test.expectSuccess) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internals/api/service.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/secrethub/secrethub-go/internals/api/uuid" 9 | ) 10 | 11 | // Errors 12 | var ( 13 | ErrInvalidServiceID = errAPI.Code("invalid_service_id").StatusError( 14 | "service id is 14 characters long and starts with s-", 15 | http.StatusBadRequest, 16 | ) 17 | ErrInvalidServiceDescription = errAPI.Code("invalid_service_description").StatusError( 18 | fmt.Sprintf( 19 | "service descriptions can at most be %d long and cannot contain any newlines or tabs", 20 | serviceDescriptionMaxLength, 21 | ), 22 | http.StatusBadRequest, 23 | ) 24 | ErrAccessDeniedToKMSKey = errAPI.Code("access_denied").StatusError("access to KMS key is denied", http.StatusForbidden) 25 | ) 26 | 27 | // Service represents a service account on SecretHub. 28 | type Service struct { 29 | AccountID uuid.UUID `json:"account_id"` 30 | ServiceID string `json:"service_id"` 31 | Repo *Repo `json:"repo"` 32 | Description string `json:"description"` 33 | CreatedBy uuid.UUID `json:"created_by"` 34 | CreatedAt time.Time `json:"created_at"` 35 | Credential *Credential `json:"credential"` 36 | } 37 | 38 | // Trim removes all non-essential fields from Service for output 39 | func (a Service) Trim() *Service { 40 | return &Service{ 41 | AccountID: a.AccountID, 42 | ServiceID: a.ServiceID, 43 | Description: a.Description, 44 | } 45 | } 46 | 47 | // ToAuditSubject converts an Service to an AuditSubject 48 | func (a Service) ToAuditSubject() *AuditSubject { 49 | return &AuditSubject{ 50 | SubjectID: a.AccountID, 51 | Type: AuditSubjectService, 52 | Service: a.Trim(), 53 | } 54 | } 55 | 56 | // ToAuditActor converts an Service to an AuditActor 57 | func (a Service) ToAuditActor() *AuditActor { 58 | return &AuditActor{ 59 | ActorID: a.AccountID, 60 | Type: "service", 61 | Service: a.Trim(), 62 | } 63 | } 64 | 65 | // CreateServiceRequest contains the required fields for creating an Service. 66 | type CreateServiceRequest struct { 67 | Description string `json:"description"` 68 | Credential *CreateCredentialRequest `json:"credential"` 69 | RepoMember *CreateRepoMemberRequest `json:"repo_member"` 70 | } 71 | 72 | // Validate validates the request fields. 73 | func (req CreateServiceRequest) Validate() error { 74 | if err := ValidateServiceDescription(req.Description); err != nil { 75 | return err 76 | } 77 | 78 | if req.Credential == nil { 79 | return ErrMissingField("credential") 80 | } 81 | if err := req.Credential.Validate(); err != nil { 82 | return err 83 | } 84 | 85 | if req.Credential.AccountKey == nil { 86 | return ErrMissingField("account_key") 87 | } 88 | if err := req.Credential.AccountKey.Validate(); err != nil { 89 | return err 90 | } 91 | 92 | if req.RepoMember == nil { 93 | return ErrMissingField("repo_member") 94 | } 95 | if err := req.RepoMember.Validate(); err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /internals/api/service_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestCreateServiceRequest_ValidateDescriptions(t *testing.T) { 9 | 10 | tests := []struct { 11 | desc string 12 | input string 13 | expected error 14 | }{ 15 | { 16 | desc: "normal description", 17 | input: "A valid description with !@#$%^&*()_-.", 18 | expected: nil, 19 | }, 20 | { 21 | desc: "maximum length description", 22 | input: strings.Repeat("a", serviceDescriptionMaxLength), 23 | expected: nil, 24 | }, 25 | { 26 | desc: "longer than maximum length description", 27 | input: strings.Repeat("a", serviceDescriptionMaxLength+1), 28 | expected: ErrInvalidServiceDescription, 29 | }, 30 | { 31 | desc: "description with non-ASCII characters", 32 | input: "立显荣朝士Σumé", 33 | expected: nil, 34 | }, 35 | { 36 | desc: "description with newline", 37 | input: "description\nmore", 38 | expected: ErrInvalidServiceDescription, 39 | }, 40 | { 41 | desc: "description with tab", 42 | input: "description\tmore", 43 | expected: ErrInvalidServiceDescription, 44 | }, 45 | { 46 | desc: "empty description", 47 | input: "", 48 | expected: nil, 49 | }, 50 | } 51 | 52 | for _, test := range tests { 53 | err := ValidateServiceDescription(test.input) 54 | if err != test.expected { 55 | t.Errorf("test %s: returned value is not as expected: %v (actual) != %v (expected)", test.desc, err, test.expected) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internals/api/tree_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/secrethub/secrethub-go/internals/api/uuid" 7 | "github.com/secrethub/secrethub-go/internals/assert" 8 | ) 9 | 10 | func TestAbsDirPath(t *testing.T) { 11 | 12 | // Tree: 13 | // namespace/repo/ 14 | // - repo 15 | // - dir/ 16 | // - subdir/ 17 | 18 | repoDir := &Dir{ 19 | DirID: uuid.New(), 20 | Name: "repo", 21 | } 22 | 23 | dir := &Dir{ 24 | DirID: uuid.New(), 25 | Name: "dir", 26 | ParentID: &repoDir.DirID, 27 | } 28 | 29 | subdir := &Dir{ 30 | DirID: uuid.New(), 31 | Name: "subdir", 32 | ParentID: &dir.DirID, 33 | } 34 | 35 | repoPath := DirPath("namespace/repo") 36 | dirPath := DirPath("namespace/repo/dir") 37 | subdirPath := DirPath("namespace/repo/dir/subdir") 38 | 39 | cases := map[string]struct { 40 | dirID uuid.UUID 41 | tree Tree 42 | expected DirPath 43 | err error 44 | }{ 45 | "path of repo with tree rooted at repo": { 46 | dirID: repoDir.DirID, 47 | tree: Tree{ 48 | ParentPath: "namespace", 49 | RootDir: repoDir, 50 | Dirs: map[uuid.UUID]*Dir{ 51 | repoDir.DirID: repoDir, 52 | }, 53 | Secrets: map[uuid.UUID]*Secret{}, 54 | }, 55 | expected: repoPath, 56 | err: nil, 57 | }, 58 | "path of dir with tree rooted at repo": { 59 | dirID: dir.DirID, 60 | tree: Tree{ 61 | ParentPath: "namespace", 62 | RootDir: repoDir, 63 | Dirs: map[uuid.UUID]*Dir{ 64 | dir.DirID: dir, 65 | }, 66 | Secrets: map[uuid.UUID]*Secret{}, 67 | }, 68 | expected: dirPath, 69 | err: nil, 70 | }, 71 | "path of dir with tree rooted at dir": { 72 | dirID: dir.DirID, 73 | tree: Tree{ 74 | ParentPath: "namespace/repo", 75 | RootDir: dir, 76 | Dirs: map[uuid.UUID]*Dir{ 77 | dir.DirID: dir, 78 | subdir.DirID: subdir, 79 | }, 80 | Secrets: map[uuid.UUID]*Secret{}, 81 | }, 82 | expected: dirPath, 83 | err: nil, 84 | }, 85 | "path of subdir with tree rooted at dir": { 86 | dirID: subdir.DirID, 87 | tree: Tree{ 88 | ParentPath: "namespace/repo", 89 | RootDir: dir, 90 | Dirs: map[uuid.UUID]*Dir{ 91 | dir.DirID: dir, 92 | subdir.DirID: subdir, 93 | }, 94 | Secrets: map[uuid.UUID]*Secret{}, 95 | }, 96 | expected: subdirPath, 97 | err: nil, 98 | }, 99 | "path of dir with dir not in tree": { 100 | dirID: dir.DirID, 101 | tree: Tree{ 102 | ParentPath: "namespace", 103 | RootDir: repoDir, 104 | Dirs: map[uuid.UUID]*Dir{}, 105 | Secrets: map[uuid.UUID]*Secret{}, 106 | }, 107 | expected: "", 108 | err: ErrDirNotFound, 109 | }, 110 | } 111 | 112 | for name, tc := range cases { 113 | t.Run(name, func(t *testing.T) { 114 | // Act 115 | actual, err := tc.tree.AbsDirPath(tc.dirID) 116 | 117 | // Assert 118 | assert.Equal(t, err, tc.err) 119 | assert.Equal(t, actual, tc.expected) 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /internals/api/user.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "net/http" 8 | 9 | "github.com/secrethub/secrethub-go/internals/api/uuid" 10 | "github.com/secrethub/secrethub-go/internals/errio" 11 | ) 12 | 13 | // Errors 14 | var ( 15 | errAPI = errio.Namespace("api") 16 | 17 | ErrInvalidUsername = errAPI.Code("invalid_username").StatusError( 18 | "usernames must be between 3 and 32 characters long and "+ 19 | "may only contain letters, numbers, dashes (-), underscores (_), and dots (.)", 20 | http.StatusBadRequest, 21 | ) 22 | ErrUsernameMustContainAlphanumeric = errAPI.Code("username_must_contain_alphanumeric").StatusError( 23 | "usernames must contain at least one alphanumeric character ", 24 | http.StatusBadRequest, 25 | ) 26 | ErrUsernameIsService = errAPI.Code("username_is_service").StatusError( 27 | "usernames cannot start with s- as that prefix is reserved for service accounts", 28 | http.StatusBadRequest, 29 | ) 30 | ErrInvalidPublicKey = errAPI.Code("invalid_public_key").StatusError("public key is invalid", http.StatusBadRequest) 31 | ErrInvalidEmail = errAPI.Code("invalid_email").StatusError("email address is invalid", http.StatusBadRequest) 32 | ErrInvalidFullName = errAPI.Code("invalid_full_name").StatusError( 33 | "full names may be at most 128 characters long and "+ 34 | "may only contain (special) letters, apostrophes ('), spaces and dashes (-)", 35 | http.StatusBadRequest, 36 | ) 37 | ErrNoPasswordNorCredential = errAPI.Code("no_password_nor_credential").StatusError("either a password or a credential should be supplied", http.StatusBadRequest) 38 | ErrTooManyVerificationRequests = errAPI.Code("too_many_verification_requests").StatusError("another verification email was requested recently, please wait a few minutes before trying again", http.StatusTooManyRequests) 39 | ) 40 | 41 | // User represents a SecretHub user. 42 | type User struct { 43 | AccountID uuid.UUID `json:"account_id"` 44 | PublicKey []byte `json:"public_key"` 45 | Username string `json:"username"` 46 | FullName string `json:"full_name"` 47 | Email string `json:"user_email,omitempty"` // Optional, private information is only returned for yourself 48 | EmailVerified bool `json:"email_verified,omitempty"` // Optional, private information is only returned for yourself 49 | CreatedAt *time.Time `json:"created_at,omitempty"` // Optional, private information is only returned for yourself 50 | LastLoginAt *time.Time `json:"last_login_at,omitempty"` // Optional, private information is only returned for yourself 51 | } 52 | 53 | // PrettyName returns a printable string with the username and full name. 54 | func (u User) PrettyName() string { 55 | if u.FullName == "" { 56 | return u.Username 57 | } 58 | return fmt.Sprintf("%s (%s)", u.Username, u.FullName) 59 | } 60 | 61 | // Trim removes all non-essential fields from User for output 62 | func (u User) Trim() *User { 63 | return &User{ 64 | AccountID: u.AccountID, 65 | Username: u.Username, 66 | FullName: u.FullName, 67 | PublicKey: u.PublicKey, 68 | } 69 | } 70 | 71 | // ToAuditSubject converts a User to an AuditSubject 72 | func (u User) ToAuditSubject() *AuditSubject { 73 | return &AuditSubject{ 74 | SubjectID: u.AccountID, 75 | Type: AuditSubjectUser, 76 | User: u.Trim(), 77 | } 78 | } 79 | 80 | // ToAuditActor converts a User to an AuditActor 81 | func (u User) ToAuditActor() *AuditActor { 82 | return &AuditActor{ 83 | ActorID: u.AccountID, 84 | Type: "user", 85 | User: u.Trim(), 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internals/api/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | // Package uuid is a utility package to standardize and abstract away how UUIDs are generated and used. 2 | package uuid 3 | 4 | import ( 5 | "bytes" 6 | 7 | gid "github.com/gofrs/uuid" 8 | 9 | "github.com/secrethub/secrethub-go/internals/errio" 10 | ) 11 | 12 | // Errors 13 | var ( 14 | ErrInvalidUUID = errio.Namespace("uuid").Code("invalid").ErrorPref("invalid uuid: %s") 15 | ) 16 | 17 | // UUID is a wrapper around github.com/gofrs/uuid.UUID. 18 | type UUID struct { 19 | gid.UUID 20 | } 21 | 22 | // New generates a new UUID. 23 | func New() UUID { 24 | id, err := gid.NewV4() 25 | if err != nil { 26 | panic(err) 27 | } 28 | return UUID{id} 29 | } 30 | 31 | // FromString reads a UUID from a string 32 | func FromString(str string) (UUID, error) { 33 | id, err := gid.FromString(str) 34 | if err != nil { 35 | return UUID{}, ErrInvalidUUID(err) 36 | } 37 | return UUID{id}, nil 38 | } 39 | 40 | // ToString converts UUID into string 41 | func (u *UUID) ToString() string { 42 | return u.UUID.String() 43 | } 44 | 45 | // IsZero returns true if the UUID is equal to the zero-value. 46 | func (u *UUID) IsZero() bool { 47 | return u.UUID == gid.UUID([gid.Size]byte{0}) 48 | } 49 | 50 | // Equal returns true if both argument UUIDs contain the same value. 51 | func Equal(a UUID, b UUID) bool { 52 | return bytes.Equal(a.UUID[:], b.UUID[:]) 53 | } 54 | 55 | // Validate validates a uuid string is a valid UUID. 56 | func Validate(str string) error { 57 | _, err := FromString(str) 58 | return err 59 | } 60 | -------------------------------------------------------------------------------- /internals/api/values.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // String converts a string into a *string. 4 | func String(val string) *string { 5 | return &val 6 | } 7 | 8 | // StringValue safely converts a *string into a string. 9 | func StringValue(val *string) string { 10 | if val != nil { 11 | return *val 12 | } 13 | return "" 14 | } 15 | 16 | // Int converts an int into a *int. 17 | func Int(val int) *int { 18 | return &val 19 | } 20 | 21 | // IntValue safely converts a *int into an int. 22 | func IntValue(val *int) int { 23 | if val != nil { 24 | return *val 25 | } 26 | return 0 27 | } 28 | -------------------------------------------------------------------------------- /internals/assert/asserts.go: -------------------------------------------------------------------------------- 1 | // Package assert is a utility package that provides simple 2 | // assertions to help with writing tests. 3 | package assert 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/kylelemons/godebug/pretty" 9 | ) 10 | 11 | // Equal errors when actual and expected are not the same, printing out the diff. 12 | func Equal(tb testing.TB, actual, expected interface{}) { 13 | tb.Helper() 14 | diff := diff(actual, expected) 15 | if diff != "" { 16 | tb.Errorf("unexpected diff (-actual +expected):\n%s", diff) 17 | } 18 | } 19 | 20 | // OK fails a test if the provided error is not nil. 21 | func OK(tb testing.TB, err error) { 22 | tb.Helper() 23 | if err != nil { 24 | tb.Fatal(err) 25 | } 26 | } 27 | 28 | func diff(actual, expected interface{}) string { 29 | c := pretty.Config{ 30 | Compact: true, 31 | IncludeUnexported: true, 32 | Formatter: pretty.DefaultFormatter, 33 | } 34 | return c.Compare(actual, expected) 35 | } 36 | -------------------------------------------------------------------------------- /internals/auth/docs.go: -------------------------------------------------------------------------------- 1 | // Package auth provides authentication to the SecretHub API. 2 | package auth 3 | -------------------------------------------------------------------------------- /internals/auth/nop.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "net/http" 4 | 5 | // NopAuthenticator is an authenticator that does not add any authentication to the request. 6 | type NopAuthenticator struct{} 7 | 8 | // Authenticate the provided request. 9 | func (s NopAuthenticator) Authenticate(r *http.Request) error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /internals/auth/session.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api/uuid" 5 | "github.com/secrethub/secrethub-go/internals/crypto" 6 | ) 7 | 8 | // NewSessionSigner returns a new SessionSigner. 9 | func NewSessionSigner(sessionID uuid.UUID, secretKey string) *SessionSigner { 10 | return &SessionSigner{ 11 | sessionID: sessionID, 12 | secretKey: secretKey, 13 | } 14 | } 15 | 16 | // SessionSigner is an implementation of the Signer interface that uses an HMAC session to authenticate a request. 17 | type SessionSigner struct { 18 | sessionID uuid.UUID 19 | secretKey string 20 | } 21 | 22 | // ID returns the session id of this signer. 23 | func (s SessionSigner) ID() (string, error) { 24 | return s.sessionID.String(), nil 25 | } 26 | 27 | // SignMethod returns the signature method of this signer. 28 | func (s SessionSigner) SignMethod() string { 29 | return "Session-HMAC" 30 | } 31 | 32 | // Sign the payload with an HMAC signature. 33 | func (s SessionSigner) Sign(msg []byte) ([]byte, error) { 34 | key := crypto.NewSymmetricKey([]byte(s.secretKey)) 35 | return key.HMAC(msg) 36 | } 37 | -------------------------------------------------------------------------------- /internals/auth/signature_internal_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "net/http" 11 | 12 | "bytes" 13 | 14 | "io/ioutil" 15 | 16 | "github.com/secrethub/secrethub-go/internals/assert" 17 | ) 18 | 19 | func TestGetMessage_Get(t *testing.T) { 20 | 21 | // Arrange 22 | req, err := http.NewRequest("GET", "https://api.secrethub.io/repos/jdoe/catpictures", nil) 23 | assert.OK(t, err) 24 | req.Header.Set("Date", "Fri, 10 Mar 2017 16:25:54 CET") 25 | 26 | expected := "GET\n" + 27 | "\n" + 28 | "Fri, 10 Mar 2017 16:25:54 CET\n" + 29 | "/repos/jdoe/catpictures;" 30 | 31 | // Act 32 | result, err := getMessage(req) 33 | assert.OK(t, err) 34 | 35 | // Assert 36 | assertMessage(t, expected, string(result)) 37 | } 38 | 39 | func TestGetMessage_Post(t *testing.T) { 40 | 41 | // Assert 42 | body := bytes.NewBufferString("GRUMBYCAT") 43 | 44 | req, err := http.NewRequest("POST", "https://api.secrethub.io/repos/jdoe/catpictures", body) 45 | assert.OK(t, err) 46 | req.Header.Set("Date", "Fri, 10 Mar 2017 16:25:54 CET") 47 | 48 | bodySum := sha256.Sum256(body.Bytes()) 49 | encodedBody := base64.StdEncoding.EncodeToString(bodySum[:]) 50 | 51 | expected := "POST\n" + 52 | encodedBody + "\n" + 53 | "Fri, 10 Mar 2017 16:25:54 CET\n" + 54 | "/repos/jdoe/catpictures;\n" + 55 | "" 56 | 57 | // Act 58 | result, err := getMessage(req) 59 | assert.OK(t, err) 60 | 61 | // Assert 62 | assertMessage(t, expected, string(result)) 63 | } 64 | 65 | func assertMessage(t *testing.T, expected, result string) { 66 | 67 | resultPayload := strings.Split(result, ";") 68 | resultSplits := strings.Split(resultPayload[0], "\n") 69 | 70 | expectedPayload := strings.Split(expected, ";") 71 | expectedSplits := strings.Split(expectedPayload[0], "\n") 72 | 73 | if len(resultSplits) != 4 { 74 | t.Errorf("Payload not correct number of lines.") 75 | } 76 | 77 | // Method 78 | if resultSplits[0] != expectedSplits[0] { 79 | t.Errorf("method not in payload correctly.\n Expected:\n%s\n Actual: \n%s\n", expectedSplits[0], resultSplits[0]) 80 | } 81 | 82 | if resultSplits[1] != expectedSplits[1] { 83 | t.Errorf("content-hash not in payload correctly.\n Expected:\n%s\n Actual: \n%s\n", expectedSplits[1], resultSplits[1]) 84 | } 85 | 86 | _, err := time.Parse(time.RFC1123, expectedSplits[2]) 87 | // Time 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | // Resource 92 | if resultSplits[3] != expectedSplits[3] { 93 | t.Errorf("resource not in payload correctly.\n Expected:\n%s\n Actual: \n%s\n", expectedSplits[3], resultSplits[3]) 94 | } 95 | } 96 | 97 | // ContentLength should still equal the body length 98 | func TestGetPayloadToSign_ContentLength(t *testing.T) { 99 | 100 | // Assert 101 | requestBody := bytes.NewBufferString("GRUMBYCAT") 102 | 103 | req, err := http.NewRequest("POST", "https://api.secrethub.io/repos/jdoe/catpictures", requestBody) 104 | assert.OK(t, err) 105 | req.Header.Set("Date", "Fri, 10 Mar 2017 16:25:54 CET") 106 | 107 | // Act 108 | _, err = getMessage(req) 109 | assert.OK(t, err) 110 | 111 | // Assert 112 | body, err := ioutil.ReadAll(req.Body) 113 | assert.OK(t, err) 114 | 115 | if len(body) != int(req.ContentLength) { 116 | t.Fatal("Content-Length should equal body length.") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internals/auth/signature_test.go: -------------------------------------------------------------------------------- 1 | package auth_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" 8 | 9 | "github.com/secrethub/secrethub-go/internals/assert" 10 | "github.com/secrethub/secrethub-go/internals/auth" 11 | "github.com/secrethub/secrethub-go/internals/crypto" 12 | ) 13 | 14 | func TestSignRequest_CheckHeadersAreSet(t *testing.T) { 15 | // Arrange 16 | clientKey, err := crypto.GenerateRSAPrivateKey(1024) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | signer := auth.NewHTTPSigner(credentials.RSACredential{RSAPrivateKey: clientKey}) 22 | 23 | req, err := http.NewRequest("GET", "https://api.secrethub.io/repos/jdoe/catpictures", nil) 24 | assert.OK(t, err) 25 | 26 | // Act 27 | err = signer.Authenticate(req) 28 | assert.OK(t, err) 29 | 30 | // Assert 31 | if req.Header.Get("Date") == "" { 32 | t.Error("Date header not set.") 33 | } 34 | 35 | if req.Header.Get("Authorization") == "" { 36 | t.Error("Authorization header not set.") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internals/aws/docs.go: -------------------------------------------------------------------------------- 1 | // Package aws provides Keyless Authentication for services running on AWS. 2 | package aws 3 | -------------------------------------------------------------------------------- /internals/aws/errors.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/awserr" 5 | "github.com/secrethub/secrethub-go/internals/errio" 6 | ) 7 | 8 | // Errors 9 | var ( 10 | awsErr = errio.Namespace("aws") 11 | ErrNoAWSCredentials = awsErr.Code("no_aws_credentials").Error("could not find any AWS credentials. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html for how to configure your credentials") 12 | ErrInvalidAWSCredential = awsErr.Code("invalid_credential").Error("credentials were not accepted by AWS") 13 | ErrAWSRequestError = awsErr.Code("request_error").Error("could not send AWS request") 14 | ErrAWSNotFound = awsErr.Code("not_found") 15 | ErrAWSAccessDenied = awsErr.Code("access_denied") 16 | ) 17 | 18 | func HandleError(err error) error { 19 | errAWS, ok := err.(awserr.Error) 20 | if ok { 21 | switch errAWS.Code() { 22 | case "NoCredentialProviders", "MissingAuthenticationToken", "MissingAuthenticationTokenException": 23 | return ErrNoAWSCredentials 24 | case "UnrecognizedClientException", "InvalidClientTokenId": 25 | return ErrInvalidAWSCredential 26 | case "RequestError": 27 | return ErrAWSRequestError 28 | case "AccessDeniedException": 29 | return ErrAWSAccessDenied.Error(errAWS.Message()) 30 | case "NotFoundException": 31 | return ErrAWSNotFound.Error(errAWS.Message()) 32 | } 33 | 34 | return awsErr.Code(errAWS.Code()).Error(errAWS.Message()) 35 | } 36 | return err 37 | } 38 | -------------------------------------------------------------------------------- /internals/aws/kms_decrypter.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/arn" 6 | "github.com/aws/aws-sdk-go/service/kms/kmsiface" 7 | 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/kms" 10 | "github.com/secrethub/secrethub-go/internals/api" 11 | ) 12 | 13 | // KMSDecrypter is an implementation of the secrethub.Decrypter interface that uses AWS KMS for decryption. 14 | type KMSDecrypter struct { 15 | kmsSvcGetter func(region string) kmsiface.KMSAPI 16 | } 17 | 18 | // NewKMSDecrypter returns a new KMSDecrypter that uses the provided configuration to configure the AWS session. 19 | func NewKMSDecrypter(cfgs ...*aws.Config) (*KMSDecrypter, error) { 20 | sess, err := session.NewSession(cfgs...) 21 | if err != nil { 22 | return nil, HandleError(err) 23 | } 24 | 25 | return &KMSDecrypter{ 26 | kmsSvcGetter: func(region string) kmsiface.KMSAPI { 27 | return kms.New(sess, aws.NewConfig().WithRegion(region)) 28 | }, 29 | }, nil 30 | } 31 | 32 | // Unwrap the provided ciphertext using AWS KMS. 33 | func (d KMSDecrypter) Unwrap(ciphertext *api.EncryptedData) ([]byte, error) { 34 | key, ok := ciphertext.Key.(*api.EncryptionKeyAWS) 35 | if !ok { 36 | return nil, api.ErrInvalidKeyType 37 | } 38 | keyARN, err := arn.Parse(key.ID) 39 | if err != nil { 40 | return nil, api.ErrInvalidCiphertext 41 | } 42 | 43 | svc := d.kmsSvcGetter(keyARN.Region) 44 | resp, err := svc.Decrypt(&kms.DecryptInput{ 45 | CiphertextBlob: ciphertext.Ciphertext, 46 | }) 47 | if err != nil { 48 | return nil, HandleError(err) 49 | } 50 | return resp.Plaintext, nil 51 | } 52 | -------------------------------------------------------------------------------- /internals/aws/kms_decrypter_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/secrethub/secrethub-go/internals/assert" 7 | 8 | "github.com/secrethub/secrethub-go/internals/api" 9 | 10 | "github.com/aws/aws-sdk-go/service/kms" 11 | "github.com/aws/aws-sdk-go/service/kms/kmsiface" 12 | ) 13 | 14 | type kmsDecryptMock struct { 15 | kmsiface.KMSAPI 16 | resp *kms.DecryptOutput 17 | err error 18 | 19 | ciphertext []byte 20 | } 21 | 22 | func (m *kmsDecryptMock) Decrypt(in *kms.DecryptInput) (*kms.DecryptOutput, error) { 23 | m.ciphertext = in.CiphertextBlob 24 | return m.resp, m.err 25 | } 26 | 27 | func TestKMSDecrypter_Unwrap(t *testing.T) { 28 | defaultCiphertext := []byte("ciphertext") 29 | defaultRegion := "eu-west-1" 30 | defaultKMSKey := "arn:aws:kms:" + defaultRegion + ":111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" 31 | defaultPlaintext := []byte("plaintext") 32 | 33 | cases := map[string]struct { 34 | input *api.EncryptedData 35 | decryptResponse *kms.DecryptOutput 36 | decryptErr error 37 | expectedRegion string 38 | expected []byte 39 | expectedErr error 40 | }{ 41 | "success": { 42 | input: api.NewEncryptedDataAWSKMS(defaultCiphertext, api.NewEncryptionKeyAWS(defaultKMSKey)), 43 | decryptResponse: &kms.DecryptOutput{ 44 | KeyId: &defaultKMSKey, 45 | Plaintext: defaultPlaintext, 46 | }, 47 | expectedRegion: defaultRegion, 48 | expected: defaultPlaintext, 49 | }, 50 | "invalid EncryptedData": { 51 | input: api.NewEncryptedDataAESGCM(defaultCiphertext, []byte("nonce"), 10, api.NewEncryptionKeyLocal(256)), 52 | expectedErr: api.ErrInvalidKeyType, 53 | }, 54 | "invalid keyID": { 55 | input: api.NewEncryptedDataAWSKMS(defaultCiphertext, api.NewEncryptionKeyAWS("not-an-arn")), 56 | expectedErr: api.ErrInvalidCiphertext, 57 | }, 58 | "decryption error": { 59 | input: api.NewEncryptedDataAWSKMS(defaultCiphertext, api.NewEncryptionKeyAWS(defaultKMSKey)), 60 | decryptErr: errTest, 61 | expectedErr: errTest, 62 | }, 63 | } 64 | 65 | for name, tc := range cases { 66 | t.Run(name, func(t *testing.T) { 67 | var usedRegion string 68 | 69 | decrypter := KMSDecrypter{ 70 | kmsSvcGetter: func(region string) kmsiface.KMSAPI { 71 | usedRegion = region 72 | return &kmsDecryptMock{ 73 | resp: tc.decryptResponse, 74 | err: tc.decryptErr, 75 | } 76 | }, 77 | } 78 | 79 | res, err := decrypter.Unwrap(tc.input) 80 | assert.Equal(t, err, tc.expectedErr) 81 | if tc.expectedErr == nil { 82 | assert.Equal(t, res, tc.expected) 83 | assert.Equal(t, usedRegion, tc.expectedRegion) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internals/crypto/docs.go: -------------------------------------------------------------------------------- 1 | // Package crypto provides the all cryptographic functions used 2 | // by the client (e.g. to encrypt/decrypt secrets). 3 | package crypto 4 | -------------------------------------------------------------------------------- /internals/crypto/hash.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import "crypto/sha256" 4 | 5 | // SHA256 creates a SHA256 hash of the given bytes. 6 | func SHA256(in []byte) []byte { 7 | hash := sha256.Sum256(in) 8 | return hash[:] 9 | } 10 | -------------------------------------------------------------------------------- /internals/crypto/pem.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "strings" 7 | ) 8 | 9 | // Errors 10 | var ( 11 | ErrCannotDecryptKey = errCrypto.Code("decrypt_error").ErrorPref("cannot decrypt key: %s") 12 | ErrIncorrectPassphrase = errCrypto.Code("incorrect_passphrase").Error("cannot decrypt key: passphrase is incorrect") 13 | ErrNoPassphraseProvided = errCrypto.Code("no_passphrase_provided").Error("cannot decode key: no passphrase provided") 14 | ErrDecryptionUnarmoredKey = errCrypto.Code("decryption_unarmored_key").Error("cannot decode key: trying to decrypt unarmored key") 15 | ) 16 | 17 | // PEMKey contains a PEM encoded key and provides decode functions to RSAPrivateKey. 18 | // Note that PEM encoded keys will be deprecated, so only decoding functions 19 | // remain in the code base. To encode keys, check out RSAPrivateKey.Export instead. 20 | type PEMKey struct { 21 | block *pem.Block 22 | } 23 | 24 | // ReadPEM reads a single PEM key from a []byte. 25 | func ReadPEM(key []byte) (*PEMKey, error) { 26 | pemBlock, rest := pem.Decode(key) 27 | if pemBlock == nil { 28 | return nil, ErrNoKeyInFile 29 | 30 | } else if len(rest) > 0 { 31 | return nil, ErrMultipleKeysInFile 32 | } 33 | 34 | return &PEMKey{ 35 | block: pemBlock, 36 | }, nil 37 | } 38 | 39 | // IsEncrypted checks if the key is encrypted or not. 40 | // This can be useful for determining whether to Decrypt 41 | // or Decode the key. 42 | func (k PEMKey) IsEncrypted() bool { 43 | procType := k.block.Headers["Proc-Type"] 44 | if procType == "" { 45 | return false 46 | } 47 | // First element is PEM RFC version. 48 | procTypeSplit := strings.Split(procType, ",")[1:] 49 | 50 | for _, value := range procTypeSplit { 51 | if value == "ENCRYPTED" { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | // Decrypt decrypts the key using the password and decodes the PEM key to an RSA Key. 59 | // If the key is not encrypted it returns ErrDecryptionUnarmoredKey and Decode should 60 | // have been called instead. Use IsEncrypted to determine whether to Decrypt or only 61 | // Decode the key. 62 | func (k *PEMKey) Decrypt(password []byte) (RSAPrivateKey, error) { 63 | if !k.IsEncrypted() { 64 | return RSAPrivateKey{}, ErrDecryptionUnarmoredKey 65 | } 66 | 67 | bytes, err := x509.DecryptPEMBlock(k.block, password) 68 | if err == x509.IncorrectPasswordError { 69 | return RSAPrivateKey{}, ErrIncorrectPassphrase 70 | } else if err != nil { 71 | return RSAPrivateKey{}, ErrCannotDecryptKey(err) 72 | } 73 | 74 | key, err := x509.ParsePKCS1PrivateKey(bytes) 75 | if err != nil { 76 | return RSAPrivateKey{}, ErrNotPKCS1Format 77 | } 78 | 79 | return NewRSAPrivateKey(key), nil 80 | } 81 | 82 | // Decode decodes the pem key to an RSA Key. If the key is encrypted it returns 83 | // ErrNoPassphraseProvided and Decrypt should have been called. Use IsEncrypted 84 | // to determine whether to Decrypt or only Decode the key. 85 | func (k PEMKey) Decode() (RSAPrivateKey, error) { 86 | if k.IsEncrypted() { 87 | return RSAPrivateKey{}, ErrNoPassphraseProvided 88 | } 89 | 90 | key, err := x509.ParsePKCS1PrivateKey(k.block.Bytes) 91 | if err != nil { 92 | return RSAPrivateKey{}, ErrNotPKCS1Format 93 | } 94 | 95 | return NewRSAPrivateKey(key), nil 96 | } 97 | -------------------------------------------------------------------------------- /internals/crypto/symmetric_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/secrethub/secrethub-go/internals/assert" 9 | ) 10 | 11 | func TestAESKey_Encrypt_Decrypt_Secret(t *testing.T) { 12 | encryptionKey, err := GenerateSymmetricKey() 13 | assert.Equal(t, err, nil) 14 | 15 | testData := []byte("testdata") 16 | 17 | ciphertext, err := encryptionKey.Encrypt(testData) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | 22 | decryptedData, err := encryptionKey.Decrypt(ciphertext) 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | 27 | if !bytes.Equal(testData, decryptedData) { 28 | t.Fail() 29 | } 30 | } 31 | 32 | func TestSymmetricKey_HMAC(t *testing.T) { 33 | // Setup 34 | indexKey, err := GenerateSymmetricKey() 35 | assert.OK(t, err) 36 | testData := []byte("testDataString") 37 | 38 | // Act 39 | result, err := indexKey.HMAC(testData) 40 | assert.OK(t, err) 41 | 42 | // Assert 43 | if bytes.Equal(result, testData) { 44 | t.Fail() 45 | } 46 | 47 | // Hash should not be appended. 48 | if len(result) > len(testData) { 49 | if bytes.Equal(result[:len(testData)], testData) { 50 | t.Fatal("Hash is appended") 51 | } 52 | } 53 | } 54 | 55 | func TestCiphertextAES_MarshalJSON(t *testing.T) { 56 | cases := map[string]struct { 57 | ciphertext CiphertextAES 58 | expected string 59 | }{ 60 | "success": { 61 | ciphertext: CiphertextAES{ 62 | Data: []byte("aes_data"), 63 | Nonce: []byte("nonce_data"), 64 | }, 65 | expected: "AES-GCM$YWVzX2RhdGE=$nonce=bm9uY2VfZGF0YQ==", 66 | }, 67 | } 68 | 69 | for name, tc := range cases { 70 | t.Run(name+" encoded", func(t *testing.T) { 71 | // Act 72 | actual := tc.ciphertext.EncodeToString() 73 | 74 | // Assert 75 | assert.Equal(t, actual, tc.expected) 76 | }) 77 | 78 | t.Run(name+" json", func(t *testing.T) { 79 | // Act 80 | actual, err := tc.ciphertext.MarshalJSON() 81 | assert.OK(t, err) 82 | expected, err := json.Marshal(tc.expected) 83 | assert.OK(t, err) 84 | 85 | // Assert 86 | assert.Equal(t, actual, expected) 87 | }) 88 | } 89 | } 90 | 91 | func TestCiphertextRSAAES_MarshalJSON(t *testing.T) { 92 | cases := map[string]struct { 93 | ciphertext CiphertextRSAAES 94 | expected string 95 | }{ 96 | "success": { 97 | ciphertext: CiphertextRSAAES{ 98 | AES: CiphertextAES{ 99 | Data: []byte("aes_data"), 100 | Nonce: []byte("nonce_data"), 101 | }, 102 | RSA: CiphertextRSA{ 103 | Data: []byte("rsa_data"), 104 | }, 105 | }, 106 | expected: "RSA-OAEP+AES-GCM$YWVzX2RhdGE=$key=cnNhX2RhdGE=,nonce=bm9uY2VfZGF0YQ==", 107 | }, 108 | } 109 | 110 | for name, tc := range cases { 111 | t.Run(name+" encoded", func(t *testing.T) { 112 | // Act 113 | actual := tc.ciphertext.EncodeToString() 114 | 115 | // Assert 116 | assert.Equal(t, actual, tc.expected) 117 | }) 118 | 119 | t.Run(name+" json", func(t *testing.T) { 120 | // Act 121 | actual, err := tc.ciphertext.MarshalJSON() 122 | assert.OK(t, err) 123 | expected, err := json.Marshal(tc.expected) 124 | assert.OK(t, err) 125 | 126 | // Assert 127 | assert.Equal(t, actual, expected) 128 | }) 129 | } 130 | } 131 | 132 | func Test_generateNonce(t *testing.T) { 133 | // act 134 | nonce1, err := generateNonce(32) 135 | if err != nil { 136 | t.Error(err) 137 | } 138 | nonce2, err := generateNonce(32) 139 | if err != nil { 140 | t.Error(err) 141 | } 142 | 143 | if bytes.Equal(nonce1, nonce2) { 144 | t.Fatal("Same Salt generated.") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /internals/gcp/errors.go: -------------------------------------------------------------------------------- 1 | package gcp 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "google.golang.org/api/googleapi" 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/grpc/status" 10 | 11 | "github.com/secrethub/secrethub-go/internals/errio" 12 | ) 13 | 14 | // Errors 15 | var ( 16 | gcpErr = errio.Namespace("gcp") 17 | ErrGCPAlreadyExists = gcpErr.Code("already_exists") 18 | ErrGCPNotFound = gcpErr.Code("not_found") 19 | ErrGCPAccessDenied = gcpErr.Code("access_denied") 20 | ErrGCPInvalidArgument = gcpErr.Code("invalid_argument") 21 | ErrGCPUnauthenticated = gcpErr.Code("unauthenticated").Error("missing valid GCP authentication") 22 | ) 23 | 24 | func HandleError(err error) error { 25 | errGCP, ok := err.(*googleapi.Error) 26 | if ok { 27 | message := errGCP.Message 28 | switch errGCP.Code { 29 | case http.StatusNotFound: 30 | if message == "" { 31 | message = "Response from the Google API: 404 Not Found" 32 | } 33 | return ErrGCPNotFound.Error(message) 34 | case http.StatusForbidden: 35 | if message == "" { 36 | message = "Response from the Google API: 403 Forbidden" 37 | } 38 | return ErrGCPAccessDenied.Error(message) 39 | case http.StatusConflict: 40 | if message == "" { 41 | message = "Response from the Google API: 409 Conflict" 42 | } 43 | return ErrGCPAlreadyExists.Error(message) 44 | } 45 | if len(errGCP.Errors) > 0 { 46 | return gcpErr.Code(errGCP.Errors[0].Reason).Error(errGCP.Errors[0].Message) 47 | } 48 | } 49 | errStatus, ok := status.FromError(err) 50 | if ok { 51 | msg := strings.TrimSuffix(errStatus.Message(), ".") 52 | switch errStatus.Code() { 53 | case codes.InvalidArgument: 54 | return ErrGCPInvalidArgument.Error(msg) 55 | case codes.NotFound: 56 | return ErrGCPNotFound.Error(msg) 57 | case codes.PermissionDenied: 58 | return ErrGCPAccessDenied.Error(msg) 59 | case codes.Unauthenticated: 60 | return ErrGCPUnauthenticated 61 | } 62 | } 63 | return err 64 | } 65 | -------------------------------------------------------------------------------- /internals/gcp/kms_decrypter.go: -------------------------------------------------------------------------------- 1 | package gcp 2 | 3 | import ( 4 | "context" 5 | 6 | kms "cloud.google.com/go/kms/apiv1" 7 | "google.golang.org/api/option" 8 | kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" 9 | 10 | "github.com/secrethub/secrethub-go/internals/api" 11 | ) 12 | 13 | // KMSDecrypter is an implementation of the secrethub.Decrypter interface that uses GCP KMS for decryption. 14 | type KMSDecrypter struct { 15 | decryptFunc func(name string, ciphertext []byte) (*kmspb.DecryptResponse, error) 16 | } 17 | 18 | // NewKMSDecrypter returns a new KMSDecrypter that uses the provided configuration to configure the GCP session. 19 | func NewKMSDecrypter(options ...option.ClientOption) (*KMSDecrypter, error) { 20 | kmsClient, err := kms.NewKeyManagementClient(context.Background(), options...) 21 | if err != nil { 22 | return nil, HandleError(err) 23 | } 24 | return &KMSDecrypter{ 25 | decryptFunc: func(name string, ciphertext []byte) (*kmspb.DecryptResponse, error) { 26 | return kmsClient.Decrypt(context.Background(), &kmspb.DecryptRequest{ 27 | Name: name, 28 | Ciphertext: ciphertext, 29 | }) 30 | }, 31 | }, nil 32 | } 33 | 34 | // Unwrap the provided ciphertext using GCP KMS. 35 | func (d KMSDecrypter) Unwrap(ciphertext *api.EncryptedData) ([]byte, error) { 36 | key, ok := ciphertext.Key.(*api.EncryptionKeyGCP) 37 | if !ok { 38 | return nil, api.ErrInvalidKeyType 39 | } 40 | resp, err := d.decryptFunc(key.ID, ciphertext.Ciphertext) 41 | if err != nil { 42 | return nil, HandleError(err) 43 | } 44 | return resp.Plaintext, nil 45 | } 46 | -------------------------------------------------------------------------------- /internals/gcp/kms_decrypter_test.go: -------------------------------------------------------------------------------- 1 | package gcp 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "google.golang.org/genproto/googleapis/cloud/kms/v1" 8 | 9 | "github.com/secrethub/secrethub-go/internals/assert" 10 | 11 | "github.com/secrethub/secrethub-go/internals/api" 12 | ) 13 | 14 | var errTest = errors.New("test-error") 15 | 16 | func TestGCPDecrypter_Unwrap(t *testing.T) { 17 | defaultCiphertext := []byte("ciphertext") 18 | defaultKMSKey := "projects/secrethub-test-1234567890.iam/locations/global/keyRings/test/cryptoKeys/test" 19 | defaultPlaintext := "plaintext" 20 | 21 | cases := map[string]struct { 22 | input *api.EncryptedData 23 | plaintext string 24 | decryptErr error 25 | expected []byte 26 | expectedErr error 27 | }{ 28 | "success": { 29 | input: api.NewEncryptedDataGCPKMS(defaultCiphertext, api.NewEncryptionKeyGCP(defaultKMSKey)), 30 | plaintext: defaultPlaintext, 31 | expected: []byte(defaultPlaintext), 32 | }, 33 | "invalid EncryptedData": { 34 | input: api.NewEncryptedDataAESGCM(defaultCiphertext, []byte("nonce"), 10, api.NewEncryptionKeyLocal(256)), 35 | expectedErr: api.ErrInvalidKeyType, 36 | }, 37 | "decryption error": { 38 | input: api.NewEncryptedDataGCPKMS(defaultCiphertext, api.NewEncryptionKeyGCP(defaultKMSKey)), 39 | decryptErr: errTest, 40 | expectedErr: errTest, 41 | }, 42 | } 43 | 44 | for name, tc := range cases { 45 | t.Run(name, func(t *testing.T) { 46 | decrypter := KMSDecrypter{ 47 | decryptFunc: func(name string, ciphertext []byte) (*kms.DecryptResponse, error) { 48 | assert.Equal(t, ciphertext, defaultCiphertext) 49 | return &kms.DecryptResponse{ 50 | Plaintext: []byte(tc.plaintext), 51 | }, tc.decryptErr 52 | }, 53 | } 54 | 55 | res, err := decrypter.Unwrap(tc.input) 56 | assert.Equal(t, err, tc.expectedErr) 57 | if tc.expectedErr == nil { 58 | assert.Equal(t, res, tc.expected) 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internals/gcp/service_creator.go: -------------------------------------------------------------------------------- 1 | package gcp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | kms "cloud.google.com/go/kms/apiv1" 8 | "google.golang.org/api/option" 9 | kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" 10 | 11 | "github.com/secrethub/secrethub-go/internals/api" 12 | ) 13 | 14 | // CredentialCreator is an implementation of the secrethub.Verifier and secrethub.Encrypter interface that can be used 15 | // to create an GCP service account. 16 | type CredentialCreator struct { 17 | keyResourceID string 18 | serviceAccountEmail string 19 | 20 | encryptFunc func(name string, plaintext []byte) (*kmspb.EncryptResponse, error) 21 | } 22 | 23 | // NewCredentialCreator returns a CredentialCreator that uses the provided GCP KMS key and Service Account Email to create a new credential. 24 | // The GCP client is configured with the optionally provided option.ClientOption. 25 | func NewCredentialCreator(serviceAccountEmail, keyResourceID string, gcpOptions ...option.ClientOption) (*CredentialCreator, map[string]string, error) { 26 | if err := api.ValidateGCPUserManagedServiceAccountEmail(serviceAccountEmail); err != nil { 27 | return nil, nil, err 28 | } 29 | if err := api.ValidateGCPKMSKeyResourceID(keyResourceID); err != nil { 30 | return nil, nil, err 31 | } 32 | 33 | kmsClient, err := kms.NewKeyManagementClient(context.Background(), gcpOptions...) 34 | if err != nil { 35 | return nil, nil, fmt.Errorf("creating kms client: %v", HandleError(err)) 36 | } 37 | 38 | return &CredentialCreator{ 39 | keyResourceID: keyResourceID, 40 | serviceAccountEmail: serviceAccountEmail, 41 | encryptFunc: func(name string, plaintext []byte) (*kmspb.EncryptResponse, error) { 42 | return kmsClient.Encrypt(context.Background(), &kmspb.EncryptRequest{ 43 | Name: name, 44 | Plaintext: plaintext, 45 | }) 46 | }, 47 | }, map[string]string{ 48 | api.CredentialMetadataGCPKMSKeyResourceID: keyResourceID, 49 | api.CredentialMetadataGCPServiceAccountEmail: serviceAccountEmail, 50 | }, nil 51 | } 52 | 53 | func (c CredentialCreator) Wrap(plaintext []byte) (*api.EncryptedData, error) { 54 | resp, err := c.encryptFunc(c.keyResourceID, plaintext) 55 | if err != nil { 56 | return nil, HandleError(err) 57 | } 58 | return api.NewEncryptedDataGCPKMS(resp.Ciphertext, api.NewEncryptionKeyGCP(c.keyResourceID)), nil 59 | } 60 | 61 | func (c CredentialCreator) Export() ([]byte, string, error) { 62 | verifierBytes := []byte(c.serviceAccountEmail) 63 | return verifierBytes, api.GetFingerprint(api.CredentialTypeGCPServiceAccount, verifierBytes), nil 64 | } 65 | 66 | func (c CredentialCreator) Type() api.CredentialType { 67 | return api.CredentialTypeGCPServiceAccount 68 | } 69 | 70 | func (c CredentialCreator) AddProof(req *api.CreateCredentialRequest) error { 71 | req.Proof = &api.CredentialProofGCPServiceAccount{} 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /internals/gcp/service_creator_test.go: -------------------------------------------------------------------------------- 1 | package gcp 2 | 3 | import ( 4 | "testing" 5 | 6 | kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" 7 | 8 | "github.com/secrethub/secrethub-go/internals/api" 9 | 10 | "github.com/secrethub/secrethub-go/internals/assert" 11 | ) 12 | 13 | func TestServiceCreator_Wrap(t *testing.T) { 14 | kmsKeyID := "123456" 15 | ciphertext := []byte("ciphertext") 16 | 17 | cases := map[string]struct { 18 | encryptErr error 19 | 20 | expectedErr error 21 | expected *api.EncryptedData 22 | }{ 23 | "success": { 24 | expected: api.NewEncryptedDataGCPKMS(ciphertext, api.NewEncryptionKeyGCP(kmsKeyID)), 25 | }, 26 | "encrypt error": { 27 | encryptErr: errTest, 28 | expectedErr: errTest, 29 | }, 30 | } 31 | 32 | for name, tc := range cases { 33 | t.Run(name, func(t *testing.T) { 34 | plaintext := []byte("plaintext") 35 | 36 | sc := CredentialCreator{ 37 | encryptFunc: func(name string, pt []byte) (*kmspb.EncryptResponse, error) { 38 | assert.Equal(t, name, kmsKeyID) 39 | assert.Equal(t, pt, plaintext) 40 | return &kmspb.EncryptResponse{ 41 | Ciphertext: ciphertext, 42 | }, tc.encryptErr 43 | }, 44 | } 45 | sc.keyResourceID = kmsKeyID 46 | 47 | res, err := sc.Wrap(plaintext) 48 | assert.Equal(t, err, tc.expectedErr) 49 | 50 | if tc.expectedErr == nil { 51 | assert.Equal(t, res, tc.expected) 52 | } 53 | 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internals/oauthorizer/authorizer.go: -------------------------------------------------------------------------------- 1 | package oauthorizer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | type Authorizer interface { 12 | AuthorizeLink(redirectURI string, state string) string 13 | ParseResponse(r *http.Request, state string) (string, error) 14 | } 15 | 16 | func NewAuthorizer(authURI, clientID string, scopes ...string) Authorizer { 17 | return authorizer{ 18 | AuthURI: authURI, 19 | ClientID: clientID, 20 | Scopes: scopes, 21 | } 22 | } 23 | 24 | type authorizer struct { 25 | AuthURI string 26 | ClientID string 27 | Scopes []string 28 | } 29 | 30 | func (a authorizer) AuthorizeLink(redirectURI string, state string) string { 31 | return fmt.Sprintf(`%s?`+ 32 | `scope=%s&`+ 33 | `access_type=online&`+ 34 | `response_type=code&`+ 35 | `redirect_uri=%s&`+ 36 | `state=%s&`+ 37 | `prompt=select_account&`+ 38 | `client_id=%s`, 39 | a.AuthURI, 40 | url.QueryEscape(strings.Join(a.Scopes, ",")), 41 | url.QueryEscape(redirectURI), 42 | state, 43 | a.ClientID, 44 | ) 45 | } 46 | 47 | func (a authorizer) ParseResponse(r *http.Request, expectedState string) (string, error) { 48 | errorMessage := r.URL.Query().Get("error") 49 | if errorMessage != "" { 50 | return "", fmt.Errorf("authorization error: %s", errorMessage) 51 | } 52 | 53 | state := r.URL.Query().Get("state") 54 | if state == "" { 55 | return "", errors.New("missing state query parameter") 56 | } 57 | if state != expectedState { 58 | return "", errors.New("state does not match") 59 | } 60 | 61 | code := r.URL.Query().Get("code") 62 | if code == "" { 63 | return "", errors.New("missing code query parameter") 64 | } 65 | return code, nil 66 | } 67 | -------------------------------------------------------------------------------- /internals/oauthorizer/callback_handler.go: -------------------------------------------------------------------------------- 1 | package oauthorizer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "sync" 10 | 11 | "github.com/secrethub/secrethub-go/pkg/randchar" 12 | ) 13 | 14 | type CallbackHandler struct { 15 | authorizer Authorizer 16 | listener net.Listener 17 | state string 18 | 19 | baseRedirectURL *url.URL 20 | 21 | errChan chan error 22 | } 23 | 24 | func NewCallbackHandler(redirectURL *url.URL, authorizer Authorizer) (CallbackHandler, error) { 25 | state, err := randchar.Generate(20) 26 | if err != nil { 27 | return CallbackHandler{}, fmt.Errorf("generating random state: %s", err) 28 | } 29 | 30 | l, err := net.Listen("tcp", "127.0.0.1:") 31 | if err != nil { 32 | return CallbackHandler{}, err 33 | } 34 | 35 | return CallbackHandler{ 36 | authorizer: authorizer, 37 | baseRedirectURL: redirectURL, 38 | listener: l, 39 | state: string(state), 40 | errChan: make(chan error, 1), 41 | }, nil 42 | } 43 | 44 | func (s CallbackHandler) ListenURL() string { 45 | return "http://" + s.listener.Addr().String() 46 | } 47 | 48 | func (s CallbackHandler) AuthorizeURL() string { 49 | return s.authorizer.AuthorizeLink(s.ListenURL(), s.state) 50 | } 51 | 52 | // WithAuthorizationCode executes the provided function with the resulting authorization code or error. 53 | // Afterwards the user is redirected to the CallbackHandler's baseRedirectURL. If the callback produced an error, 54 | // the error is appended to the redirect url: &error=. 55 | // The provided callback function will only be executed once, even if multiple successful callbacks arrive at the server. 56 | // This function returns when the callback has been executed and the user is redirected. 57 | func (s CallbackHandler) WithAuthorizationCode(callback func(string) error) error { 58 | defer s.listener.Close() 59 | 60 | ctx, cancel := context.WithCancel(context.Background()) 61 | 62 | go func() { 63 | s.errChan <- http.Serve(s.listener, http.HandlerFunc(s.handleRequest(callback, cancel))) 64 | cancel() 65 | }() 66 | 67 | <-ctx.Done() 68 | 69 | select { 70 | case err := <-s.errChan: 71 | return err 72 | default: 73 | return nil 74 | } 75 | } 76 | 77 | func (s CallbackHandler) handleRequest(callback func(string) error, done func()) func(w http.ResponseWriter, r *http.Request) { 78 | var once sync.Once 79 | var redirectURL *url.URL 80 | return func(w http.ResponseWriter, r *http.Request) { 81 | once.Do(func() { 82 | redirectURL = s.baseRedirectURL 83 | err := func() error { 84 | code, err := s.authorizer.ParseResponse(r, s.state) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | err = callback(code) 90 | if err != nil { 91 | return err 92 | } 93 | return nil 94 | }() 95 | if err != nil { 96 | q := redirectURL.Query() 97 | q.Set("error", err.Error()) 98 | redirectURL.RawQuery = q.Encode() 99 | s.errChan <- err 100 | } 101 | }) 102 | 103 | http.Redirect(w, r, redirectURL.String(), http.StatusSeeOther) 104 | done() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pkg/randchar/example_test.go: -------------------------------------------------------------------------------- 1 | package randchar_test 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/secrethub/secrethub-go/pkg/randchar" 7 | ) 8 | 9 | // Generate a random slice of 30 alphanumeric characters. 10 | func ExampleRand_Generate() { 11 | val, err := randchar.Generate(30) 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | print(string(val)) 16 | } 17 | 18 | // Generate a 15 character alphanumeric string with at least 3 symbols, 1 uppercase letter, 19 | // 1 lowercase letter and 1 digit. 20 | func ExampleRand_Generate_withRules() { 21 | symbolsRule := randchar.Min(3, randchar.Symbols) 22 | uppercaseRule := randchar.Min(1, randchar.Uppercase) 23 | lowercaseRule := randchar.Min(1, randchar.Lowercase) 24 | numberRule := randchar.Min(1, randchar.Numeric) 25 | 26 | rand, err := randchar.NewRand(randchar.All, symbolsRule, uppercaseRule, lowercaseRule, numberRule) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | val, err := rand.Generate(15) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | print(string(val)) 36 | } 37 | 38 | // Generate a 10 character alphanumeric string containing lowercase letters and digits. 39 | func ExampleRand_Generate_combineCharsets() { 40 | customCharset := randchar.Lowercase.Add(randchar.Numeric) 41 | rand, err := randchar.NewRand(customCharset) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | val, err := rand.Generate(10) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | print(string(val)) 51 | } 52 | 53 | // Generate an 8 character long hexadecimal string. 54 | func ExampleRand_Generate_customCharset() { 55 | hexCharset := randchar.NewCharset("0123456789ABCDEF") 56 | rand, err := randchar.NewRand(hexCharset) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | val, err := rand.Generate(8) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | print(string(val)) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/randchar/fakes/generator.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | // Package fakes provides mock implementations to be used in testing. 4 | package fakes 5 | 6 | // FakeRandomGenerator can be used to mock a RandomGenerator. 7 | type FakeRandomGenerator struct { 8 | Ret []byte 9 | Err error 10 | } 11 | 12 | // Generate returns the mocked Generate response. 13 | func (generator FakeRandomGenerator) Generate(length int) ([]byte, error) { 14 | return generator.Ret, generator.Err 15 | } 16 | -------------------------------------------------------------------------------- /pkg/secrethub/account_key.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/internals/crypto" 6 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" 7 | ) 8 | 9 | // DefaultAccountKeyLength defines the default bit size for account keys. 10 | const DefaultAccountKeyLength = 4096 11 | 12 | func generateAccountKey() (crypto.RSAPrivateKey, error) { 13 | return crypto.GenerateRSAPrivateKey(DefaultAccountKeyLength) 14 | } 15 | 16 | // AccountKeyService handles operations on SecretHub account keys. 17 | type AccountKeyService interface { 18 | // Create creates an account key for the client's credential. 19 | Create(verifier credentials.Verifier, encrypter credentials.Encrypter) (*api.EncryptedAccountKey, error) 20 | // Exists returns whether an account key exists for the client's credential. 21 | Exists() (bool, error) 22 | } 23 | 24 | type accountKeyService struct { 25 | client *Client 26 | } 27 | 28 | // newAccountKeyService creates a new accountKeyService 29 | func newAccountKeyService(client *Client) accountKeyService { 30 | return accountKeyService{ 31 | client: client, 32 | } 33 | } 34 | 35 | // Create creates an account key for the clients credential. 36 | func (s accountKeyService) Create(verifier credentials.Verifier, encrypter credentials.Encrypter) (*api.EncryptedAccountKey, error) { 37 | _, fingerprint, err := verifier.Export() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | key, err := generateAccountKey() 43 | if err != nil { 44 | return nil, err 45 | } 46 | return s.client.createAccountKey(fingerprint, key, encrypter) 47 | } 48 | 49 | // Exists returns whether an account key exists for the client's credential. 50 | func (s accountKeyService) Exists() (bool, error) { 51 | _, err := s.client.getAccountKey() 52 | if api.IsErrNotFound(err) { 53 | return false, nil 54 | } 55 | if err != nil { 56 | return false, err 57 | } 58 | return true, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/secrethub/audit.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/internals/errio" 6 | "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" 8 | ) 9 | 10 | func (c *Client) decryptAuditEvents(events ...*api.Audit) error { 11 | accountKey, err := c.getAccountKey() 12 | if err != nil { 13 | return errio.Error(err) 14 | } 15 | 16 | // Decrypt all Secret names 17 | for _, event := range events { 18 | if event.Subject.Deleted { 19 | continue 20 | } 21 | 22 | if event.Subject.Type == api.AuditSubjectSecret || event.Subject.Type == api.AuditSubjectSecretMember { 23 | event.Subject.Secret, err = event.Subject.EncryptedSecret.Decrypt(accountKey) 24 | if err != nil { 25 | return errio.Error(err) 26 | } 27 | } else if event.Subject.Type == api.AuditSubjectSecretVersion { 28 | event.Subject.SecretVersion, err = event.Subject.EncryptedSecretVersion.Decrypt(accountKey) 29 | if err != nil { 30 | return errio.Error(err) 31 | } 32 | } 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func newAuditEventIterator(newPaginator func() (*http.AuditPaginator, error), client *Client) *auditEventIterator { 39 | return &auditEventIterator{ 40 | iterator: iterator.New(func() (iterator.Paginator, error) { 41 | return newPaginator() 42 | }), 43 | decryptAuditEvents: client.decryptAuditEvents, 44 | } 45 | } 46 | 47 | type AuditEventIterator interface { 48 | Next() (api.Audit, error) 49 | } 50 | 51 | type auditEventIterator struct { 52 | iterator iterator.Iterator 53 | decryptAuditEvents func(...*api.Audit) error 54 | } 55 | 56 | func (it *auditEventIterator) Next() (api.Audit, error) { 57 | item, err := it.iterator.Next() 58 | if err != nil { 59 | return api.Audit{}, err 60 | } 61 | audit := item.(api.Audit) 62 | err = it.decryptAuditEvents(&audit) 63 | if err != nil { 64 | return api.Audit{}, err 65 | } 66 | return audit, nil 67 | } 68 | 69 | // AuditEventIteratorParams can be used to configure iteration of audit events. 70 | // 71 | // For now, there's nothing to configure. We'll add filter options soon. 72 | // The struct is already added, so that adding parameters is backwards compatible. 73 | type AuditEventIteratorParams struct{} 74 | -------------------------------------------------------------------------------- /pkg/secrethub/audit_test.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/internals/api/uuid" 8 | "github.com/secrethub/secrethub-go/internals/assert" 9 | "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" 10 | ) 11 | 12 | type fakeAuditPaginator struct { 13 | events []api.Audit 14 | returned bool 15 | } 16 | 17 | func (pag *fakeAuditPaginator) Next() ([]interface{}, error) { 18 | if pag.returned { 19 | return []interface{}{}, nil 20 | } 21 | 22 | res := make([]interface{}, len(pag.events)) 23 | for i, event := range pag.events { 24 | res[i] = event 25 | } 26 | pag.returned = true 27 | return res, nil 28 | } 29 | 30 | func TestAuditEventIterator_Next(t *testing.T) { 31 | events := []api.Audit{ 32 | { 33 | EventID: uuid.New(), 34 | Action: api.AuditActionRead, 35 | }, 36 | } 37 | 38 | iter := auditEventIterator{ 39 | iterator: iterator.New(func() (iterator.Paginator, error) { 40 | return &fakeAuditPaginator{events: events}, nil 41 | }), 42 | decryptAuditEvents: func(audit ...*api.Audit) error { 43 | return nil 44 | }, 45 | } 46 | 47 | for _, event := range events { 48 | actual, err := iter.Next() 49 | 50 | assert.Equal(t, err, nil) 51 | assert.Equal(t, actual, event) 52 | } 53 | _, err := iter.Next() 54 | assert.Equal(t, err, iterator.Done) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/secrethub/client_options.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "time" 7 | 8 | "github.com/secrethub/secrethub-go/pkg/secrethub/configdir" 9 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" 10 | httpclient "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 11 | ) 12 | 13 | // Errors 14 | var ( 15 | ErrInvalidServerURL = errClient.Code("invalid_server_url").ErrorPref("%s") 16 | ) 17 | 18 | // ClientOption is an option that can be set on a secrethub.Client. 19 | type ClientOption func(*Client) error 20 | 21 | // WithTimeout overrides the default request timeout of the HTTP client. 22 | func WithTimeout(timeout time.Duration) ClientOption { 23 | return func(c *Client) error { 24 | c.httpClient.Options(httpclient.WithTimeout(timeout)) 25 | return nil 26 | } 27 | } 28 | 29 | // WithServerURL overrides the default server endpoint URL used by the HTTP client. 30 | func WithServerURL(serverURL string) ClientOption { 31 | return func(c *Client) error { 32 | parsedURL, err := url.Parse(serverURL) 33 | if err != nil { 34 | return ErrInvalidServerURL(err) 35 | } 36 | 37 | c.httpClient.Options(httpclient.WithServerURL(*parsedURL)) 38 | return nil 39 | } 40 | } 41 | 42 | // WithTransport replaces the DefaultTransport used by the HTTP client with the provided RoundTripper. 43 | func WithTransport(transport http.RoundTripper) ClientOption { 44 | return func(c *Client) error { 45 | c.httpClient.Options(httpclient.WithTransport(transport)) 46 | return nil 47 | } 48 | } 49 | 50 | // WithAppInfo sets the AppInfo to be used for identifying the application that is using the SecretHub Client. 51 | func WithAppInfo(appInfo *AppInfo) ClientOption { 52 | return func(c *Client) error { 53 | if err := appInfo.ValidateName(); err != nil { 54 | return err 55 | } 56 | c.appInfo = append(c.appInfo, appInfo) 57 | return nil 58 | } 59 | } 60 | 61 | // WithConfigDir sets the configuration directory to use (among others) for sourcing the credential file from. 62 | func WithConfigDir(configDir configdir.Dir) ClientOption { 63 | return func(c *Client) error { 64 | c.ConfigDir = &configDir 65 | return nil 66 | } 67 | } 68 | 69 | // WithCredentials sets the credential to be used for authenticating to the API and decrypting the account key. 70 | func WithCredentials(provider credentials.Provider) ClientOption { 71 | return func(c *Client) error { 72 | authenticator, decrypter, err := provider.Provide(c.httpClient) 73 | if err != nil { 74 | return err 75 | } 76 | c.decrypter = decrypter 77 | c.httpClient.Options(httpclient.WithAuthenticator(authenticator)) 78 | return nil 79 | } 80 | } 81 | 82 | // WithDefaultPassphraseReader sets a default passphrase reader that is used for decrypting an encrypted key credential 83 | // if no credential is set explicitly. 84 | func WithDefaultPassphraseReader(reader credentials.Reader) ClientOption { 85 | return func(c *Client) error { 86 | c.defaultPassphraseReader = reader 87 | return nil 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /pkg/secrethub/client_test.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/secrethub/secrethub-go/internals/assert" 9 | ) 10 | 11 | func TestClient_userAgent(t *testing.T) { 12 | cases := map[string]struct { 13 | appInfo []*AppInfo 14 | envAppName string 15 | envAppVersion string 16 | expected string 17 | err error 18 | }{ 19 | "default": {}, 20 | "multiple app info layers": { 21 | appInfo: []*AppInfo{ 22 | {Name: "secrethub-xgo", Version: "0.1.0"}, 23 | {Name: "secrethub-java", Version: "0.2.0"}, 24 | }, 25 | expected: "secrethub-xgo/0.1.0 secrethub-java/0.2.0", 26 | }, 27 | "no version number": { 28 | appInfo: []*AppInfo{ 29 | {Name: "terraform-provider-secrethub"}, 30 | }, 31 | expected: "terraform-provider-secrethub", 32 | }, 33 | "top level app info from environment": { 34 | appInfo: []*AppInfo{ 35 | {Name: "secrethub-cli", Version: "0.37.0"}, 36 | }, 37 | envAppName: "secrethub-circleci-orb", 38 | envAppVersion: "1.0.0", 39 | expected: "secrethub-cli/0.37.0 secrethub-circleci-orb/1.0.0", 40 | }, 41 | "invalid app name": { 42 | appInfo: []*AppInfo{ 43 | {Name: "illegal-name*%!@", Version: "0.1.0"}, 44 | }, 45 | err: ErrInvalidAppInfoName, 46 | }, 47 | "ignore faulty environment variable": { 48 | appInfo: []*AppInfo{ 49 | {Name: "secrethub-cli", Version: "0.37.0"}, 50 | }, 51 | envAppName: "illegal-name*%!@", 52 | expected: "secrethub-cli/0.37.0", 53 | }, 54 | } 55 | 56 | for name, tc := range cases { 57 | t.Run(name, func(t *testing.T) { 58 | os.Setenv("SECRETHUB_APP_INFO_NAME", tc.envAppName) 59 | os.Setenv("SECRETHUB_APP_INFO_VERSION", tc.envAppVersion) 60 | 61 | var opts []ClientOption 62 | for _, info := range tc.appInfo { 63 | opts = append(opts, WithAppInfo(info)) 64 | } 65 | client := &Client{} 66 | err := client.with(opts...) 67 | assert.Equal(t, err, tc.err) 68 | 69 | client.loadAppInfoFromEnv() 70 | 71 | userAgent := client.userAgent() 72 | pattern := tc.expected + " \\(.*\\)" 73 | matched, err := regexp.MatchString(pattern, userAgent) 74 | assert.OK(t, err) 75 | if !matched { 76 | t.Errorf("user agent '%s' doesn't match pattern '%s'", userAgent, pattern) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/secrethub/client_version.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | // ClientVersion is the current version of the client 4 | // Do not edit this unless you know what you're doing. 5 | const ClientVersion = "v0.33.0" 6 | -------------------------------------------------------------------------------- /pkg/secrethub/configdir/dir.go: -------------------------------------------------------------------------------- 1 | // Package configdir provides simple functions to manage the SecretHub 2 | // configuration directory. 3 | package configdir 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/mitchellh/go-homedir" 13 | ) 14 | 15 | var ( 16 | // ErrCredentialNotFound is returned when a credential file does not exist but CredentialFile.Read() is called. 17 | ErrCredentialNotFound = errors.New("credential not found") 18 | ) 19 | 20 | // Dir represents the configuration directory located at some path 21 | // on the file system. 22 | type Dir struct { 23 | path string 24 | } 25 | 26 | // New a new Dir which represents a configuration directory at the given location. 27 | func New(path string) Dir { 28 | return Dir{ 29 | path: path, 30 | } 31 | } 32 | 33 | // Default is the default way to get the location of the SecretHub 34 | // configuration directory, sourcing it from the environment variable 35 | // SECRETHUB_CONFIG_DIR or falling back to the ~/.secrethub directory. 36 | func Default() (*Dir, error) { 37 | envDir := os.Getenv("SECRETHUB_CONFIG_DIR") 38 | if envDir != "" { 39 | return &Dir{ 40 | path: envDir, 41 | }, nil 42 | } 43 | 44 | homeDir, err := homedir.Dir() 45 | if err != nil { 46 | return &Dir{}, fmt.Errorf("cannot get home directory: %v", err) 47 | } 48 | return &Dir{ 49 | path: filepath.Join(homeDir, ".secrethub"), 50 | }, nil 51 | } 52 | 53 | // Credential returns the file that contains the SecretHub API credential. 54 | func (c Dir) Credential() *CredentialFile { 55 | return &CredentialFile{ 56 | path: filepath.Join(c.path, "credential"), 57 | } 58 | } 59 | 60 | // Path returns the path on the filesystem at which the config directory is located. 61 | func (c Dir) Path() string { 62 | return c.path 63 | } 64 | 65 | func (c Dir) String() string { 66 | return c.path 67 | } 68 | 69 | // CredentialFile represents the file that contains the SecretHub API credential. 70 | // By default, it's a file named "credential" in the configuration directory. 71 | type CredentialFile struct { 72 | path string 73 | } 74 | 75 | // Path returns the path on the filesystem at which the credential file is located. 76 | func (f *CredentialFile) Path() string { 77 | return f.path 78 | } 79 | 80 | // Source returns the path to the credential file. 81 | func (f *CredentialFile) Source() string { 82 | return f.path 83 | } 84 | 85 | // Write writes the given bytes to the credential file. 86 | func (f *CredentialFile) Write(data []byte) error { 87 | err := os.MkdirAll(filepath.Dir(f.path), os.FileMode(0700)) 88 | if err != nil { 89 | return err 90 | } 91 | return ioutil.WriteFile(f.path, data, os.FileMode(0600)) 92 | } 93 | 94 | // Exists returns true when a file exists at the path this credential points to. 95 | func (f *CredentialFile) Exists() bool { 96 | if _, err := os.Stat(f.path); os.IsNotExist(err) { 97 | return false 98 | } 99 | return true 100 | } 101 | 102 | // Read reads from the filesystem and returns the contents of the credential file. 103 | func (f *CredentialFile) Read() ([]byte, error) { 104 | file, err := os.Open(f.path) 105 | if os.IsNotExist(err) { 106 | return nil, ErrCredentialNotFound 107 | } else if err != nil { 108 | return nil, err 109 | } 110 | return ioutil.ReadAll(file) 111 | } 112 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/aws.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | awssdk "github.com/aws/aws-sdk-go/aws" 5 | 6 | "github.com/secrethub/secrethub-go/internals/auth" 7 | "github.com/secrethub/secrethub-go/internals/aws" 8 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials/sessions" 9 | "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 10 | ) 11 | 12 | // UseAWS returns a Provider that can be used to use an assumed AWS role as a credential for SecretHub. 13 | // The provided awsCfg is used to configure the AWS client. 14 | // If used on AWS (e.g. from an EC2-instance), this extra configuration is not required and the correct configuration 15 | // should be auto-detected by the AWS client. 16 | // 17 | // Usage: 18 | // credentials.UseAWS() 19 | // credentials.UseAWS(&aws.Config{Region: aws.String("eu-west-1")}) 20 | func UseAWS(awsCfg ...*awssdk.Config) Provider { 21 | return providerFunc(func(httpClient *http.Client) (auth.Authenticator, Decrypter, error) { 22 | decrypter, err := aws.NewKMSDecrypter(awsCfg...) 23 | if err != nil { 24 | return nil, nil, err 25 | } 26 | authenticator := sessions.NewSessionRefresher(httpClient, sessions.NewAWSSessionCreator(awsCfg...)) 27 | return authenticator, decrypter, nil 28 | }) 29 | } 30 | 31 | // CreateAWS returns a Creator that creates an AWS-based credential. 32 | // The kmsKeyID is the ID of the key in KMS that is used to encrypt the account key. 33 | // The roleARN is for the IAM role that should be assumed to use this credential. 34 | // The role should have decryption permission on the provided KMS key. 35 | // awsCfg can be used to optionally configure the used AWS client. For example to set the region. 36 | // The KMS key id and role are returned in the credentials metadata. 37 | func CreateAWS(kmsKeyID string, roleARN string, awsCfg ...*awssdk.Config) Creator { 38 | return &awsCreator{ 39 | kmsKeyID: kmsKeyID, 40 | roleARN: roleARN, 41 | awsCfg: awsCfg, 42 | } 43 | } 44 | 45 | type awsCreator struct { 46 | kmsKeyID string 47 | roleARN string 48 | awsCfg []*awssdk.Config 49 | 50 | credentialCreator *aws.CredentialCreator 51 | metadata map[string]string 52 | } 53 | 54 | func (ac *awsCreator) Create() error { 55 | creator, metadata, err := aws.NewCredentialCreator(ac.kmsKeyID, ac.roleARN, ac.awsCfg...) 56 | if err != nil { 57 | return err 58 | } 59 | ac.credentialCreator = creator 60 | ac.metadata = metadata 61 | return nil 62 | } 63 | 64 | func (ac *awsCreator) Verifier() Verifier { 65 | return ac.credentialCreator 66 | } 67 | 68 | func (ac *awsCreator) Encrypter() Encrypter { 69 | return ac.credentialCreator 70 | } 71 | 72 | func (ac *awsCreator) Metadata() map[string]string { 73 | return ac.metadata 74 | } 75 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/creators.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/crypto" 5 | ) 6 | 7 | // Creator is an interface is accepted by functions that need a new credential to be created. 8 | type Creator interface { 9 | // Create creates the actual credential (e.g. by generating a key). 10 | Create() error 11 | // Verifier returns information that the server can use to verify a request authenticated with the credential. 12 | Verifier() Verifier 13 | // Encrypter returns a wrapper that is used to encrypt data, typically an account key. 14 | Encrypter() Encrypter 15 | // Metadata returns a set of metadata about the credential. The result can be empty if no metadata is provided. 16 | Metadata() map[string]string 17 | } 18 | 19 | // CreateKey returns a Creator that creates a key based credential. 20 | // After use, the key can be accessed with the Export() method. 21 | // The user of CreateKey() is responsible for saving the exported key. 22 | // If this is not done, the credential will be unusable. 23 | func CreateKey() *KeyCreator { 24 | return &KeyCreator{} 25 | } 26 | 27 | // KeyCreator is used to create a new key-based credential. 28 | type KeyCreator struct { 29 | Key 30 | } 31 | 32 | // Create generates a new key and stores it in the KeyCreator. 33 | func (kc *KeyCreator) Create() error { 34 | key, err := GenerateRSACredential(crypto.RSAKeyLength) 35 | if err != nil { 36 | return err 37 | } 38 | kc.key = key 39 | return nil 40 | } 41 | 42 | // Metadata returns a set of metadata associated with this credential. 43 | func (kc *KeyCreator) Metadata() map[string]string { 44 | return map[string]string{} 45 | } 46 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/creators_test.go: -------------------------------------------------------------------------------- 1 | package credentials_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | 8 | "github.com/secrethub/secrethub-go/pkg/secrethub" 9 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" 10 | ) 11 | 12 | func ExampleCreateKey() { 13 | client := secrethub.Must(secrethub.NewClient()) 14 | 15 | credential := credentials.CreateKey() 16 | _, err := client.Services().Create("my/repo", "description", credential) 17 | if err != nil { 18 | // handle error 19 | } 20 | key, err := credential.Export() 21 | if err != nil { 22 | // handle error 23 | } 24 | fmt.Printf("The key credential of the service is:\n%s", key) 25 | } 26 | 27 | func ExampleCreateAWS() { 28 | client := secrethub.Must(secrethub.NewClient()) 29 | 30 | credential := credentials.CreateAWS("1234abcd-12ab-34cd-56ef-1234567890ab", "MyIAMRole") 31 | _, err := client.Services().Create("my/repo", "description", credential) 32 | if err != nil { 33 | // handle error 34 | } 35 | } 36 | 37 | func ExampleCreateAWS_setRegion() { 38 | client := secrethub.Must(secrethub.NewClient()) 39 | 40 | credential := credentials.CreateAWS( 41 | "1234abcd-12ab-34cd-56ef-1234567890ab", 42 | "MyIAMRole", 43 | &aws.Config{Region: aws.String("eu-west-1")}) 44 | _, err := client.Services().Create("my/repo", "description", credential) 45 | if err != nil { 46 | // handle error 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/credentials.go: -------------------------------------------------------------------------------- 1 | // Package credentials provides utilities for managing SecretHub API credentials. 2 | package credentials 3 | 4 | import "github.com/secrethub/secrethub-go/internals/api" 5 | 6 | // Verifier exports verification bytes that can be used to verify signed data is processed by the owner of a signer. 7 | type Verifier interface { 8 | // Export returns the data to be stored server side to verify an http request authenticated with this credential. 9 | Export() (verifierBytes []byte, fingerprint string, err error) 10 | // Type returns what type of credential this is. 11 | Type() api.CredentialType 12 | // AddProof adds the proof of this credential's possession to a CreateCredentialRequest. 13 | AddProof(req *api.CreateCredentialRequest) error 14 | } 15 | 16 | // Decrypter decrypts data, typically an account key. 17 | type Decrypter interface { 18 | // Unwrap decrypts data, typically an account key. 19 | Unwrap(ciphertext *api.EncryptedData) ([]byte, error) 20 | } 21 | 22 | // Encrypter encrypts data, typically an account key. 23 | type Encrypter interface { 24 | // Wrap encrypts data, typically an account key. 25 | Wrap(plaintext []byte) (*api.EncryptedData, error) 26 | } 27 | 28 | // CreatorProvider is both a credential creator and provider. 29 | type CreatorProvider interface { 30 | Creator 31 | Provider 32 | } 33 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/gcp.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "google.golang.org/api/option" 5 | 6 | "github.com/secrethub/secrethub-go/internals/auth" 7 | "github.com/secrethub/secrethub-go/internals/gcp" 8 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials/sessions" 9 | "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 10 | ) 11 | 12 | // UseGCPServiceAccount returns a Provider that can be used to use a GCP Service Account as a credential for SecretHub. 13 | // The provided gcpOptions is used to configure the GCP client. 14 | // If used on GCP (e.g. from a Compute Engine instance), this extra configuration is not required and the correct 15 | // configuration should be auto-detected by the GCP client. 16 | // 17 | // Access to the GCP metadata server is required for this function to work. In practice, this means that it can 18 | // only be run on GCP. 19 | // 20 | // Usage: 21 | // credentials.UseGCPServiceAccount() 22 | func UseGCPServiceAccount(gcpOptions ...option.ClientOption) Provider { 23 | return providerFunc(func(httpClient *http.Client) (auth.Authenticator, Decrypter, error) { 24 | decrypter, err := gcp.NewKMSDecrypter(gcpOptions...) 25 | if err != nil { 26 | return nil, nil, err 27 | } 28 | authenticator := sessions.NewSessionRefresher(httpClient, sessions.NewGCPSessionCreator(gcpOptions...)) 29 | return authenticator, decrypter, nil 30 | }) 31 | } 32 | 33 | // CreateGCPServiceAccount returns a Creator that creates a credential for a GCP Service Account. 34 | // The serviceAccountEmail is the email of the GCP Service Account that can use this SecretHub service account. 35 | // The kmsResourceID is the Resource ID of the key in KMS that is used to encrypt the account key. 36 | // The service account should have decryption permission on the provided KMS key. 37 | // gcpOptions can be used to optionally configure the used GCP client. For example to set a custom API key. 38 | // The KMS key id and service account email are returned in the credentials metadata. 39 | func CreateGCPServiceAccount(serviceAccountEmail string, keyResourceID string, gcpOptions ...option.ClientOption) Creator { 40 | return &gcpServiceAccountCreator{ 41 | keyResourceID: keyResourceID, 42 | serviceAccountEmail: serviceAccountEmail, 43 | gcpOptions: gcpOptions, 44 | } 45 | } 46 | 47 | type gcpServiceAccountCreator struct { 48 | keyResourceID string 49 | serviceAccountEmail string 50 | 51 | gcpOptions []option.ClientOption 52 | 53 | credentialCreator *gcp.CredentialCreator 54 | metadata map[string]string 55 | } 56 | 57 | func (gc *gcpServiceAccountCreator) Create() error { 58 | creator, metadata, err := gcp.NewCredentialCreator(gc.serviceAccountEmail, gc.keyResourceID, gc.gcpOptions...) 59 | if err != nil { 60 | return err 61 | } 62 | gc.metadata = metadata 63 | gc.credentialCreator = creator 64 | return nil 65 | } 66 | 67 | func (gc *gcpServiceAccountCreator) Verifier() Verifier { 68 | return gc.credentialCreator 69 | } 70 | 71 | func (gc *gcpServiceAccountCreator) Encrypter() Encrypter { 72 | return gc.credentialCreator 73 | } 74 | 75 | func (gc *gcpServiceAccountCreator) Metadata() map[string]string { 76 | return gc.metadata 77 | } 78 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/key.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/secrethub/secrethub-go/internals/auth" 9 | "github.com/secrethub/secrethub-go/internals/crypto" 10 | "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 11 | ) 12 | 13 | type ErrLoadingCredential struct { 14 | Location string 15 | Err error 16 | } 17 | 18 | func (e ErrLoadingCredential) Error() string { 19 | return "load credential " + e.Location + ": " + e.Err.Error() 20 | } 21 | 22 | // Key is a credential that uses a local key for all its operations. 23 | type Key struct { 24 | key *RSACredential 25 | exportPassphrase Reader 26 | } 27 | 28 | // Verifier returns a Verifier that can be used for creating a new credential from this Key. 29 | func (k Key) Verifier() Verifier { 30 | return k.key 31 | } 32 | 33 | // Encrypter returns a Encrypter that can be used to encrypt data with this Key. 34 | func (k Key) Encrypter() Encrypter { 35 | return k.key 36 | } 37 | 38 | // Provide implements the Provider interface for a Key. 39 | func (k Key) Provide(httpClient *http.Client) (auth.Authenticator, Decrypter, error) { 40 | return k.key, k.key, nil 41 | } 42 | 43 | // Passphrase returns a new Key that uses the provided passphraseReader to obtain a passphrase that is used for 44 | // encryption when Export() is called. 45 | func (k Key) Passphrase(passphraseReader Reader) Key { 46 | k.exportPassphrase = passphraseReader 47 | return k 48 | } 49 | 50 | // Export the key of this credential to string format to save for later use. 51 | // If a passphrase was set with Passphrase(), this passphrase is used for encrypting the key. 52 | func (k Key) Export() ([]byte, error) { 53 | if k.key == nil { 54 | return nil, errors.New("key has not yet been generated created. Use KeyCreator before calling Export()") 55 | } 56 | if k.exportPassphrase != nil { 57 | passphrase, err := k.exportPassphrase.Read() 58 | if err != nil { 59 | return nil, err 60 | } 61 | passBasedKey, err := NewPassBasedKey(passphrase) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return EncodeEncryptedCredential(k.key, passBasedKey) 66 | } 67 | return EncodeCredential(k.key) 68 | } 69 | 70 | // ImportKey returns a Key by loading it from the provided credentialReader. 71 | // If the key is encrypted with a passphrase, passphraseReader should be provided. This is used to read a passphrase 72 | // from that is used for decryption. If the passphrase is incorrect, a new passphrase will be read up to 3 times. 73 | func ImportKey(credentialReader, passphraseReader Reader) (Key, error) { 74 | bytes, err := credentialReader.Read() 75 | if err != nil { 76 | return Key{}, err 77 | } 78 | encoded, err := defaultParser.parse(bytes) 79 | if err != nil { 80 | return Key{}, err 81 | } 82 | if encoded.IsEncrypted() { 83 | const credentialPassphraseEnvVar = "SECRETHUB_CREDENTIAL_PASSPHRASE" 84 | envPassphrase := os.Getenv(credentialPassphraseEnvVar) 85 | if envPassphrase != "" { 86 | credential, err := decryptKey([]byte(envPassphrase), encoded) 87 | if err != nil { 88 | if crypto.IsWrongKey(err) { 89 | err = ErrCannotDecryptCredential 90 | } 91 | return Key{}, fmt.Errorf("decrypting credential with passphrase read from $%s: %v", credentialPassphraseEnvVar, err) 92 | } 93 | return Key{key: credential}, nil 94 | } 95 | if passphraseReader == nil { 96 | return Key{}, ErrNeedPassphrase 97 | } 98 | 99 | // Try up to three times to get the correct passphrase. 100 | for i := 0; i < 3; i++ { 101 | passphrase, err := passphraseReader.Read() 102 | if err != nil { 103 | return Key{}, err 104 | } 105 | if len(passphrase) == 0 { 106 | continue 107 | } 108 | 109 | credential, err := decryptKey(passphrase, encoded) 110 | if crypto.IsWrongKey(err) { 111 | continue 112 | } else if err != nil { 113 | return Key{}, err 114 | } 115 | 116 | return Key{key: credential}, nil 117 | } 118 | 119 | return Key{}, ErrCannotDecryptCredential 120 | } 121 | credential, err := encoded.Decode() 122 | if err != nil { 123 | return Key{}, err 124 | } 125 | 126 | return Key{key: credential}, nil 127 | } 128 | 129 | func decryptKey(passphrase []byte, encoded *encodedCredential) (*RSACredential, error) { 130 | key, err := NewPassBasedKey(passphrase) 131 | if err != nil { 132 | return nil, err 133 | } 134 | return encoded.DecodeEncrypted(key) 135 | } 136 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/pass_based_encryption.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/secrethub/secrethub-go/internals/crypto" 7 | "github.com/secrethub/secrethub-go/internals/errio" 8 | ) 9 | 10 | // PassBasedKey can encrypt a Credential into token values. 11 | type PassBasedKey interface { 12 | // Name returns the name of the key derivation algorithm. 13 | Name() string 14 | // Encrypt encrypts a given payload with the passphrase derived key and returns encrypted bytes and header with encryption parameter values. 15 | Encrypt(payload []byte) ([]byte, map[string]interface{}, error) 16 | // Decrypt decrypts a payload with the key and accepts the raw JSON header to read values from. 17 | Decrypt(payload []byte, header []byte) ([]byte, error) 18 | } 19 | 20 | // passbasedKeyHeader is a helper type to help encoding 21 | // and decoding header values for the Scrypt encryption. 22 | type passbasedKeyHeader struct { 23 | KeyLen int `json:"klen"` 24 | Salt []byte `json:"salt"` 25 | N int `json:"n"` 26 | R int `json:"r"` 27 | P int `json:"p"` 28 | Nonce []byte `json:"nonce"` 29 | } 30 | 31 | // passBasedKey wraps an scrypt derived key and implements 32 | // the PassBasedKey interface. 33 | type passBasedKey struct { 34 | key *crypto.ScryptKey 35 | passphrase []byte 36 | } 37 | 38 | // NewPassBasedKey generates a new key from a passphrase. 39 | func NewPassBasedKey(passphrase []byte) (PassBasedKey, error) { 40 | key, err := crypto.GenerateScryptKey(passphrase) 41 | if err != nil { 42 | return nil, errio.Error(err) 43 | } 44 | 45 | return passBasedKey{ 46 | key: key, 47 | passphrase: passphrase, 48 | }, nil 49 | } 50 | 51 | // Encrypt implements the PassBasedKey interface and encrypts a payload, 52 | // returning the encrypted payload and header values. 53 | func (p passBasedKey) Encrypt(payload []byte) ([]byte, map[string]interface{}, error) { 54 | ciphertext, err := p.key.Encrypt(payload, crypto.SaltOperationLocalCredentialEncryption) 55 | if err != nil { 56 | return nil, nil, errio.Error(err) 57 | } 58 | 59 | header := passbasedKeyHeader{ 60 | KeyLen: p.key.KeyLen, 61 | Salt: p.key.Salt, 62 | N: p.key.N, 63 | R: p.key.R, 64 | P: p.key.P, 65 | Nonce: ciphertext.Nonce, 66 | } 67 | raw, err := json.Marshal(header) 68 | if err != nil { 69 | return nil, nil, errio.Error(err) 70 | } 71 | 72 | headerMap := make(map[string]interface{}) 73 | err = json.Unmarshal(raw, &headerMap) 74 | if err != nil { 75 | return nil, nil, errio.Error(err) 76 | } 77 | 78 | return ciphertext.Data, headerMap, nil 79 | } 80 | 81 | // Name implements the PassBasedKey interface. 82 | func (p passBasedKey) Name() string { 83 | return "scrypt" 84 | } 85 | 86 | // Decrypt decrypts an encrypted payload and reads values from the header when necessary. 87 | func (p passBasedKey) Decrypt(payload []byte, rawHeader []byte) ([]byte, error) { 88 | header := passbasedKeyHeader{} 89 | err := json.Unmarshal(rawHeader, &header) 90 | if err != nil { 91 | return nil, errio.Error(err) 92 | } 93 | 94 | key, err := crypto.DeriveScryptKey(p.passphrase, header.Salt, header.N, header.R, header.P, header.KeyLen) 95 | if err != nil { 96 | return nil, errio.Error(err) 97 | } 98 | 99 | return key.Decrypt( 100 | crypto.CiphertextAES{ 101 | Data: payload, 102 | Nonce: header.Nonce, 103 | }, 104 | crypto.SaltOperationLocalCredentialEncryption, 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/providers.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/auth" 5 | "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 6 | ) 7 | 8 | // Provider provides a credential that can be used for authentication and decryption when called. 9 | type Provider interface { 10 | Provide(*http.Client) (auth.Authenticator, Decrypter, error) 11 | } 12 | 13 | // UseKey returns a Provider that reads a key credential from credentialReader. 14 | // If the key credential is encrypted, a passphrase must be set by calling Passphrase on the returned KeyProvider, 15 | // 16 | // Usage: 17 | // credentials.UseKey(credentials.FromString("")) 18 | // credentials.UseKey(credentials.FromFile("/path/to/credential")).Passphrase(credentials.FromString("passphrase")) 19 | func UseKey(credentialReader Reader) KeyProvider { 20 | return KeyProvider{ 21 | credentialReader: credentialReader, 22 | } 23 | } 24 | 25 | // KeyProvider is a Provider that reads a key from a Reader. 26 | // If the key is encrypted with a passphrase, Passphrase() should be called on the KeyProvider to set the Reader that 27 | // provides the passphrase that can be used to decrypt the key. 28 | type KeyProvider struct { 29 | credentialReader Reader 30 | passphraseReader Reader 31 | } 32 | 33 | // Passphrase returns a new Provider that uses the passphraseReader to read a passphrase if the read key is encrypted. 34 | func (k KeyProvider) Passphrase(passphraseReader Reader) Provider { 35 | k.passphraseReader = passphraseReader 36 | return k 37 | } 38 | 39 | // Provide implements the Provider interface for a KeyProvider. 40 | func (k KeyProvider) Provide(httpClient *http.Client) (auth.Authenticator, Decrypter, error) { 41 | key, err := ImportKey(k.credentialReader, k.passphraseReader) 42 | if err != nil { 43 | if source, ok := k.credentialReader.(CredentialSource); ok { 44 | return nil, nil, ErrLoadingCredential{ 45 | Location: source.Source(), 46 | Err: err, 47 | } 48 | } 49 | return nil, nil, err 50 | } 51 | return key.Provide(httpClient) 52 | } 53 | 54 | // providerFunc is a helper type to let any func(*http.Client) (UsableCredential, error) implement the Provider interface. 55 | type providerFunc func(*http.Client) (auth.Authenticator, Decrypter, error) 56 | 57 | // Provide lets providerFunc implement the Provider interface. 58 | func (f providerFunc) Provide(httpClient *http.Client) (auth.Authenticator, Decrypter, error) { 59 | return f(httpClient) 60 | } 61 | 62 | // CredentialSource should be implemented by credential readers to allow returning credential reading errors 63 | // that include the credentials source (e.g. path to credential file, environment variable etc.). 64 | type CredentialSource interface { 65 | Source() string 66 | } 67 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/readers.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | ) 7 | 8 | // Reader helps with reading bytes from a configured source. 9 | type Reader interface { 10 | // Read reads from the reader and returns the resulting bytes. 11 | Read() ([]byte, error) 12 | } 13 | 14 | // FromFile returns a reader that reads the contents of a file, 15 | // e.g. a credential or a passphrase. 16 | func FromFile(path string) Reader { 17 | return newReader(path, func() ([]byte, error) { 18 | return ioutil.ReadFile(path) 19 | }) 20 | } 21 | 22 | // FromEnv returns a reader that reads the contents of an 23 | // environment variable, e.g. a credential or a passphrase. 24 | func FromEnv(key string) Reader { 25 | return newReader("$"+key, func() ([]byte, error) { 26 | return []byte(os.Getenv(key)), nil 27 | }) 28 | } 29 | 30 | // FromBytes returns a reader that simply returns the given bytes 31 | // when Read() is called. 32 | func FromBytes(raw []byte) Reader { 33 | return readerFunc(func() ([]byte, error) { 34 | return raw, nil 35 | }) 36 | } 37 | 38 | // FromString returns a reader that simply returns the given string as 39 | // a byte slice when Read() is called. 40 | func FromString(raw string) Reader { 41 | return readerFunc(func() ([]byte, error) { 42 | return []byte(raw), nil 43 | }) 44 | } 45 | 46 | type readerFunc func() ([]byte, error) 47 | 48 | func (r readerFunc) Read() ([]byte, error) { 49 | return r() 50 | } 51 | 52 | // newReader is a helper function to create a reader with a source from any func() ([]byte, error). 53 | func newReader(source string, read func() ([]byte, error)) Reader { 54 | return reader{ 55 | source: source, 56 | readFunc: read, 57 | } 58 | } 59 | 60 | type reader struct { 61 | source string 62 | readFunc func() ([]byte, error) 63 | } 64 | 65 | func (r reader) Source() string { 66 | return r.source 67 | } 68 | 69 | // Read implements the Reader interface. 70 | func (r reader) Read() ([]byte, error) { 71 | return r.readFunc() 72 | } 73 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/sessions/refresher.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "net/http" 5 | 6 | httpclient "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 7 | ) 8 | 9 | // SessionRefresher implements auth.Authenticator by using sessions for authentication that are automatically 10 | // refreshed when they are about to expire. 11 | type SessionRefresher struct { 12 | httpClient *httpclient.Client 13 | currentSession Session 14 | sessionCreator SessionCreator 15 | } 16 | 17 | // NewSessionRefresher creates a new SessionRefresher that uses the httpClient for requesting new sessions with 18 | // the SessionCreator. 19 | func NewSessionRefresher(httpClient *httpclient.Client, sessionCreator SessionCreator) *SessionRefresher { 20 | return &SessionRefresher{ 21 | httpClient: httpClient, 22 | sessionCreator: sessionCreator, 23 | } 24 | } 25 | 26 | // Authenticate the given request with a session that is automatically refreshed when in almost expires. 27 | func (r *SessionRefresher) Authenticate(req *http.Request) error { 28 | if r.currentSession == nil || r.currentSession.NeedsRefresh() { 29 | newSession, err := r.sessionCreator.Create(r.httpClient) 30 | if err != nil { 31 | return err 32 | } 33 | r.currentSession = newSession 34 | } 35 | return r.currentSession.Authenticator().Authenticate(req) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/sessions/session.go: -------------------------------------------------------------------------------- 1 | // Package sessions provides session authentication to the SecretHub API for the HTTP client. 2 | package sessions 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/secrethub/secrethub-go/internals/api/uuid" 8 | "github.com/secrethub/secrethub-go/internals/auth" 9 | "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 10 | ) 11 | 12 | const expirationMargin = time.Second * 30 13 | 14 | // SessionCreator can create a new SecretHub session with a http.Client. 15 | type SessionCreator interface { 16 | Create(httpClient *http.Client) (Session, error) 17 | } 18 | 19 | // Session provides a auth.Authenticator than can be temporarily used to temporarily authenticate to the SecretHub API. 20 | type Session interface { 21 | NeedsRefresh() bool 22 | Authenticator() auth.Authenticator 23 | } 24 | 25 | type hmacSession struct { 26 | sessionID uuid.UUID 27 | sessionKey string 28 | 29 | expireTime 30 | } 31 | 32 | // Authenticator returns an auth.Authenticator that can be used to authenticate a request with an HMAC session. 33 | func (h hmacSession) Authenticator() auth.Authenticator { 34 | return auth.NewHTTPSigner(auth.NewSessionSigner(h.sessionID, h.sessionKey)) 35 | } 36 | 37 | type expireTime time.Time 38 | 39 | // NeedsRefresh returns true when the session is about to expire and should be refreshed. 40 | func (t expireTime) NeedsRefresh() bool { 41 | return time.Now().After(time.Time(t).Add(-expirationMargin)) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/sessions/session_aws.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | shaws "github.com/secrethub/secrethub-go/internals/aws" 8 | "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/endpoints" 12 | "github.com/aws/aws-sdk-go/aws/session" 13 | "github.com/aws/aws-sdk-go/service/sts" 14 | ) 15 | 16 | const ( 17 | // Currently always use the eu-west-1 region. 18 | defaultAWSRegionForSTS = endpoints.EuWest1RegionID 19 | ) 20 | 21 | type awsSessionCreator struct { 22 | awsConfig []*aws.Config 23 | } 24 | 25 | // NewAWSSessionCreator returns a SessionCreator that uses AWS STS authentication to request sessions. 26 | func NewAWSSessionCreator(awsCfg ...*aws.Config) SessionCreator { 27 | return &awsSessionCreator{ 28 | awsConfig: awsCfg, 29 | } 30 | } 31 | 32 | // Create a new Session using AWS STS for authentication. 33 | func (s *awsSessionCreator) Create(httpClient *http.Client) (Session, error) { 34 | region := defaultAWSRegionForSTS 35 | 36 | getCallerIdentityReq, err := getCallerIdentityRequest(region, s.awsConfig...) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | req := api.NewAuthRequestAWSSTS(api.SessionTypeHMAC, region, getCallerIdentityReq) 42 | resp, err := httpClient.CreateSession(req) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if resp.Type != api.SessionTypeHMAC { 47 | return nil, api.ErrInvalidSessionType 48 | } 49 | sess := resp.HMAC() 50 | 51 | return &hmacSession{ 52 | sessionID: sess.SessionID, 53 | sessionKey: sess.Payload.SessionKey, 54 | expireTime: expireTime(sess.Expires), 55 | }, nil 56 | } 57 | 58 | // getCallerIdentityRequest returns the raw bytes of a signed GetCallerIdentity request. 59 | func getCallerIdentityRequest(region string, awsCfg ...*aws.Config) ([]byte, error) { 60 | // Explicitly set the endpoint because the aws sdk by default uses the global endpoint. 61 | cfg := aws.NewConfig().WithRegion(region).WithEndpoint("sts." + region + ".amazonaws.com") 62 | awsSession, err := session.NewSession(append(awsCfg, cfg)...) 63 | if err != nil { 64 | return nil, shaws.HandleError(err) 65 | } 66 | 67 | svc := sts.New(awsSession, cfg) 68 | identityRequest, _ := svc.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{}) 69 | 70 | // Sign the CallerIdentityRequest with the AWS access key 71 | err = identityRequest.Sign() 72 | if err != nil { 73 | return nil, shaws.HandleError(err) 74 | } 75 | 76 | var buf bytes.Buffer 77 | err = identityRequest.HTTPRequest.Write(&buf) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return buf.Bytes(), nil 82 | } 83 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/sessions/session_gcp.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "errors" 5 | 6 | "cloud.google.com/go/compute/metadata" 7 | "google.golang.org/api/option" 8 | 9 | "github.com/secrethub/secrethub-go/internals/api" 10 | "github.com/secrethub/secrethub-go/internals/gcp" 11 | "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 12 | ) 13 | 14 | type gcpSessionCreator struct { 15 | gcpOptions []option.ClientOption 16 | } 17 | 18 | // NewAWSSessionCreator returns a SessionCreator that uses a GCP Service Account Identity Token to request sessions. 19 | func NewGCPSessionCreator(gcpOptions ...option.ClientOption) SessionCreator { 20 | return &gcpSessionCreator{ 21 | gcpOptions: gcpOptions, 22 | } 23 | } 24 | 25 | // Create a new Session using GCP Service Account Identity Token for authentication. 26 | func (s *gcpSessionCreator) Create(httpClient *http.Client) (Session, error) { 27 | if !metadata.OnGCE() { 28 | return nil, errors.New("GCP Identity Provider only supported when running on GCP") 29 | 30 | } 31 | idToken, err := metadata.Get("instance/service-accounts/default/identity?audience=secrethub&format=full") 32 | if err != nil { 33 | return nil, gcp.HandleError(err) 34 | } 35 | 36 | req := api.NewAuthRequestGCPServiceAccount(api.SessionTypeHMAC, idToken) 37 | resp, err := httpClient.CreateSession(req) 38 | if err != nil { 39 | return nil, err 40 | } 41 | if resp.Type != api.SessionTypeHMAC { 42 | return nil, api.ErrInvalidSessionType 43 | } 44 | sess := resp.HMAC() 45 | 46 | return &hmacSession{ 47 | sessionID: sess.SessionID, 48 | sessionKey: sess.Payload.SessionKey, 49 | expireTime: expireTime(sess.Expires), 50 | }, nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/sessions/session_test.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/secrethub/secrethub-go/internals/assert" 8 | ) 9 | 10 | func TestExpireTimeNeedsRefresh(t *testing.T) { 11 | cases := map[string]struct { 12 | in time.Time 13 | expected bool 14 | }{ 15 | "not expired": { 16 | in: time.Now().Add(time.Minute), 17 | expected: false, 18 | }, 19 | "in margin": { 20 | in: time.Now().Add(time.Second * 10), 21 | expected: true, 22 | }, 23 | "past": { 24 | in: time.Now().Add(-time.Second * 10), 25 | expected: true, 26 | }, 27 | } 28 | 29 | for name, tc := range cases { 30 | t.Run(name, func(t *testing.T) { 31 | assert.Equal(t, expireTime(tc.in).NeedsRefresh(), tc.expected) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/secrethub/credentials/setup_code.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | httpclient "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" 8 | 9 | "github.com/secrethub/secrethub-go/internals/auth" 10 | ) 11 | 12 | type SetupCode struct { 13 | code string 14 | } 15 | 16 | // Provide implements the Provider interface for the setup code. 17 | // Note that no decrypter is ever returned as setup codes cannot be used to decrypt secrets. 18 | func (s *SetupCode) Provide(client *httpclient.Client) (auth.Authenticator, Decrypter, error) { 19 | return s, nil, nil 20 | } 21 | 22 | func NewSetupCode(code string) *SetupCode { 23 | return &SetupCode{ 24 | code: code, 25 | } 26 | } 27 | 28 | // Authenticate authenticates the given request with a setup code, by providing the "SetupCode" tag and the setup code 29 | // in the "Authorization" header. 30 | func (s *SetupCode) Authenticate(r *http.Request) error { 31 | r.Header.Set("Authorization", fmt.Sprintf("%s-%s %s", auth.AuthHeaderVersionV1, "SetupCode", s.code)) 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/secrethub/example_test.go: -------------------------------------------------------------------------------- 1 | package secrethub_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | 9 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" 10 | 11 | "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" 12 | ) 13 | 14 | var client secrethub.ClientInterface 15 | 16 | // Create a new Client. 17 | func ExampleNewClient() { 18 | client, err := secrethub.NewClient() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | // use the client 24 | _, err = client.Secrets().ReadString("workspace/repo/secret") 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | } 29 | 30 | // Create a new client that uses native AWS services to handle encryption and authentication. 31 | func ExampleNewClient_aws() { 32 | client, err := secrethub.NewClient(secrethub.WithCredentials(credentials.UseAWS())) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | // use the client 38 | _, err = client.Secrets().ReadString("workspace/repo/secret") 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | 44 | // Create a new repository. 45 | func ExampleClient_Repos_create() { 46 | _, err := client.Repos().Create("workspace/repo") 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | } 51 | 52 | // Create a new directory. 53 | func ExampleClient_Dirs_create() { 54 | _, err := client.Dirs().Create("workspace/repo/dir") 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | } 59 | 60 | // List all audit events for a given repository. 61 | func ExampleClient_Repos_eventIterator() { 62 | iter := client.Repos().EventIterator("workspace/repo", &secrethub.AuditEventIteratorParams{}) 63 | for { 64 | event, err := iter.Next() 65 | if err == iterator.Done { 66 | break 67 | } else if err != nil { 68 | log.Fatal(err) 69 | } 70 | fmt.Printf("Audit event logged at:%s form ip address: %s", event.LoggedAt.Local(), event.IPAddress) 71 | } 72 | } 73 | 74 | // Write a secret. 75 | func ExampleClient_Secrets_write() { 76 | secret := []byte("secret_value_123") 77 | _, err := client.Secrets().Write("workspace/repo/secret", secret) 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | } 82 | 83 | // Read a secret. 84 | func ExampleClient_Secrets_read() { 85 | secret, err := client.Secrets().Read("workspace/repo/secret") 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | 90 | fmt.Print(string(secret.Data)) 91 | } 92 | 93 | // List all audit events for a given secret. 94 | func ExampleClient_Secrets_eventIterator() { 95 | iter := client.Secrets().EventIterator("workspace/repo/secret", &secrethub.AuditEventIteratorParams{}) 96 | for { 97 | event, err := iter.Next() 98 | if err == iterator.Done { 99 | break 100 | } else if err != nil { 101 | log.Fatal(err) 102 | } 103 | fmt.Printf("Audit event logged at:%s form ip address: %s", event.LoggedAt.Local(), event.IPAddress) 104 | } 105 | } 106 | 107 | // Create a service account credential. 108 | func ExampleClient_Services_create() { 109 | credentialCreator := credentials.CreateKey() 110 | service, err := client.Services().Create("workspace/repo", "Service account description", credentialCreator) 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | 115 | key, err := credentialCreator.Export() 116 | if err != nil { 117 | log.Fatal(err) 118 | } 119 | 120 | fmt.Printf("Service ID: %s\n", service.ServiceID) 121 | fmt.Printf("Credential: %s\n", string(key)) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/accessrule.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | ) 9 | 10 | // AccessRuleService is a mock of the AccessRuleService interface. 11 | type AccessRuleService struct { 12 | DeleteFunc func(path string, accountName string) error 13 | GetFunc func(path string, accountName string) (*api.AccessRule, error) 14 | ListLevelsFunc func(path string) ([]*api.AccessLevel, error) 15 | ListFunc func(path string, depth int, ancestors bool) ([]*api.AccessRule, error) 16 | SetFunc func(path string, permission string, accountName string) (*api.AccessRule, error) 17 | IteratorFunc func() secrethub.AccessRuleIterator 18 | LevelIteratorFunc func() secrethub.AccessLevelIterator 19 | } 20 | 21 | func (s *AccessRuleService) Iterator(path string, _ *secrethub.AccessRuleIteratorParams) secrethub.AccessRuleIterator { 22 | return s.IteratorFunc() 23 | } 24 | 25 | func (s *AccessRuleService) LevelIterator(path string, _ *secrethub.AccessLevelIteratorParams) secrethub.AccessLevelIterator { 26 | return s.LevelIteratorFunc() 27 | } 28 | 29 | // Delete implements the AccessRuleService interface Delete function. 30 | func (s *AccessRuleService) Delete(path string, accountName string) error { 31 | return s.DeleteFunc(path, accountName) 32 | } 33 | 34 | // Get implements the AccessRuleService interface Get function. 35 | func (s *AccessRuleService) Get(path string, accountName string) (*api.AccessRule, error) { 36 | return s.GetFunc(path, accountName) 37 | } 38 | 39 | // ListLevels implements the AccessRuleService interface ListLevels function. 40 | func (s *AccessRuleService) ListLevels(path string) ([]*api.AccessLevel, error) { 41 | return s.ListLevelsFunc(path) 42 | } 43 | 44 | // List implements the AccessRuleService interface List function. 45 | func (s *AccessRuleService) List(path string, depth int, ancestors bool) ([]*api.AccessRule, error) { 46 | return s.ListFunc(path, depth, ancestors) 47 | } 48 | 49 | // Set implements the AccessRuleService interface Set function. 50 | func (s *AccessRuleService) Set(path string, permission string, accountName string) (*api.AccessRule, error) { 51 | return s.SetFunc(path, permission, accountName) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/account.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/internals/api/uuid" 8 | "github.com/secrethub/secrethub-go/pkg/secrethub" 9 | ) 10 | 11 | // AccountService is a mock of the AccountService interface. 12 | type AccountService struct { 13 | MeFunc func() (*api.Account, error) 14 | DeleteFunc func(accountID uuid.UUID) error 15 | GetFunc func(name string) (*api.Account, error) 16 | AccountKeyService secrethub.AccountKeyService 17 | } 18 | 19 | func (s *AccountService) Keys() secrethub.AccountKeyService { 20 | return s.AccountKeyService 21 | } 22 | 23 | // Get implements the AccountService interface Get function. 24 | func (s *AccountService) Get(name string) (*api.Account, error) { 25 | return s.GetFunc(name) 26 | } 27 | 28 | // Delete implements the AccountService interface Delete function. 29 | func (s *AccountService) Delete(accountID uuid.UUID) error { 30 | return s.DeleteFunc(accountID) 31 | } 32 | 33 | // Me implements the AccountService interface Me function. 34 | func (s *AccountService) Me() (*api.Account, error) { 35 | return s.MeFunc() 36 | } 37 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/audit.go: -------------------------------------------------------------------------------- 1 | package fakeclient 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" 6 | ) 7 | 8 | type AuditEventIterator struct { 9 | Events []api.Audit 10 | Err error 11 | i int 12 | } 13 | 14 | func (iter *AuditEventIterator) Next() (api.Audit, error) { 15 | if iter.Err != nil { 16 | return api.Audit{}, iter.Err 17 | } 18 | 19 | if iter.i >= len(iter.Events) { 20 | return api.Audit{}, iterator.Done 21 | } 22 | res := iter.Events[iter.i] 23 | iter.i++ 24 | return res, nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/client.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | // Package fakeclient provides mock implementations of 4 | // the client to be used for testing. 5 | package fakeclient 6 | 7 | import "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | 9 | var _ secrethub.ClientInterface = (*Client)(nil) 10 | 11 | // Client implements the secrethub.Client interface. 12 | type Client struct { 13 | AccessRuleService *AccessRuleService 14 | AccountService *AccountService 15 | CredentialService *CredentialService 16 | DirService *DirService 17 | IDPLinkService *IDPLinkService 18 | MeService *MeService 19 | OrgService *OrgService 20 | RepoService *RepoService 21 | SecretService *SecretService 22 | ServiceService *ServiceService 23 | UserService *UserService 24 | } 25 | 26 | // AccessRules implements the secrethub.Client interface. 27 | func (c Client) AccessRules() secrethub.AccessRuleService { 28 | return c.AccessRuleService 29 | } 30 | 31 | // Accounts implements the secrethub.Client interface. 32 | func (c Client) Accounts() secrethub.AccountService { 33 | return c.AccountService 34 | } 35 | 36 | // Dirs implements the secrethub.Client interface. 37 | func (c Client) Dirs() secrethub.DirService { 38 | return c.DirService 39 | } 40 | 41 | // Me implements the secrethub.Client interface. 42 | func (c Client) Me() secrethub.MeService { 43 | return c.MeService 44 | } 45 | 46 | // Orgs implements the secrethub.Client interface. 47 | func (c Client) Orgs() secrethub.OrgService { 48 | return c.OrgService 49 | } 50 | 51 | // Repos implements the secrethub.Client interface. 52 | func (c Client) Repos() secrethub.RepoService { 53 | return c.RepoService 54 | } 55 | 56 | // Secrets implements the secrethub.Client interface. 57 | func (c Client) Secrets() secrethub.SecretService { 58 | return c.SecretService 59 | } 60 | 61 | // Services implements the secrethub.Client interface. 62 | func (c Client) Services() secrethub.ServiceService { 63 | return c.ServiceService 64 | } 65 | 66 | // Users implements the secrethub.Client interface. 67 | func (c Client) Users() secrethub.UserService { 68 | return c.UserService 69 | } 70 | 71 | func (c Client) IDPLinks() secrethub.IDPLinkService { 72 | return c.IDPLinkService 73 | } 74 | 75 | func (c Client) Credentials() secrethub.CredentialService { 76 | return c.CredentialService 77 | } 78 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/credentials.go: -------------------------------------------------------------------------------- 1 | package fakeclient 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/pkg/secrethub" 6 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" 8 | ) 9 | 10 | type CredentialService struct { 11 | CreateFunc func(credentials.Creator, string) (*api.Credential, error) 12 | DisableFunc func(fingerprint string) error 13 | ListFunc func(_ *secrethub.CredentialListParams) secrethub.CredentialIterator 14 | } 15 | 16 | func (c *CredentialService) Create(creator credentials.Creator, description string) (*api.Credential, error) { 17 | return c.CreateFunc(creator, description) 18 | } 19 | 20 | func (c *CredentialService) Disable(fingerprint string) error { 21 | return c.DisableFunc(fingerprint) 22 | } 23 | 24 | func (c *CredentialService) List(credentialListParams *secrethub.CredentialListParams) secrethub.CredentialIterator { 25 | return c.ListFunc(credentialListParams) 26 | } 27 | 28 | type CredentialIterator struct { 29 | Credentials []*api.Credential 30 | CurrentIndex int 31 | Err error 32 | } 33 | 34 | func (c *CredentialIterator) Next() (api.Credential, error) { 35 | if c.Err != nil { 36 | return api.Credential{}, c.Err 37 | } 38 | 39 | currentIndex := c.CurrentIndex 40 | if currentIndex >= len(c.Credentials) { 41 | return api.Credential{}, iterator.Done 42 | } 43 | c.CurrentIndex++ 44 | return *c.Credentials[currentIndex], nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/dir.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | ) 9 | 10 | // DirService is a mock of the DirService interface. 11 | type DirService struct { 12 | CreateFunc func(path string) (*api.Dir, error) 13 | ExistsFunc func(path string) (bool, error) 14 | DeleteFunc func(path string) error 15 | GetTreeFunc func(path string, depth int, ancestors bool) (*api.Tree, error) 16 | secrethub.DirService 17 | } 18 | 19 | // Create implements the DirService interface Create function. 20 | func (s *DirService) Create(path string) (*api.Dir, error) { 21 | return s.CreateFunc(path) 22 | } 23 | 24 | // Exists implements the DirService interface Exists function. 25 | func (s *DirService) Exists(path string) (bool, error) { 26 | return s.ExistsFunc(path) 27 | } 28 | 29 | // Delete implements the DirService interface Delete function. 30 | func (s *DirService) Delete(path string) error { 31 | return s.DeleteFunc(path) 32 | } 33 | 34 | // GetTree implements the DirService interface GetTree function. 35 | func (s *DirService) GetTree(path string, depth int, ancestors bool) (*api.Tree, error) { 36 | return s.GetTreeFunc(path, depth, ancestors) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/idp_link.go: -------------------------------------------------------------------------------- 1 | package fakeclient 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/internals/oauthorizer" 6 | "github.com/secrethub/secrethub-go/pkg/secrethub" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" 8 | ) 9 | 10 | type IDPLinkService struct { 11 | GCPService secrethub.IDPLinkGCPService 12 | } 13 | 14 | func (i IDPLinkService) GCP() secrethub.IDPLinkGCPService { 15 | return i.GCPService 16 | } 17 | 18 | type IDPLinkGCPService struct { 19 | CreateFunc func(namespace string, projectID string, authorizationCode string, redirectURI string) (*api.IdentityProviderLink, error) 20 | ListFunc func(namespace string, params *secrethub.IdpLinkIteratorParams) secrethub.IdpLinkIterator 21 | GetFunc func(namespace string, projectID string) (*api.IdentityProviderLink, error) 22 | ExistsFunc func(namespace string, projectID string) (bool, error) 23 | DeleteFunc func(namespace string, projectID string) error 24 | AuthorizationCodeListenerFunc func(namespace string, projectID string) (oauthorizer.CallbackHandler, error) 25 | } 26 | 27 | func (i IDPLinkGCPService) Create(namespace string, projectID string, authorizationCode, redirectURI string) (*api.IdentityProviderLink, error) { 28 | return i.CreateFunc(namespace, projectID, authorizationCode, redirectURI) 29 | } 30 | 31 | func (i IDPLinkGCPService) List(namespace string, params *secrethub.IdpLinkIteratorParams) secrethub.IdpLinkIterator { 32 | return i.ListFunc(namespace, params) 33 | } 34 | 35 | func (i IDPLinkGCPService) Get(namespace string, projectID string) (*api.IdentityProviderLink, error) { 36 | return i.GetFunc(namespace, projectID) 37 | } 38 | 39 | func (i IDPLinkGCPService) Exists(namespace string, projectID string) (bool, error) { 40 | return i.ExistsFunc(namespace, projectID) 41 | } 42 | 43 | func (i IDPLinkGCPService) Delete(namespace string, projectID string) error { 44 | return i.DeleteFunc(namespace, projectID) 45 | } 46 | 47 | func (i IDPLinkGCPService) AuthorizationCodeListener(namespace string, projectID string) (oauthorizer.CallbackHandler, error) { 48 | return i.AuthorizationCodeListenerFunc(namespace, projectID) 49 | } 50 | 51 | type IDPLinkIterator struct { 52 | IDPLinks []*api.IdentityProviderLink 53 | CurrentIndex int 54 | Err error 55 | } 56 | 57 | func (c *IDPLinkIterator) Next() (api.IdentityProviderLink, error) { 58 | if c.Err != nil { 59 | return api.IdentityProviderLink{}, c.Err 60 | } 61 | 62 | currentIndex := c.CurrentIndex 63 | if currentIndex >= len(c.IDPLinks) { 64 | return api.IdentityProviderLink{}, iterator.Done 65 | } 66 | c.CurrentIndex++ 67 | return *c.IDPLinks[currentIndex], nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/me.go: -------------------------------------------------------------------------------- 1 | package fakeclient 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/pkg/secrethub" 6 | ) 7 | 8 | type MeService struct { 9 | GetUserFunc func() (*api.User, error) 10 | SendVerificationEmailFunc func() error 11 | ListReposFunc func() ([]*api.Repo, error) 12 | RepoIteratorFunc func(_ *secrethub.RepoIteratorParams) secrethub.RepoIterator 13 | } 14 | 15 | func (m *MeService) GetUser() (*api.User, error) { 16 | return m.GetUserFunc() 17 | } 18 | 19 | func (m *MeService) SendVerificationEmail() error { 20 | return m.SendVerificationEmailFunc() 21 | } 22 | 23 | func (m *MeService) ListRepos() ([]*api.Repo, error) { 24 | return m.ListReposFunc() 25 | } 26 | 27 | func (m *MeService) RepoIterator(repoIteratorParams *secrethub.RepoIteratorParams) secrethub.RepoIterator { 28 | return m.RepoIteratorFunc(repoIteratorParams) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/org.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | ) 9 | 10 | // OrgService is a mock of the RepoService interface. 11 | type OrgService struct { 12 | CreateFunc func(name string, description string) (*api.Org, error) 13 | DeleteFunc func(name string) error 14 | GetFunc func(name string) (*api.Org, error) 15 | MembersService secrethub.OrgMemberService 16 | ListMineFunc func() ([]*api.Org, error) 17 | IteratorFunc func(params *secrethub.OrgIteratorParams) secrethub.OrgIterator 18 | } 19 | 20 | func (s *OrgService) Iterator(params *secrethub.OrgIteratorParams) secrethub.OrgIterator { 21 | return s.IteratorFunc(params) 22 | } 23 | 24 | // Create implements the RepoService interface Create function. 25 | func (s *OrgService) Create(name string, description string) (*api.Org, error) { 26 | return s.CreateFunc(name, description) 27 | } 28 | 29 | // Delete implements the RepoService interface Delete function. 30 | func (s *OrgService) Delete(name string) error { 31 | return s.DeleteFunc(name) 32 | } 33 | 34 | // Get implements the RepoService interface Get function. 35 | func (s *OrgService) Get(name string) (*api.Org, error) { 36 | return s.GetFunc(name) 37 | } 38 | 39 | // Members returns a mock of the OrgMemberService interface. 40 | func (s *OrgService) Members() secrethub.OrgMemberService { 41 | return s.MembersService 42 | } 43 | 44 | // ListMine implements the RepoService interface ListMine function. 45 | func (s *OrgService) ListMine() ([]*api.Org, error) { 46 | return s.ListMineFunc() 47 | } 48 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/org_member.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | ) 9 | 10 | // OrgMemberService is a mock of the OrgMemberService interface. 11 | type OrgMemberService struct { 12 | InviteFunc func(org string, username string, role string) (*api.OrgMember, error) 13 | GetFunc func(org string, username string) (*api.OrgMember, error) 14 | UpdateFunc func(org string, username string, role string) (*api.OrgMember, error) 15 | RevokeFunc func(org string, username string, opts *api.RevokeOpts) (*api.RevokeOrgResponse, error) 16 | ListFunc func(org string) ([]*api.OrgMember, error) 17 | IteratorFunc func(org string, params *secrethub.OrgMemberIteratorParams) secrethub.OrgMemberIterator 18 | } 19 | 20 | func (s *OrgMemberService) Invite(org string, username string, role string) (*api.OrgMember, error) { 21 | return s.InviteFunc(org, username, role) 22 | } 23 | 24 | func (s *OrgMemberService) Get(org string, username string) (*api.OrgMember, error) { 25 | return s.GetFunc(org, username) 26 | } 27 | 28 | func (s *OrgMemberService) Update(org string, username string, role string) (*api.OrgMember, error) { 29 | return s.UpdateFunc(org, username, role) 30 | } 31 | 32 | func (s *OrgMemberService) Revoke(org string, username string, opts *api.RevokeOpts) (*api.RevokeOrgResponse, error) { 33 | return s.RevokeFunc(org, username, opts) 34 | } 35 | 36 | func (s *OrgMemberService) List(org string) ([]*api.OrgMember, error) { 37 | return s.ListFunc(org) 38 | } 39 | 40 | func (s *OrgMemberService) Iterator(org string, params *secrethub.OrgMemberIteratorParams) secrethub.OrgMemberIterator { 41 | return s.IteratorFunc(org, params) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/repo.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | ) 9 | 10 | // RepoService is a mock of the RepoService interface. 11 | type RepoService struct { 12 | ListFunc func(namespace string) ([]*api.Repo, error) 13 | ListAccountsFunc func(path string) ([]*api.Account, error) 14 | ListEventsFunc func(path string, subjectTypes api.AuditSubjectTypeList) ([]*api.Audit, error) 15 | ListMineFunc func() ([]*api.Repo, error) 16 | CreateFunc func(path string) (*api.Repo, error) 17 | DeleteFunc func(path string) error 18 | GetFunc func(path string) (*api.Repo, error) 19 | UserService secrethub.RepoUserService 20 | RepoServiceService secrethub.RepoServiceService 21 | AuditEventIterator *AuditEventIterator 22 | secrethub.RepoService 23 | } 24 | 25 | // List implements the RepoService interface List function. 26 | func (s *RepoService) List(namespace string) ([]*api.Repo, error) { 27 | return s.ListFunc(namespace) 28 | } 29 | 30 | // ListAccounts implements the RepoService interface ListAccounts function. 31 | func (s *RepoService) ListAccounts(path string) ([]*api.Account, error) { 32 | return s.ListAccountsFunc(path) 33 | } 34 | 35 | // ListEvents implements the RepoService interface ListEvents function. 36 | func (s *RepoService) ListEvents(path string, subjectTypes api.AuditSubjectTypeList) ([]*api.Audit, error) { 37 | return s.ListEventsFunc(path, subjectTypes) 38 | } 39 | 40 | // EventIterator implements the RepoService interface EventIterator function. 41 | func (s *RepoService) EventIterator(path string, config *secrethub.AuditEventIteratorParams) secrethub.AuditEventIterator { 42 | return s.AuditEventIterator 43 | } 44 | 45 | // ListMine implements the RepoService interface ListMine function. 46 | func (s *RepoService) ListMine() ([]*api.Repo, error) { 47 | return s.ListMineFunc() 48 | } 49 | 50 | // Create implements the RepoService interface Create function. 51 | func (s *RepoService) Create(path string) (*api.Repo, error) { 52 | return s.CreateFunc(path) 53 | } 54 | 55 | // Delete implements the RepoService interface Delete function. 56 | func (s *RepoService) Delete(path string) error { 57 | return s.DeleteFunc(path) 58 | } 59 | 60 | // Get implements the RepoService interface Get function. 61 | func (s *RepoService) Get(path string) (*api.Repo, error) { 62 | return s.GetFunc(path) 63 | } 64 | 65 | // Users returns the mocked UserService. 66 | func (s *RepoService) Users() secrethub.RepoUserService { 67 | return s.UserService 68 | } 69 | 70 | // Services returns the mocked RepoServiceService. 71 | func (s *RepoService) Services() secrethub.RepoServiceService { 72 | return s.RepoServiceService 73 | } 74 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/repo_service.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | ) 9 | 10 | // RepoServiceService is a mock of the RepoServiceService interface. 11 | type RepoServiceService struct { 12 | ListFunc func(path string) ([]*api.Service, error) 13 | IteratorFunc func() secrethub.ServiceIterator 14 | } 15 | 16 | func (s *RepoServiceService) Iterator(path string, _ *secrethub.RepoServiceIteratorParams) secrethub.ServiceIterator { 17 | return s.IteratorFunc() 18 | } 19 | 20 | // List implements the RepoServiceService interface List function. 21 | func (s *RepoServiceService) List(path string) ([]*api.Service, error) { 22 | return s.ListFunc(path) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/repo_user.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | ) 9 | 10 | // RepoUserService is a mock of the RepoUserService interface. 11 | type RepoUserService struct { 12 | InviteFunc func(path string, username string) (*api.RepoMember, error) 13 | ListFunc func(path string) ([]*api.User, error) 14 | RevokeFunc func(path string, username string) (*api.RevokeRepoResponse, error) 15 | IteratorFunc func() secrethub.UserIterator 16 | } 17 | 18 | func (s *RepoUserService) Iterator(path string, params *secrethub.UserIteratorParams) secrethub.UserIterator { 19 | return s.IteratorFunc() 20 | } 21 | 22 | // Invite implements the RepoUserService interface Invite function. 23 | func (s *RepoUserService) Invite(path string, username string) (*api.RepoMember, error) { 24 | return s.InviteFunc(path, username) 25 | } 26 | 27 | // List implements the RepoUserService interface List function. 28 | func (s *RepoUserService) List(path string) ([]*api.User, error) { 29 | return s.ListFunc(path) 30 | } 31 | 32 | // Revoke implements the RepoUserService interface Revoke function. 33 | func (s *RepoUserService) Revoke(path string, username string) (*api.RevokeRepoResponse, error) { 34 | return s.RevokeFunc(path, username) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/secret_version.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | ) 9 | 10 | // SecretVersionService can be used to mock a SecretVersionService. 11 | type SecretVersionService struct { 12 | DeleteFunc func(path string) error 13 | GetWithDataFunc func(path string) (*api.SecretVersion, error) 14 | GetWithoutDataFunc func(path string) (*api.SecretVersion, error) 15 | ListWithDataFunc func(path string) ([]*api.SecretVersion, error) 16 | ListWithoutDataFunc func(path string) ([]*api.SecretVersion, error) 17 | IteratorFunc func(path string, params *secrethub.SecretVersionIteratorParams) secrethub.SecretVersionIterator 18 | } 19 | 20 | // Delete implements the SecretVersionService interface Delete function. 21 | func (s *SecretVersionService) Delete(path string) error { 22 | return s.DeleteFunc(path) 23 | } 24 | 25 | // GetWithData implements the SecretVersionService interface GetWithData function. 26 | func (s *SecretVersionService) GetWithData(path string) (*api.SecretVersion, error) { 27 | return s.GetWithDataFunc(path) 28 | } 29 | 30 | // GetWithoutData implements the SecretVersionService interface GetWithoutData function. 31 | func (s *SecretVersionService) GetWithoutData(path string) (*api.SecretVersion, error) { 32 | return s.GetWithoutDataFunc(path) 33 | } 34 | 35 | // ListWithData implements the SecretVersionService interface ListWithData function. 36 | func (s *SecretVersionService) ListWithData(path string) ([]*api.SecretVersion, error) { 37 | return s.ListWithDataFunc(path) 38 | } 39 | 40 | // ListWithoutData implements the SecretVersionService interface ListWithoutData function. 41 | func (s *SecretVersionService) ListWithoutData(path string) ([]*api.SecretVersion, error) { 42 | return s.ListWithoutDataFunc(path) 43 | } 44 | 45 | func (s *SecretVersionService) Iterator(path string, params *secrethub.SecretVersionIteratorParams) secrethub.SecretVersionIterator { 46 | return s.IteratorFunc(path, params) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/service.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" 9 | ) 10 | 11 | // ServiceService is a mock of the ServiceService interface. 12 | type ServiceService struct { 13 | CreateFunc func(path string, description string, credentialCreator credentials.Creator) (*api.Service, error) 14 | DeleteFunc func(id string) (*api.RevokeRepoResponse, error) 15 | GetFunc func(id string) (*api.Service, error) 16 | ListFunc func(path string) ([]*api.Service, error) 17 | AWSService *ServiceAWSService 18 | 19 | IteratorFunc func() secrethub.ServiceIterator 20 | } 21 | 22 | func (s *ServiceService) Iterator(path string, _ *secrethub.ServiceIteratorParams) secrethub.ServiceIterator { 23 | return s.IteratorFunc() 24 | } 25 | 26 | // Create implements the ServiceService interface Create function. 27 | func (s *ServiceService) Create(path string, description string, credentialCreator credentials.Creator) (*api.Service, error) { 28 | return s.CreateFunc(path, description, credentialCreator) 29 | } 30 | 31 | // Delete implements the ServiceService interface Delete function. 32 | func (s *ServiceService) Delete(id string) (*api.RevokeRepoResponse, error) { 33 | return s.DeleteFunc(id) 34 | } 35 | 36 | // Get implements the ServiceService interface Get function. 37 | func (s *ServiceService) Get(id string) (*api.Service, error) { 38 | return s.GetFunc(id) 39 | } 40 | 41 | // List implements the ServiceService interface List function. 42 | func (s *ServiceService) List(path string) ([]*api.Service, error) { 43 | return s.ListFunc(path) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/service_aws.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/secrethub/secrethub-go/internals/api" 8 | ) 9 | 10 | // ServiceAWSService is a mock of the ServiceAWSService interface. 11 | type ServiceAWSService struct { 12 | CreateFunc func(path string, description string, keyID, role string, cfgs ...*aws.Config) (*api.Service, error) 13 | } 14 | 15 | // Create implements the ServiceAWSService interface Create function. 16 | func (s *ServiceAWSService) Create(path string, description string, keyID, role string, cfgs ...*aws.Config) (*api.Service, error) { 17 | return s.CreateFunc(path, description, keyID, role, cfgs...) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/secrethub/fakeclient/user.go: -------------------------------------------------------------------------------- 1 | // +build !production 2 | 3 | package fakeclient 4 | 5 | import ( 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | ) 8 | 9 | // UserService is a mock of the UserService interface. 10 | type UserService struct { 11 | GetFunc func(username string) (*api.User, error) 12 | MeFunc func() (*api.User, error) 13 | } 14 | 15 | // Get implements the UserService interface Get function. 16 | func (s *UserService) Get(username string) (*api.User, error) { 17 | return s.GetFunc(username) 18 | } 19 | 20 | // Me implements the UserService interface Me function. 21 | func (s *UserService) Me() (*api.User, error) { 22 | return s.MeFunc() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/secrethub/internals/http/encoding.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/secrethub/secrethub-go/internals/errio" 11 | ) 12 | 13 | // Errors 14 | var ( 15 | ErrWrongContentType = errHTTP.Code("wrong_content_type").Error("server returned wrong content type in header") 16 | ) 17 | 18 | // validator is an interface that helps validate the values of arguments. 19 | type validator interface { 20 | Validate() error 21 | } 22 | 23 | // encodeRequest simplifies setting the request body and correct headers. 24 | // If in == nil, nothing happens, else it will attempt to encode the body as json. 25 | // Before encoding, it will validate the input if possible. 26 | func encodeRequest(req *http.Request, in interface{}) error { 27 | if in == nil { 28 | return nil 29 | } 30 | 31 | validator, ok := in.(validator) 32 | if ok { 33 | err := validator.Validate() 34 | if err != nil { 35 | return errio.StatusError(err) 36 | } 37 | } 38 | 39 | jsonBytes, err := json.Marshal(in) 40 | if err != nil { 41 | return errHTTP.Code("cannot_encode_request").StatusErrorf("cannot encode request: %v", http.StatusBadRequest, err) 42 | } 43 | 44 | buf := bytes.NewBuffer(jsonBytes) 45 | req.Body = ioutil.NopCloser(buf) 46 | 47 | req.ContentLength = int64(len(jsonBytes)) 48 | req.Header.Set("Content-Length", strconv.Itoa(len(jsonBytes))) 49 | req.Header.Set("Content-Type", "application/json") 50 | 51 | return nil 52 | } 53 | 54 | // decodeResponse reads the response body and checks for the correct headers. 55 | // If out == nil, nothing happens, else it will attempt to decode the body as json. 56 | // After decoding, it will validate the result if possible. 57 | func decodeResponse(resp *http.Response, out interface{}) error { 58 | if out == nil { 59 | return nil 60 | } 61 | 62 | if t := resp.Header.Get("Content-Type"); t != "application/json" { 63 | return ErrWrongContentType 64 | } 65 | 66 | bytes, err := ioutil.ReadAll(resp.Body) 67 | if err != nil { 68 | return errio.StatusError(err) 69 | } 70 | 71 | err = json.Unmarshal(bytes, out) 72 | if err != nil { 73 | return errHTTP.Code("cannot_decode_response").StatusErrorf("cannot decode response: %v", http.StatusInternalServerError, err) 74 | } 75 | 76 | validator, ok := out.(validator) 77 | if ok { 78 | err := validator.Validate() 79 | if err != nil { 80 | return errio.StatusError(err) 81 | } 82 | } 83 | return nil 84 | } 85 | 86 | // parseError parses the body of an http.Response into an errio.PublicStatusError. 87 | // If unsuccessful, it simply outputs the statuscode and the bytes of the body. 88 | func parseError(resp *http.Response) error { 89 | bytes, err := ioutil.ReadAll(resp.Body) 90 | if err != nil { 91 | return errHTTP.Code("cannot_read_response").Errorf("cannot read the server response: %s", err) 92 | } 93 | 94 | // Try to unmarshal into a PublicStatusError 95 | e := errio.PublicStatusError{} 96 | err = json.Unmarshal(bytes, &e) 97 | if err != nil { 98 | // Degrade with a best effort error message. 99 | log.Debugf("body: %v", string(bytes)) 100 | return errHTTP.Code("cannot_parse_server_response").Errorf("%d - %s: %v", 101 | resp.StatusCode, 102 | resp.Status, 103 | err, 104 | ) 105 | } 106 | if e.Message == "" { 107 | return errHTTP.Code("unexpected_message_in_server_error").Errorf("%d - %s. Response:\n%s", 108 | resp.StatusCode, 109 | resp.Status, 110 | string(bytes), 111 | ) 112 | } 113 | 114 | e.StatusCode = resp.StatusCode 115 | return e 116 | } 117 | -------------------------------------------------------------------------------- /pkg/secrethub/internals/http/options.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "time" 7 | 8 | "github.com/secrethub/secrethub-go/internals/auth" 9 | ) 10 | 11 | // ClientOption is an option that can be set on an http.Client. 12 | type ClientOption func(*Client) 13 | 14 | // WithServerURL overrides the default server endpoint URL used by the HTTP client. 15 | func WithServerURL(url url.URL) ClientOption { 16 | return func(client *Client) { 17 | client.base = getBaseURL(url) 18 | } 19 | } 20 | 21 | // WithTransport replaces the DefaultTransport used by the HTTP client with the provided RoundTripper. 22 | func WithTransport(transport http.RoundTripper) ClientOption { 23 | return func(client *Client) { 24 | client.client.Transport = transport 25 | } 26 | } 27 | 28 | // WithTimeout overrides the default request timeout of the HTTP client. 29 | func WithTimeout(timeout time.Duration) ClientOption { 30 | return func(client *Client) { 31 | client.client.Timeout = timeout 32 | } 33 | } 34 | 35 | // WithUserAgent overrides the default user-agent supplied by HTTP client in requests. 36 | func WithUserAgent(userAgent string) ClientOption { 37 | return func(client *Client) { 38 | client.userAgent = userAgent 39 | } 40 | } 41 | 42 | // WithAuthenticator sets the authenticator used to authenticate requests made by the HTTP client. 43 | func WithAuthenticator(authenticator auth.Authenticator) ClientOption { 44 | return func(client *Client) { 45 | client.authenticator = authenticator 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/secrethub/iterator/iterator.go: -------------------------------------------------------------------------------- 1 | // Package iterator provides a generic iterator to be used as a building block for typed iterators. 2 | // In your applications, use the typed iterators returned by the secrethub package. 3 | package iterator 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/secrethub/secrethub-go/internals/errio" 9 | ) 10 | 11 | // Errors 12 | var ( 13 | Done = errio.Namespace("iterator").Code("done").Error("there are no more items left") 14 | ) 15 | 16 | type Paginator interface { 17 | Next() ([]interface{}, error) 18 | } 19 | 20 | type PaginatorConstructor func() (Paginator, error) 21 | 22 | type Iterator struct { 23 | newPaginator PaginatorConstructor 24 | paginator Paginator 25 | currentIndex int 26 | items []interface{} 27 | mutex *sync.Mutex 28 | } 29 | 30 | func New(newPaginator PaginatorConstructor) Iterator { 31 | return Iterator{ 32 | newPaginator: newPaginator, 33 | currentIndex: 0, 34 | items: nil, 35 | mutex: &sync.Mutex{}, 36 | } 37 | } 38 | 39 | func (it *Iterator) Next() (interface{}, error) { 40 | it.mutex.Lock() 41 | defer it.mutex.Unlock() 42 | 43 | var err error 44 | if it.paginator == nil { 45 | it.paginator, err = it.newPaginator() 46 | if err != nil { 47 | return nil, err 48 | } 49 | } 50 | 51 | return it.nextUnsafe() 52 | } 53 | 54 | // nextUnsafe should only be called from one goroutine at a time, 55 | // with the exception of nextUnsafe calling itself. 56 | // Use Next to enforce this. 57 | func (it *Iterator) nextUnsafe() (interface{}, error) { 58 | if it.items == nil || (len(it.items) > 0 && len(it.items) <= it.currentIndex) { 59 | var err error 60 | it.items, err = it.paginator.Next() 61 | if err != nil { 62 | return nil, err 63 | } 64 | it.currentIndex = 0 65 | return it.nextUnsafe() 66 | } 67 | 68 | if len(it.items) == 0 { 69 | return nil, Done 70 | } 71 | 72 | res := it.items[it.currentIndex] 73 | it.currentIndex++ 74 | return res, nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/secrethub/iterator/iterator_test.go: -------------------------------------------------------------------------------- 1 | package iterator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/secrethub/secrethub-go/internals/assert" 7 | ) 8 | 9 | func TestPaginatorConstructorWithFetch(t *testing.T) { 10 | it := New(PaginatorFactory(func() ([]interface{}, error) { 11 | return []interface{}{"this", "is", "a", "test"}, nil 12 | })) 13 | 14 | expected := []string{"this", "is", "a", "test"} 15 | i := 0 16 | 17 | for { 18 | str, err := it.Next() 19 | if err == Done { 20 | break 21 | } else if err != nil { 22 | t.Fail() 23 | } else { 24 | assert.Equal(t, str, expected[i]) 25 | i++ 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/secrethub/iterator/paginator.go: -------------------------------------------------------------------------------- 1 | package iterator 2 | 3 | type paginator struct { 4 | fetched bool 5 | fetch func() ([]interface{}, error) 6 | } 7 | 8 | // PaginatorFactory returns a paginator constructor that constructs a paginator 9 | // with the provided fetch function. 10 | func PaginatorFactory(fetch func() ([]interface{}, error)) PaginatorConstructor { 11 | return func() (Paginator, error) { 12 | return &paginator{ 13 | fetched: false, 14 | fetch: fetch, 15 | }, nil 16 | } 17 | } 18 | 19 | // Next returns the next page of items or an empty page if there are none left. 20 | func (p *paginator) Next() ([]interface{}, error) { 21 | if p.fetched { 22 | return make([]interface{}, 0), nil 23 | } 24 | 25 | res, err := p.fetch() 26 | if err != nil { 27 | return nil, err 28 | } 29 | p.fetched = true 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/secrethub/main_test.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | 7 | "github.com/go-chi/chi" 8 | 9 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" 10 | ) 11 | 12 | var ( 13 | cred1 *credentials.RSACredential 14 | cred1PublicKey []byte 15 | cred1Fingerprint string 16 | cred1Verifier []byte 17 | ) 18 | 19 | func init() { 20 | var err error 21 | cred1, err = credentials.GenerateRSACredential(1024) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | cred1PublicKey, err = cred1.Public().Encode() 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | cred1Verifier, cred1Fingerprint, err = cred1.Export() 32 | if err != nil { 33 | panic(err) 34 | } 35 | } 36 | 37 | // setup starts a test server and returns a router on which tests can register handlers. 38 | // Tests should use the returned client options to create new Clients and should call the 39 | // cleanup func() when done. 40 | func setup() (chi.Router, []ClientOption, func()) { 41 | // router is the HTTP router used with the test server. 42 | router := chi.NewRouter() 43 | 44 | // Strip prefixes so tests can register routes on e.g. /users instead of /v1/users. 45 | handler := http.NewServeMux() 46 | handler.Handle("/v1/", http.StripPrefix("/v1", router)) 47 | 48 | // server is a test HTTP server used to provide mock API responses. 49 | server := httptest.NewServer(handler) 50 | 51 | opts := []ClientOption{ 52 | WithServerURL(server.URL), 53 | WithCredentials(cred1), 54 | } 55 | 56 | return router, opts, server.Close 57 | } 58 | -------------------------------------------------------------------------------- /pkg/secrethub/me.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" 6 | ) 7 | 8 | // MeService handles operations on the authenticated account. 9 | type MeService interface { 10 | // GetUser retrieves the current users details. 11 | GetUser() (*api.User, error) 12 | // SendVerificationEmail sends an email to the authenticated user's registered email address 13 | // for them to prove they own that email address. 14 | SendVerificationEmail() error 15 | // ListRepos retrieves all repositories of the current user. 16 | // Deprecated: Use iterator function instead. 17 | ListRepos() ([]*api.Repo, error) 18 | // RepoIterator returns an iterator that retrieves all repos of the current user. 19 | RepoIterator(_ *RepoIteratorParams) RepoIterator 20 | } 21 | 22 | type meService struct { 23 | client *Client 24 | repoService RepoService 25 | userService UserService 26 | } 27 | 28 | func newMeService(client *Client) MeService { 29 | return meService{ 30 | client: client, 31 | repoService: newRepoService(client), 32 | userService: newUserService(client), 33 | } 34 | } 35 | 36 | // ListRepos retrieves all repositories of the current user. 37 | func (ms meService) ListRepos() ([]*api.Repo, error) { 38 | return ms.repoService.ListMine() 39 | } 40 | 41 | // GetUser retrieves the current users details. 42 | func (ms meService) GetUser() (*api.User, error) { 43 | return ms.userService.Me() 44 | } 45 | 46 | // SendVerificationEmail sends an email to the authenticated user's registered email address 47 | // for them to prove they own that email address. 48 | func (ms meService) SendVerificationEmail() error { 49 | return ms.client.httpClient.SendVerificationEmail() 50 | } 51 | 52 | // RepoIterator returns an iterator that retrieves all repos of the current user. 53 | func (ms meService) RepoIterator(params *RepoIteratorParams) RepoIterator { 54 | return &repoIterator{ 55 | iterator: iterator.New( 56 | iterator.PaginatorFactory( 57 | func() ([]interface{}, error) { 58 | repos, err := ms.client.httpClient.ListMyRepos() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | res := make([]interface{}, len(repos)) 64 | for i, element := range repos { 65 | res[i] = element 66 | } 67 | return res, nil 68 | }, 69 | ), 70 | ), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/secrethub/org.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/internals/errio" 6 | "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" 7 | ) 8 | 9 | // OrgService handles operations on organisations on SecretHub. 10 | type OrgService interface { 11 | // Create creates an organization. 12 | Create(name string, description string) (*api.Org, error) 13 | // Get retrieves an organization. 14 | Get(name string) (*api.Org, error) 15 | // Members returns an OrgMemberService. 16 | Members() OrgMemberService 17 | // Delete removes an organization. 18 | Delete(name string) error 19 | // ListMine returns the organizations of the current user. 20 | // Deprecated: Use iterator function instead. 21 | ListMine() ([]*api.Org, error) 22 | // Iterator returns an iterator that lists all organizations of the current user. 23 | Iterator(params *OrgIteratorParams) OrgIterator 24 | } 25 | 26 | func newOrgService(client *Client) OrgService { 27 | return orgService{ 28 | client: client, 29 | } 30 | } 31 | 32 | type orgService struct { 33 | client *Client 34 | } 35 | 36 | // Create creates an organization and adds the current account as an admin member. 37 | func (s orgService) Create(name string, description string) (*api.Org, error) { 38 | in := &api.CreateOrgRequest{ 39 | Name: name, 40 | Description: description, 41 | } 42 | 43 | err := in.Validate() 44 | if err != nil { 45 | return nil, errio.Error(err) 46 | } 47 | 48 | return s.client.httpClient.CreateOrg(in) 49 | } 50 | 51 | // Delete permanently deletes an organization and all of its resources. 52 | func (s orgService) Delete(name string) error { 53 | err := api.ValidateOrgName(name) 54 | if err != nil { 55 | return errio.Error(err) 56 | } 57 | 58 | return s.client.httpClient.DeleteOrg(name) 59 | } 60 | 61 | // Get retrieves an organization. 62 | func (s orgService) Get(name string) (*api.Org, error) { 63 | err := api.ValidateOrgName(name) 64 | if err != nil { 65 | return nil, errio.Error(err) 66 | } 67 | 68 | return s.client.httpClient.GetOrg(name) 69 | } 70 | 71 | // Members returns an OrgMemberService. 72 | func (s orgService) Members() OrgMemberService { 73 | return newOrgMemberService(s.client) 74 | } 75 | 76 | // ListMine returns the organizations of the current user. 77 | func (s orgService) ListMine() ([]*api.Org, error) { 78 | return s.client.httpClient.ListMyOrgs() 79 | } 80 | 81 | // Iterator returns an iterator that lists all organizations of the current user. 82 | func (s orgService) Iterator(params *OrgIteratorParams) OrgIterator { 83 | return &orgIterator{ 84 | iterator: iterator.New( 85 | iterator.PaginatorFactory( 86 | func() ([]interface{}, error) { 87 | orgs, err := s.client.httpClient.ListMyOrgs() 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | res := make([]interface{}, len(orgs)) 93 | for i, element := range orgs { 94 | res[i] = element 95 | } 96 | return res, nil 97 | }, 98 | ), 99 | ), 100 | } 101 | } 102 | 103 | type OrgIteratorParams struct{} 104 | 105 | // OrgIterator iterates over organizations. 106 | type OrgIterator interface { 107 | Next() (api.Org, error) 108 | } 109 | 110 | type orgIterator struct { 111 | iterator iterator.Iterator 112 | } 113 | 114 | // Next returns the next organization or iterator.Done as an error if all of them have been returned. 115 | func (it *orgIterator) Next() (api.Org, error) { 116 | item, err := it.iterator.Next() 117 | if err != nil { 118 | return api.Org{}, err 119 | } 120 | 121 | return *item.(*api.Org), nil 122 | } 123 | -------------------------------------------------------------------------------- /pkg/secrethub/repo_service.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/internals/errio" 6 | "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" 7 | ) 8 | 9 | // RepoServiceService handles operations on services of repositories. 10 | type RepoServiceService interface { 11 | // List lists the services of the given repository. 12 | // Deprecated: Use iterator function instead. 13 | List(path string) ([]*api.Service, error) 14 | // Iterator returns an iterator that lists all services of the given repository. 15 | Iterator(path string, _ *RepoServiceIteratorParams) ServiceIterator 16 | } 17 | 18 | func newRepoServiceService(client *Client) RepoServiceService { 19 | return &repoServiceService{ 20 | client: client, 21 | } 22 | } 23 | 24 | type repoServiceService struct { 25 | client *Client 26 | } 27 | 28 | // List lists the services of the given repository. 29 | func (s repoServiceService) List(path string) ([]*api.Service, error) { 30 | repoPath, err := api.NewRepoPath(path) 31 | if err != nil { 32 | return nil, errio.Error(err) 33 | } 34 | 35 | services, err := s.client.httpClient.ListServices(repoPath.GetNamespaceAndRepoName()) 36 | if err != nil { 37 | return nil, errio.Error(err) 38 | } 39 | 40 | return services, nil 41 | } 42 | 43 | // Iterator returns an iterator that lists all services of the given repository. 44 | func (s repoServiceService) Iterator(path string, _ *RepoServiceIteratorParams) ServiceIterator { 45 | return &serviceIterator{ 46 | iterator: iterator.New( 47 | iterator.PaginatorFactory( 48 | func() ([]interface{}, error) { 49 | repoPath, err := api.NewRepoPath(path) 50 | if err != nil { 51 | return nil, errio.Error(err) 52 | } 53 | 54 | services, err := s.client.httpClient.ListServices(repoPath.GetNamespaceAndRepoName()) 55 | if err != nil { 56 | return nil, errio.Error(err) 57 | } 58 | 59 | res := make([]interface{}, len(services)) 60 | for i, element := range services { 61 | res[i] = element 62 | } 63 | return res, nil 64 | }, 65 | ), 66 | ), 67 | } 68 | } 69 | 70 | // RepoServiceIteratorParams defines parameters used when listing Services of a given repo. 71 | type RepoServiceIteratorParams struct{} 72 | -------------------------------------------------------------------------------- /pkg/secrethub/secret_key.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/secrethub/secrethub-go/internals/api" 7 | "github.com/secrethub/secrethub-go/internals/api/uuid" 8 | "github.com/secrethub/secrethub-go/internals/crypto" 9 | "github.com/secrethub/secrethub-go/internals/errio" 10 | ) 11 | 12 | // getSecretKey gets the current key for a given secret. 13 | func (c *Client) getSecretKey(secretPath api.SecretPath) (*api.SecretKey, error) { 14 | blindName, err := c.convertPathToBlindName(secretPath) 15 | if err != nil { 16 | return nil, errio.Error(err) 17 | } 18 | 19 | encKey, err := c.httpClient.GetCurrentSecretKey(blindName) 20 | if err != nil { 21 | return nil, errio.Error(err) 22 | } 23 | 24 | accountKey, err := c.getAccountKey() 25 | if err != nil { 26 | return nil, errio.Error(err) 27 | } 28 | 29 | return encKey.Decrypt(accountKey) 30 | } 31 | 32 | // createSecretKey creates a new secret key for a given secret. 33 | func (c *Client) createSecretKey(secretPath api.SecretPath) (*api.SecretKey, error) { 34 | secretKey, err := crypto.GenerateSymmetricKey() 35 | if err != nil { 36 | return nil, errio.Error(err) 37 | } 38 | 39 | parentPath, err := secretPath.GetParentPath() 40 | if err != nil { 41 | return nil, errio.Error(err) 42 | } 43 | 44 | blindName, err := c.convertPathToBlindName(secretPath) 45 | if err != nil { 46 | return nil, errio.Error(err) 47 | } 48 | 49 | encryptedKeysMap := make(map[uuid.UUID]api.EncryptedKeyRequest) 50 | 51 | tries := 0 52 | for { 53 | // Get all accounts that have permission to read the secret. 54 | accounts, err := c.listDirAccounts(parentPath) 55 | if err != nil { 56 | return nil, errio.Error(err) 57 | } 58 | 59 | for _, account := range accounts { 60 | _, ok := encryptedKeysMap[account.AccountID] 61 | if !ok { 62 | publicKey, err := crypto.ImportRSAPublicKey(account.PublicKey) 63 | if err != nil { 64 | return nil, errio.Error(err) 65 | } 66 | 67 | encryptedSecretKey, err := publicKey.Wrap(secretKey.Export()) 68 | if err != nil { 69 | return nil, errio.Error(err) 70 | } 71 | 72 | encryptedKeysMap[account.AccountID] = api.EncryptedKeyRequest{ 73 | AccountID: account.AccountID, 74 | EncryptedKey: encryptedSecretKey, 75 | } 76 | } 77 | } 78 | 79 | encryptedFor := make([]api.EncryptedKeyRequest, len(encryptedKeysMap)) 80 | i := 0 81 | for _, encryptedKey := range encryptedKeysMap { 82 | encryptedFor[i] = encryptedKey 83 | i++ 84 | } 85 | 86 | in := &api.CreateSecretKeyRequest{ 87 | EncryptedFor: encryptedFor, 88 | } 89 | 90 | resp, err := c.httpClient.CreateSecretKey(blindName, in) 91 | if err == nil { 92 | accountKey, err := c.getAccountKey() 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return resp.Decrypt(accountKey) 98 | } 99 | if !errio.EqualsAPIError(api.ErrNotEncryptedForAccounts, err) { 100 | return nil, err 101 | } 102 | if tries >= missingMemberRetries { 103 | return nil, fmt.Errorf("cannot create secret key: access rules giving access to the secret (key) are simultaneously being created; you may try again") 104 | } 105 | tries++ 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /pkg/secrethub/user.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "github.com/secrethub/secrethub-go/internals/api" 5 | "github.com/secrethub/secrethub-go/internals/crypto" 6 | "github.com/secrethub/secrethub-go/internals/errio" 7 | "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" 8 | ) 9 | 10 | // UserService handles operations on users from SecretHub. 11 | type UserService interface { 12 | // Me gets the account's user if it exists. 13 | Me() (*api.User, error) 14 | // Get a user by their username. 15 | Get(username string) (*api.User, error) 16 | } 17 | 18 | func newUserService(client *Client) UserService { 19 | return userService{ 20 | client: client, 21 | } 22 | } 23 | 24 | type userService struct { 25 | client *Client 26 | } 27 | 28 | // Me gets the account's user if it exists. 29 | func (s userService) Me() (*api.User, error) { 30 | return s.client.httpClient.GetMyUser() 31 | } 32 | 33 | // Get retrieves the user with the given username from SecretHub. 34 | func (s userService) Get(username string) (*api.User, error) { 35 | err := api.ValidateUsername(username) 36 | if err != nil { 37 | return nil, errio.Error(err) 38 | } 39 | 40 | user, err := s.client.httpClient.GetUser(username) 41 | if err != nil { 42 | return nil, errio.Error(err) 43 | } 44 | 45 | return user, nil 46 | } 47 | 48 | // createAccountKey adds the account key for the clients credential. 49 | func (c *Client) createAccountKey(credentialFingerprint string, accountKey crypto.RSAPrivateKey, encrypter credentials.Encrypter) (*api.EncryptedAccountKey, error) { 50 | accountKeyRequest, err := c.createAccountKeyRequest(encrypter, accountKey) 51 | if err != nil { 52 | return nil, errio.Error(err) 53 | } 54 | 55 | err = accountKeyRequest.Validate() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | result, err := c.httpClient.CreateAccountKey(accountKeyRequest, credentialFingerprint) 61 | if err != nil { 62 | return nil, errio.Error(err) 63 | } 64 | return result, nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/secrethub/user_test.go: -------------------------------------------------------------------------------- 1 | package secrethub 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/go-chi/chi" 10 | "github.com/secrethub/secrethub-go/internals/api" 11 | "github.com/secrethub/secrethub-go/internals/api/uuid" 12 | 13 | "github.com/secrethub/secrethub-go/internals/assert" 14 | ) 15 | 16 | const ( 17 | username = "dev1" 18 | fullName = "Developer Uno" 19 | email = "dev1@testing.com" 20 | ) 21 | 22 | func TestGetUser(t *testing.T) { 23 | 24 | // Arrange 25 | router, opts, cleanup := setup() 26 | defer cleanup() 27 | 28 | userService := newUserService( 29 | Must(NewClient(opts...)), 30 | ) 31 | 32 | now := time.Now().UTC() 33 | expectedResponse := &api.User{ 34 | AccountID: uuid.New(), 35 | Username: username, 36 | FullName: fullName, 37 | Email: email, 38 | CreatedAt: &now, 39 | LastLoginAt: &now, 40 | } 41 | 42 | router.Get("/users/{username}", func(w http.ResponseWriter, r *http.Request) { 43 | // Assert 44 | usernameParam := chi.URLParam(r, "username") 45 | assert.Equal(t, usernameParam, username) 46 | 47 | // Respond 48 | w.Header().Set("Content-Type", "application/json") 49 | w.WriteHeader(http.StatusOK) 50 | _ = json.NewEncoder(w).Encode(expectedResponse) 51 | }) 52 | 53 | // Act 54 | actual, err := userService.Get(username) 55 | 56 | // Assert 57 | assert.OK(t, err) 58 | assert.Equal(t, actual, expectedResponse) 59 | } 60 | 61 | func TestGetUser_NotFound(t *testing.T) { 62 | 63 | // Arrange 64 | router, opts, cleanup := setup() 65 | defer cleanup() 66 | 67 | userService := newUserService( 68 | Must(NewClient(opts...)), 69 | ) 70 | 71 | expected := api.ErrUserNotFound 72 | 73 | router.Get("/users/{username}", func(w http.ResponseWriter, r *http.Request) { 74 | // Respond 75 | w.Header().Set("Content-Type", "application/json") 76 | w.WriteHeader(expected.StatusCode) 77 | _ = json.NewEncoder(w).Encode(expected) 78 | }) 79 | 80 | // Act 81 | _, err := userService.Get("dev1") 82 | 83 | // Assert 84 | assert.Equal(t, err, expected) 85 | } 86 | 87 | func TestGetUser_InvalidArgument(t *testing.T) { 88 | 89 | // Arrange 90 | _, opts, cleanup := setup() 91 | defer cleanup() 92 | 93 | userService := newUserService( 94 | Must(NewClient(opts...)), 95 | ) 96 | 97 | // Act 98 | _, err := userService.Get("invalidname$#@%%") 99 | 100 | // Assert 101 | assert.Equal(t, err, api.ErrInvalidUsername) 102 | } 103 | 104 | func TestGetMyUser(t *testing.T) { 105 | 106 | // Arrange 107 | router, opts, cleanup := setup() 108 | defer cleanup() 109 | 110 | userService := newUserService( 111 | Must(NewClient(opts...)), 112 | ) 113 | 114 | now := time.Now().UTC() 115 | expected := &api.User{ 116 | AccountID: uuid.New(), 117 | Username: username, 118 | FullName: fullName, 119 | Email: email, 120 | CreatedAt: &now, 121 | LastLoginAt: &now, 122 | } 123 | 124 | router.Get("/me/user", func(w http.ResponseWriter, r *http.Request) { 125 | // Respond 126 | w.Header().Set("Content-Type", "application/json") 127 | w.WriteHeader(http.StatusOK) 128 | _ = json.NewEncoder(w).Encode(expected) 129 | }) 130 | 131 | // Act 132 | actual, err := userService.Me() 133 | 134 | // Assert 135 | assert.OK(t, err) 136 | assert.Equal(t, actual, expected) 137 | } 138 | 139 | func TestGetMyUser_NotFound(t *testing.T) { 140 | 141 | // Arrange 142 | router, opts, cleanup := setup() 143 | defer cleanup() 144 | 145 | userService := newUserService( 146 | Must(NewClient(opts...)), 147 | ) 148 | 149 | expected := api.ErrRequestNotAuthenticated 150 | 151 | router.Get("/me/user", func(w http.ResponseWriter, r *http.Request) { 152 | // Respond 153 | w.Header().Set("Content-Type", "application/json") 154 | w.WriteHeader(expected.StatusCode) 155 | _ = json.NewEncoder(w).Encode(expected) 156 | }) 157 | 158 | // Act 159 | _, err := userService.Me() 160 | 161 | // Assert 162 | assert.Equal(t, err, expected) 163 | } 164 | -------------------------------------------------------------------------------- /scripts/check-version/check-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 4 | RELEASE_PREFIX="release/" 5 | 6 | if [[ $BRANCH == ${RELEASE_PREFIX}* ]] 7 | then 8 | VERSION=${BRANCH#"$RELEASE_PREFIX"} 9 | go run ./scripts/check-version/main.go "${VERSION}" 10 | else 11 | echo "Not on a release branch, skipping version check." 12 | fi 13 | -------------------------------------------------------------------------------- /scripts/check-version/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/secrethub/secrethub-go/pkg/secrethub" 8 | ) 9 | 10 | func main() { 11 | expected := os.Args[1] 12 | actual := secrethub.ClientVersion 13 | 14 | if actual != expected { 15 | fmt.Fprintf(os.Stderr, "version not as expected: expected %s got %s\n", expected, actual) 16 | os.Exit(1) 17 | } else { 18 | fmt.Println("version as expected") 19 | } 20 | } 21 | --------------------------------------------------------------------------------