├── _config.yml ├── keymanager ├── fixtures │ ├── keyid.txt │ └── eckey.pem ├── mocks │ └── testmockkeymanager.go ├── keymanager_test.go └── keymanager.go ├── circle.yml ├── .gitignore ├── resources └── 331px-Sputnik-stamp-ussr.jpg ├── requesthandling ├── fixtures │ └── test_identity.der ├── payload.go ├── requestconfig_test.go ├── requestconfig.go ├── signing_test.go ├── requestmanager_test.go └── requestmanager.go ├── sputnik.go ├── cmd ├── fixtures │ ├── query_sample_payload.json │ └── modify_sample_payload.json ├── commands_test.go ├── keyid.go ├── store.go ├── requests.go ├── print.go ├── create.go ├── identity.go ├── get.go ├── root.go ├── identitydelete.go └── post.go ├── main.go ├── bitbucket-pipelines.yml ├── jenkinsfile ├── LICENSE └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /keymanager/fixtures/keyid.txt: -------------------------------------------------------------------------------- 1 | abc 2 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - go test ./... -v 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | */.DS_Store 3 | sputnik 4 | go.mod 5 | go.sum 6 | -------------------------------------------------------------------------------- /resources/331px-Sputnik-stamp-ussr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q231950/sputnik/HEAD/resources/331px-Sputnik-stamp-ussr.jpg -------------------------------------------------------------------------------- /requesthandling/fixtures/test_identity.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q231950/sputnik/HEAD/requesthandling/fixtures/test_identity.der -------------------------------------------------------------------------------- /requesthandling/payload.go: -------------------------------------------------------------------------------- 1 | package requesthandling 2 | 3 | // Payload represents the payload to send with a request 4 | type Payload struct { 5 | Fields map[string]interface{} 6 | } 7 | -------------------------------------------------------------------------------- /sputnik.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/apex/log" 4 | 5 | // Hello makes sure that the earth is still spinning around the sun 6 | func Hello() { 7 | log.Debug("This is спутник.") 8 | } 9 | -------------------------------------------------------------------------------- /keymanager/fixtures/eckey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIH7QO3OzlxJ8OqoVRgBBHJK/iQB1s22MiWLO5WNzNjoioAoGCCqGSM49 3 | AwEHoUQDQgAEuTDvRjrfQW7CHI68iJBpBpRkhT0JKMKA7vaSPvkckv9za3l1ji2L 4 | 0VsFSOIvSlgpUyC96pRxcIBR/E2gqmLbbA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /cmd/fixtures/query_sample_payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "query":{ 3 | "recordType":"City", 4 | "filterBy":[ 5 | { 6 | "comparator":"EQUALS", 7 | "fieldName":"name", 8 | "fieldValue":{ 9 | "value":"La Citta Nel Cielo" 10 | } 11 | } 12 | ] 13 | }, 14 | "resultsLimit":1 15 | } 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/apex/log" 7 | "github.com/apex/log/handlers/cli" 8 | 9 | "github.com/q231950/sputnik/cmd" 10 | ) 11 | 12 | func main() { 13 | 14 | log.SetHandler(cli.New(os.Stderr)) 15 | log.SetLevel(log.InfoLevel) 16 | 17 | if err := cmd.RootCmd.Execute(); err != nil { 18 | log.Errorf("%s", err) 19 | os.Exit(-1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bitbucket-pipelines.yml: -------------------------------------------------------------------------------- 1 | image: golang:1.8 2 | 3 | pipelines: 4 | default: 5 | - step: 6 | script: 7 | - PACKAGE_PATH="${GOPATH}/src/bitbucket.org/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}" 8 | - mkdir -pv "${PACKAGE_PATH}" 9 | - tar -cO --exclude-vcs --exclude=bitbucket-pipelines.yml . | tar -xv -C "${PACKAGE_PATH}" 10 | - cd "${PACKAGE_PATH}" 11 | - go get -v -t -u 12 | - go build -v 13 | - go test ./... -v 14 | -------------------------------------------------------------------------------- /requesthandling/requestconfig_test.go: -------------------------------------------------------------------------------- 1 | package requesthandling 2 | 3 | import "testing" 4 | 5 | func TestRequestConfigInit(t *testing.T) { 6 | config := NewRequestConfig("3", "com.test.go", "public") 7 | if config.Version != "3" { 8 | t.Errorf("Request Config Version has not been initialised correctly") 9 | } 10 | 11 | if config.ContainerID != "com.test.go" { 12 | t.Errorf("Request Container ID has not been initialised correctly") 13 | } 14 | 15 | if config.Database != "public" { 16 | t.Errorf("Request Database has not been initialised correctly") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /requesthandling/requestconfig.go: -------------------------------------------------------------------------------- 1 | package requesthandling 2 | 3 | // RequestConfig is used to initialise RequestManagers. It specifies the Cloudkit API version and container ID to use for requests 4 | type RequestConfig struct { 5 | Version string 6 | ContainerID string 7 | Database string 8 | } 9 | 10 | // NewRequestConfig creates a fresh config with the given version and container ID 11 | func NewRequestConfig(version string, containerID string, database string) RequestConfig { 12 | return RequestConfig{Version: version, ContainerID: containerID, Database: database} 13 | } 14 | -------------------------------------------------------------------------------- /keymanager/mocks/testmockkeymanager.go: -------------------------------------------------------------------------------- 1 | package keymanager 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | ) 8 | 9 | type MockKeyManager struct { 10 | } 11 | 12 | func (m MockKeyManager) PublicKey() *ecdsa.PublicKey { 13 | return nil 14 | } 15 | 16 | func (m MockKeyManager) PrivateKey() *ecdsa.PrivateKey { 17 | c := elliptic.P256() 18 | key, _ := ecdsa.GenerateKey(c, rand.Reader) 19 | return key 20 | } 21 | 22 | func (m MockKeyManager) KeyID() string { 23 | return "key id" 24 | } 25 | 26 | func (m MockKeyManager) RemoveSigningIdentity() error { 27 | return nil 28 | } 29 | 30 | func (m MockKeyManager) StoreKeyID(key string) error { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /cmd/commands_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRootCommand(t *testing.T) { 10 | flag := RootCmd.Flag("config") 11 | assert.NotNil(t, flag) 12 | } 13 | 14 | func TestRequestsCommand(t *testing.T) { 15 | run := requestsCmd.Run 16 | assert.NotNil(t, run) 17 | } 18 | 19 | func TestPostRequestCommand(t *testing.T) { 20 | run := postCmd.Run 21 | assert.NotNil(t, run) 22 | } 23 | 24 | func TestPostRequestCommandJSONFlag(t *testing.T) { 25 | flag := postCmd.Flag("json-file-path") 26 | assert.NotNil(t, flag) 27 | } 28 | 29 | func TestPostRequestCommandPayloadFlag(t *testing.T) { 30 | flag := postCmd.Flag("payload") 31 | assert.NotNil(t, flag) 32 | } 33 | 34 | func TestPostRequestCommandOperationFlag(t *testing.T) { 35 | flag := postCmd.Flag("operation") 36 | assert.NotNil(t, flag) 37 | } 38 | -------------------------------------------------------------------------------- /jenkinsfile: -------------------------------------------------------------------------------- 1 | node { 2 | ws("${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_ID}/src/github.com/q231950/sputnik") { 3 | def root = tool name: 'Go 1.9.2', type: 'go' 4 | withEnv(["GOROOT=${root}", "GOPATH=${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_ID}/", "PATH+GO=${root}/bin"]) { 5 | env.PATH="${GOPATH}/bin:$PATH" 6 | 7 | stage("Clone") { 8 | checkout scm 9 | } 10 | 11 | stage("Get Dependencies") { 12 | sh 'go get -v' 13 | } 14 | 15 | stage("Get Test Dependencies") { 16 | sh "go get -u github.com/stretchr/testify/assert" 17 | sh "go get -u github.com/jstemmer/go-junit-report" 18 | } 19 | 20 | stage("Test") { 21 | sh 'go test ./... -v 2>&1 | go-junit-report > report.xml' 22 | } 23 | 24 | stage("Publish Results") { 25 | junit '**/report.xml' 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cmd/fixtures/modify_sample_payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "operations":[ 3 | { 4 | "operationType":"create", 5 | "record":{ 6 | "recordType":"City", 7 | "fields":{ 8 | "geonameid":{ 9 | "value":"my id" 10 | }, 11 | "name":{ 12 | "value":"La Citta Nel Cielo" 13 | }, 14 | "alternatenames":{ 15 | "value":"" 16 | }, 17 | "location":{ 18 | "value":{ 19 | "latitude":40.0, 20 | "longitude":10.0 21 | } 22 | }, 23 | "countrycode":{ 24 | "value":"AU" 25 | }, 26 | "population":{ 27 | "value":500 28 | }, 29 | "elevation":{ 30 | "value":800 31 | }, 32 | "timezone":{ 33 | "value":"some" 34 | } 35 | } 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Martin Kim Dung-Pham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/bb/q231950/sputnik/tree/master.svg?style=svg)](https://circleci.com/bb/q231950/sputnik/tree/master) ![Go Report Card](https://goreportcard.com/badge/github.com/q231950/sputnik) 2 | 3 | # 🛰 спутник 4 | 5 | ## Talk to  CloudKit. Server-to-Server in Go. 6 | 7 | > **Sputnik** enables you to connect to CloudKit from within your Go package or application using CloudKit's Server-to-Server Web Service API. **Sputnik** handles request signing for you and offers ways to interact with CloudKit directly from the CLI. 8 | 9 | ### Signing Requests 10 | 11 | Sputnik manages the most cumbersome part of CloudKit communication - the signing of your requests. For more information on signing have a look in the [Managing the Signing Identity](https://github.com/q231950/sputnik/wiki/Managing-the-Signing-Identity) section of the [Wiki](https://github.com/q231950/sputnik/wiki). 12 | 13 | ### Usage 14 | 15 | You can use Sputnik either from [the command line](https://github.com/q231950/sputnik/wiki/Sending-Requests#the-sputnik-binary) or [as a package](https://github.com/q231950/sputnik/wiki/Sending-Requests#the-sputnik-package). For more information about requests have a look in the [Sending Requests](https://github.com/q231950/sputnik/wiki/Sending-Requests) section of the [Wiki](https://github.com/q231950/sputnik/wiki). 16 | 17 | [Baikonur](https://github.com/q231950/baikonur) uses Sputnik to insert city records into a CloudKit container: 18 | 19 | ```go 20 | keyManager := keymanager.New() 21 | 22 | config := requesthandling.RequestConfig{Version: "1", ContainerID: "iCloud.com.some.bundle", Database: "public"} 23 | requestManager := requesthandling.New(config, &keyManager) 24 | 25 | request, error := requestManager.PostRequest("modify", json) 26 | client := &http.Client{} 27 | response, error := client.Do(request) 28 | ``` 29 | 30 | ## State 31 | 32 | Please try this package and see how it works for you. Feedback and contributions are welcome <3 33 | 34 | ![Gemeinfrei, Link](resources/331px-Sputnik-stamp-ussr.jpg) 35 | -------------------------------------------------------------------------------- /cmd/keyid.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | log "github.com/apex/log" 25 | 26 | "github.com/q231950/sputnik/keymanager" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // keyidCmd represents the keyid command 31 | var keyidCmd = &cobra.Command{ 32 | Use: "keyid", 33 | Short: "Show the key ID that is currently in use", 34 | Long: `You can provide a Cloudkit key ID by 2 methods 35 | #1 use the Sputnik command 'keyid store ' 36 | #2 by setting an environment variable 'SPUTNIK_CLOUDKIT_KEYID'`, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | log.Info("Attempting to retrieve the current key id...") 39 | keyManager := keymanager.New() 40 | keyID := keyManager.KeyID() 41 | if len(keyID) > 0 { 42 | log.Infof("The following key id is stored: `%s`", keyID) 43 | } else { 44 | log.Error("No iCloud key id specified. Please either provide one by `sputnik keyid store ` or set the environment variable `SPUTNIK_CLOUDKIT_KEYID`.") 45 | } 46 | }, 47 | } 48 | 49 | func init() { 50 | RootCmd.AddCommand(keyidCmd) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/store.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | log "github.com/apex/log" 25 | "github.com/q231950/sputnik/keymanager" 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | // storeCmd represents the store command 30 | var storeCmd = &cobra.Command{ 31 | Use: "store", 32 | Short: "Stores the Key ID from the Cloudkit Dashboard.", 33 | Long: `After providing the Cloudkit Dashboard with the public key, the keyId is generated. 34 | Use this command to store the key ID in Sputnik's secrets folder in ~/.sputnik/secrets/`, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | if len(args) == 1 { 37 | keyID := args[0] 38 | keyManager := keymanager.New() 39 | err := keyManager.StoreKeyID(keyID) 40 | if err != nil { 41 | log.Errorf("Failed to store kei id (%s)", err) 42 | } else { 43 | log.Infof("The following key id has been stored: `%s`", keyID) 44 | } 45 | } else { 46 | log.Error("`keyid store` requires one argument which is the key id to store.") 47 | } 48 | }, 49 | } 50 | 51 | func init() { 52 | keyidCmd.AddCommand(storeCmd) 53 | } 54 | -------------------------------------------------------------------------------- /cmd/requests.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "io/ioutil" 26 | 27 | "github.com/apex/log" 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | var payloadFilePath string 32 | var payload string 33 | var operation string 34 | var container string 35 | 36 | // requestsCmd represents the requests command 37 | var requestsCmd = &cobra.Command{ 38 | Use: "requests", 39 | Short: "A brief description of your command", 40 | Long: `A longer description that spans multiple lines and likely contains examples 41 | and usage of using your command. For example: 42 | 43 | Cobra is a CLI library for Go that empowers applications. 44 | This application is a tool to generate the needed files 45 | to quickly create a Cobra application.`, 46 | Run: func(cmd *cobra.Command, args []string) { 47 | fmt.Println("requests called") 48 | }, 49 | } 50 | 51 | func init() { 52 | RootCmd.AddCommand(requestsCmd) 53 | } 54 | 55 | func payloadFromFile(path string) string { 56 | bytes, err := ioutil.ReadFile(path) 57 | if err != nil { 58 | log.Error(err.Error()) 59 | } 60 | return string(bytes) 61 | } 62 | -------------------------------------------------------------------------------- /requesthandling/signing_test.go: -------------------------------------------------------------------------------- 1 | package requesthandling 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | keymanager "github.com/q231950/sputnik/keymanager/mocks" 8 | ) 9 | 10 | type TestableRequestManager interface { 11 | RequestManager 12 | HashedBody(body string) string 13 | } 14 | 15 | func TestPostRequestDateParameterIsInPerimeter(t *testing.T) { 16 | keyManager := keymanager.MockKeyManager{} 17 | config := RequestConfig{Version: "1", ContainerID: "containerid", Database: "public"} 18 | requestManager := New(config, &keyManager) 19 | request, _ := requestManager.PostRequest("users/caller", "") 20 | dateString := request.Header.Get("X-Apple-CloudKit-Request-ISO8601Date") 21 | 22 | expectedTime := time.Now().UTC() 23 | roundedExpectedTime := expectedTime.Round(time.Minute) 24 | 25 | actualTime, _ := time.Parse(time.RFC3339, dateString) 26 | roundedTime := actualTime.Round(time.Minute) 27 | 28 | if !roundedExpectedTime.Equal(roundedTime) { 29 | t.Errorf("The date parameter must not differ by more than a minute from now") 30 | } 31 | } 32 | 33 | func TestMessageFormat(t *testing.T) { 34 | keyManager := keymanager.MockKeyManager{} 35 | config := RequestConfig{Version: "1", ContainerID: "containerid", Database: "public"} 36 | requestManager := New(config, &keyManager) 37 | message := requestManager.message("date", "body", "service url") 38 | if message != "date:body:service url" { 39 | t.Errorf("The request payload needs to be properly formatted") 40 | } 41 | } 42 | 43 | func TestEmptyHashedBody(t *testing.T) { 44 | keyManager := keymanager.MockKeyManager{} 45 | config := NewRequestConfig("version", "containerID", "public") 46 | requestManager := TestableRequestManager(&CloudkitRequestManager{config, &keyManager}) 47 | body := "" 48 | hash := requestManager.HashedBody(body) 49 | 50 | if string(hash) != "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" { 51 | t.Errorf("Signature is not correct") 52 | } 53 | } 54 | 55 | func TestSignMessage(t *testing.T) { 56 | keyManager := keymanager.MockKeyManager{} 57 | config := NewRequestConfig("version", "containerID", "public") 58 | r := New(config, &keyManager) 59 | signature := r.SignatureForMessage([]byte("message")) 60 | 61 | if signature == nil { 62 | t.Errorf("A message should be signed when a private key is available") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cmd/print.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | log "github.com/apex/log" 25 | 26 | "github.com/q231950/sputnik/keymanager" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // printCmd represents the print command 31 | var printCmd = &cobra.Command{ 32 | Use: "print", 33 | Short: "Print the public key", 34 | Long: `Use this command to get the public key to paste into the CloudKit Dashboard when granting API access.`, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | log.Info("Attempting to print the public key") 37 | 38 | keyManager := keymanager.New() 39 | exists, err := keyManager.SigningIdentityExists() 40 | if err != nil { 41 | log.Errorf("Error in SigningIdentityExists: %s", err) 42 | } 43 | if exists { 44 | log.Infof("Printing the public key\n%s", keyManager.PublicKeyString()) 45 | } else { 46 | log.Info("The ec key does not exist, need to create, one moment, please") 47 | keyManager.CreateSigningIdentity() 48 | 49 | log.Infof("Ok done. This is it: \n%s", keyManager.PublicKeyString()) 50 | } 51 | }, 52 | } 53 | 54 | func init() { 55 | eckeyCmd.AddCommand(printCmd) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | log "github.com/apex/log" 25 | 26 | "github.com/q231950/sputnik/keymanager" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // createCmd represents the create command 31 | var createCmd = &cobra.Command{ 32 | Use: "create", 33 | Short: "Creates a new signing identity", 34 | Long: `For now, a file named eckey.pem will be put into the secrets folder.`, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | log.Infof("Attempting to create a new identity...") 37 | createECKey() 38 | }, 39 | } 40 | 41 | func init() { 42 | eckeyCmd.AddCommand(createCmd) 43 | } 44 | 45 | func createECKey() { 46 | keyManager := keymanager.New() 47 | exists, err := keyManager.SigningIdentityExists() 48 | if err != nil { 49 | log.Errorf("Error in SigningIdentityExists: %s", err) 50 | } 51 | 52 | if exists { 53 | log.Error("There is an existing identity. You need to remove it with `./sputnik identity remove` before you can create a new one.") 54 | if len(keyManager.KeyID()) != 0 { 55 | // a key ID has been stored 56 | log.Infof("The current identity is linked with the following iCloud key ID:\n%s", keyManager.KeyID()) 57 | } else { 58 | log.Infof("This is the current identity:\n%s", keyManager.ECKey()) 59 | } 60 | } else { 61 | log.Info("Creating an identity") 62 | keyManager.CreateSigningIdentity() 63 | log.Info("Done") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cmd/identity.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | log "github.com/apex/log" 25 | 26 | "github.com/q231950/sputnik/keymanager" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // The identity command shows the current identity. 31 | var eckeyCmd = &cobra.Command{ 32 | Use: "identity", 33 | Short: "Show the signing identity", 34 | Long: `Show the signing identity that is used for signing the iCloud requests.`, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | log.Debug("Attempting to retrieve the current identity...") 37 | keyManager := keymanager.New() 38 | keyExists, err := keyManager.SigningIdentityExists() 39 | if err != nil { 40 | log.Errorf("Error in SigningIdentityExists: %s", err) 41 | } 42 | 43 | if keyExists { 44 | identity := keyManager.ECKey() 45 | log.Debug("The current identity you can create a new server-to-server key with in the iCloud Dashboard:") 46 | log.Infof("\n%s", identity) 47 | 48 | keyID := keyManager.KeyID() 49 | if len(keyID) == 0 { 50 | log.Error("No iCloud KeyID specified. Please either provide one by `sputnik keyid store ` or set the environment variable `SPUTNIK_CLOUDKIT_KEYID`.") 51 | } 52 | } else { 53 | log.Error("A signing identity could not be found. A signing identity can be created by `./sputnik identity create`") 54 | } 55 | }, 56 | } 57 | 58 | func init() { 59 | RootCmd.AddCommand(eckeyCmd) 60 | } 61 | -------------------------------------------------------------------------------- /cmd/get.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "github.com/apex/log" 25 | "github.com/q231950/sputnik/keymanager" 26 | "github.com/q231950/sputnik/requesthandling" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // getCmd represents the get command 31 | var getCmd = &cobra.Command{ 32 | Use: "get", 33 | Short: "A brief description of your command", 34 | Long: `A longer description that spans multiple lines and likely contains examples 35 | and usage of using your command. For example: 36 | 37 | Cobra is a CLI library for Go that empowers applications. 38 | This application is a tool to generate the needed files 39 | to quickly create a Cobra application.`, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | log.WithField("Payload", payload).Info("Attempting to GET...") 42 | keyManager := keymanager.New() 43 | config := requesthandling.RequestConfig{} 44 | requestManager := requesthandling.New(config, &keyManager) 45 | requestManager.GetRequest("lookup", "{}") 46 | }, 47 | } 48 | 49 | func init() { 50 | requestsCmd.AddCommand(getCmd) 51 | 52 | getCmd.Flags().StringVarP(&payloadFilePath, "json-file-path", "j", "", "A path to a file that contains the json payload") 53 | getCmd.Flags().StringVarP(&payload, "payload", "p", "", "A json payload as string") 54 | getCmd.Flags().StringVarP(&operation, "operation", "o", "", "The operation to execute: Depending on your intention, operation-specific subpaths may be of [modify, query, lookup, changes, resolve, accept]") 55 | } 56 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "os" 25 | 26 | log "github.com/apex/log" 27 | 28 | "github.com/spf13/cobra" 29 | "github.com/spf13/viper" 30 | ) 31 | 32 | var cfgFile string 33 | 34 | // RootCmd represents the base command when called without any subcommands 35 | var RootCmd = &cobra.Command{ 36 | Use: "sputnik", 37 | Short: "sputnik talks to CloudKit", 38 | Long: `спутник talks to CloudKit: 39 | 40 | Easily communicate server to server using CloudKit in the app and Go in your backend.️`, 41 | } 42 | 43 | // Execute adds all child commands to the root command sets flags appropriately. 44 | // This is called by main.main(). It only needs to happen once to the rootCmd. 45 | func Execute() { 46 | if err := RootCmd.Execute(); err != nil { 47 | log.Errorf("%s", err) 48 | os.Exit(-1) 49 | } 50 | } 51 | 52 | func init() { 53 | cobra.OnInitialize(initConfig) 54 | 55 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sputnik.yaml)") 56 | } 57 | 58 | // initConfig reads in config file and ENV variables if set. 59 | func initConfig() { 60 | if cfgFile != "" { // enable ability to specify config file via flag 61 | viper.SetConfigFile(cfgFile) 62 | } 63 | 64 | viper.SetConfigName(".sputnik") // name of config file (without extension) 65 | viper.AddConfigPath("$HOME") // adding home directory as first search path 66 | viper.AutomaticEnv() // read in environment variables that match 67 | 68 | // If a config file is found, read it in. 69 | if err := viper.ReadInConfig(); err == nil { 70 | log.Debugf("Using config file: `%s`", viper.ConfigFileUsed()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cmd/identitydelete.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | log "github.com/apex/log" 25 | "github.com/q231950/sputnik/keymanager" 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | // identitydeleteCmd represents the identitydelete command 30 | var identitydeleteCmd = &cobra.Command{ 31 | Use: "remove", 32 | Short: "Removes the signing identity", 33 | Long: ` 34 | This command is destructive! 35 | 36 | 'remove' removes the current signing identity. This makes the key ID in the Cloudkit Dashboard useless. After running this command you should also revoke the key ID in the matching container in your https://icloud.developer.apple.com/dashboard/.`, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | log.Info("Attempting to remove the current identity...") 39 | keyManager := keymanager.New() 40 | exists, err := keyManager.SigningIdentityExists() 41 | if err != nil { 42 | log.Errorf("Error in SigningIdentityExists: %s", err) 43 | } 44 | if exists { 45 | removeSigningIdentity(&keyManager) 46 | } else { 47 | log.Warn("There is no signing identity to remove.") 48 | } 49 | }, 50 | } 51 | 52 | func removeSigningIdentity(keyManager keymanager.KeyManager) { 53 | pub := keyManager.PublicKey() 54 | keyID := keyManager.KeyID() 55 | err := keyManager.RemoveSigningIdentity() 56 | if err != nil { 57 | log.Errorf("An error occurred while removing the signing identity (%s)", err) 58 | } else { 59 | log.Info("Your signing identity has been removed. Make sure to revoke the corresponding KeyID in the Cloudkit Dashboard.") 60 | log.Infof("The identity with the following public key was removed:\n%s", pub) 61 | log.Infof("The following key ID is now useless (unless you kept a copy of the private key somewhere outside of Sputnik):\n%s", keyID) 62 | } 63 | } 64 | 65 | func init() { 66 | eckeyCmd.AddCommand(identitydeleteCmd) 67 | } 68 | -------------------------------------------------------------------------------- /requesthandling/requestmanager_test.go: -------------------------------------------------------------------------------- 1 | package requesthandling 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | mocks "github.com/q231950/sputnik/keymanager/mocks" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // This Example shows how to create a request manager. 14 | // 15 | // A request manager requires a keymanager for handling authentication as well as a valid configuration. 16 | func ExampleRequestManager() { 17 | keyManager := mocks.MockKeyManager{} 18 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.mycontainer", Database: "public"} 19 | requestManager := New(config, &keyManager) 20 | fmt.Printf("Container:%s, Version:%s, Database:%s", 21 | requestManager.Config.ContainerID, 22 | requestManager.Config.Version, 23 | requestManager.Config.Database) 24 | // Output: Container:iCloud.com.mycontainer, Version:1, Database:public 25 | } 26 | 27 | func TestNewRequestManager(t *testing.T) { 28 | keyManager := mocks.MockKeyManager{} 29 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.elbedev.shelve.dev", Database: "public"} 30 | requestManager := New(config, keyManager) 31 | if requestManager.keyManager != keyManager { 32 | t.Errorf("A Request Manager's key manager should be the same that was used at initialisation") 33 | } 34 | } 35 | 36 | func TestPostRequest(t *testing.T) { 37 | keyManager := mocks.MockKeyManager{} 38 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.elbedev.shelve.dev", Database: "public"} 39 | requestManager := New(config, &keyManager) 40 | request, _ := requestManager.PostRequest("modify", "") 41 | 42 | assert.Equal(t, "/database/1/iCloud.com.elbedev.shelve.dev/development/public/records/modify", request.URL.Path) 43 | } 44 | 45 | func samplePostRequest() (*http.Request, error) { 46 | keyManager := mocks.MockKeyManager{} 47 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.elbedev.shelve.dev", Database: "public"} 48 | requestManager := New(config, &keyManager) 49 | return requestManager.request("some_operation", POST, `{"key":"value", "keys":["value1", "value2"]}`) 50 | } 51 | 52 | func TestRequest(t *testing.T) { 53 | request, err := samplePostRequest() 54 | assert.NotNil(t, request) 55 | assert.Nil(t, err) 56 | } 57 | 58 | func TestRequestMethod(t *testing.T) { 59 | request, _ := samplePostRequest() 60 | assert.Equal(t, "POST", request.Method) 61 | } 62 | 63 | func TestRequestDate(t *testing.T) { 64 | request, _ := samplePostRequest() 65 | dateString := request.Header.Get("X-Apple-CloudKit-Request-ISO8601Date") 66 | 67 | expectedTime := time.Now().UTC() 68 | roundedExpectedTime := expectedTime.Round(time.Minute) 69 | actualTime, _ := time.Parse(time.RFC3339, dateString) 70 | roundedTime := actualTime.Round(time.Minute) 71 | 72 | assert.Equal(t, roundedExpectedTime, roundedTime, "The date parameter must not differ by more than a minute from now") 73 | } 74 | 75 | func TestPayloadFormat(t *testing.T) { 76 | keyManager := mocks.MockKeyManager{} 77 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.elbedev.shelve.dev", Database: "public"} 78 | requestManager := New(config, &keyManager) 79 | message := requestManager.message("date", "body", "service url") 80 | if message != "date:body:service url" { 81 | t.Errorf("The request payload needs to be properly formatted") 82 | } 83 | } 84 | 85 | func TestRequestHost(t *testing.T) { 86 | request, _ := samplePostRequest() 87 | assert.Equal(t, "api.apple-cloudkit.com", request.URL.Host) 88 | } 89 | 90 | func TestRequestScheme(t *testing.T) { 91 | request, _ := samplePostRequest() 92 | assert.Equal(t, "https", request.URL.Scheme) 93 | } 94 | -------------------------------------------------------------------------------- /cmd/post.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Martin Kim Dung-Pham 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "encoding/json" 25 | "io/ioutil" 26 | "net/http" 27 | 28 | log "github.com/apex/log" 29 | "github.com/q231950/sputnik/keymanager" 30 | "github.com/q231950/sputnik/requesthandling" 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | // postCmd represents the post command 35 | var postCmd = &cobra.Command{ 36 | Use: "post", 37 | Short: "post allows you to send post requests with a given payload", 38 | Long: `post allows you to send post requests with a given payload: 39 | 40 | Here, a payload from file is post'ed with the operation modify 41 | ./sputnik requests post --operation "modify" --payload-file-path 'path/to/file.json' 42 | ./sputnik requests post -o "modify" --pf 'path/to/file.json' 43 | 44 | ./sputnik requests post --operation "modify" --payload '' 45 | ./sputnik requests post -o "modify" -p '' 46 | 47 | `, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | log.WithFields(log.Fields{ 50 | "Operation": operation}).Info("Attempting to POST...") 51 | 52 | if operation == "" { 53 | log.Error("Missing operation, please provide one. See `sputnik help requests post`") 54 | } 55 | 56 | var payloadToUse string 57 | if payloadFilePath != "" { 58 | payloadToUse = payloadFromFile(payloadFilePath) 59 | log.WithFields(log.Fields{ 60 | "Payload": payloadToUse}).Info("Payload from file") 61 | 62 | } else { 63 | payloadToUse = payload 64 | } 65 | 66 | keyManager := keymanager.New() 67 | 68 | if container != "" { 69 | config := requesthandling.RequestConfig{Version: "1", Database: "public", ContainerID: container} 70 | requestManager := requesthandling.New(config, &keyManager) 71 | 72 | request, err := requestManager.PostRequest(operation, payloadToUse) 73 | if err != nil { 74 | log.Error(err.Error()) 75 | } else { 76 | client := http.Client{} 77 | resp, err := client.Do(request) 78 | if err != nil { 79 | panic(err) 80 | } else { 81 | body, _ := ioutil.ReadAll(resp.Body) 82 | s, _ := json.MarshalIndent(string(body), "", " ") 83 | log.Info(string(s)) 84 | } 85 | } 86 | } else { 87 | log.Error("Missing container, please provide one. See `sputnik help requests post`") 88 | } 89 | }, 90 | } 91 | 92 | func init() { 93 | requestsCmd.AddCommand(postCmd) 94 | postCmd.Flags().StringVarP(&payloadFilePath, "json-file-path", "j", "", "A path to a file that contains the json payload") 95 | postCmd.Flags().StringVarP(&payload, "payload", "p", "", "A json payload as string") 96 | postCmd.Flags().StringVarP(&operation, "operation", "o", "", "The operation to execute: Depending on your intention, operation-specific subpaths may be of [modify, query, lookup, changes, resolve, accept]") 97 | postCmd.Flags().StringVarP(&container, "container", "c", "", "The CloudKit container to access. (normally `iCloud.your.bundle.identifier`)") 98 | } 99 | -------------------------------------------------------------------------------- /requesthandling/requestmanager.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package requesthandling offers means to create signed requests for interfacing with CloudKit. 3 | 4 | A Configuration is required to know the CloudKit container's properties, the KeyManager is required for signing requests. 5 | */ 6 | package requesthandling 7 | 8 | import ( 9 | "bytes" 10 | "crypto" 11 | "crypto/rand" 12 | "crypto/sha256" 13 | "encoding/base64" 14 | "net/http" 15 | "strings" 16 | "time" 17 | 18 | log "github.com/apex/log" 19 | "github.com/q231950/sputnik/keymanager" 20 | ) 21 | 22 | // The HTTPMethod defines the method of a request 23 | type HTTPMethod string 24 | 25 | const ( 26 | // GET represents HTTP GET 27 | GET HTTPMethod = "GET" 28 | // POST represents HTTP POST 29 | POST = "POST" 30 | // PUT represents HTTP PUT 31 | PUT = "PUT" 32 | ) 33 | 34 | // The RequestManager interface exposes methods for creating requests 35 | type RequestManager interface { 36 | PostRequest(string, string) (*http.Request, error) 37 | GetRequest(string, string) (*http.Request, error) 38 | } 39 | 40 | // CloudkitRequestManager is the concrete implementation of RequestManager 41 | type CloudkitRequestManager struct { 42 | Config RequestConfig 43 | keyManager keymanager.KeyManager 44 | } 45 | 46 | // New creates a new RequestManager 47 | func New(config RequestConfig, keyManager keymanager.KeyManager) CloudkitRequestManager { 48 | return CloudkitRequestManager{Config: config, keyManager: keyManager} 49 | } 50 | 51 | // PostRequest is a convenience method for creating POST requests 52 | func (cm CloudkitRequestManager) PostRequest(operationPath string, body string) (*http.Request, error) { 53 | return cm.request(operationPath, POST, body) 54 | } 55 | 56 | // GetRequest is a convenience method for creating POST requests 57 | func (cm CloudkitRequestManager) GetRequest(operationPath string, body string) (*http.Request, error) { 58 | return cm.request(operationPath, GET, body) 59 | } 60 | 61 | // Request creates a signed request with the given parameters 62 | func (cm *CloudkitRequestManager) request(p string, method HTTPMethod, payload string) (*http.Request, error) { 63 | keyID := cm.keyManager.KeyID() 64 | 65 | currentDate := cm.formattedTime(time.Now()) 66 | path := cm.subpath(p) 67 | hashedBody := cm.HashedBody(payload) 68 | message := cm.message(currentDate, hashedBody, path) 69 | signature := cm.SignatureForMessage([]byte(message)) 70 | encodedSignature := string(base64.StdEncoding.EncodeToString(signature)) 71 | url := "https://api.apple-cloudkit.com" + path 72 | 73 | log.WithFields(log.Fields{ 74 | "key id": keyID, 75 | "date": currentDate, 76 | "body": hashedBody, 77 | "base64 encoded signature": encodedSignature, 78 | "path": path}).Debug("Creating request") 79 | 80 | return cm.requestWithHeaders(string(method), url, []byte(payload), keyID, currentDate, encodedSignature) 81 | } 82 | 83 | // request creates a request with the given parameters. 84 | // - method POST/GET/... 85 | // - body is used as body for POST requests. 86 | // - url the request's endpoint 87 | // - keyID Header parameter X-Apple-CloudKit-Request-KeyID 88 | // - date Header parameter X-Apple-CloudKit-Request-ISO8601Date 89 | // - signature Header parameter X-Apple-CloudKit-Request-SignatureV1 90 | func (cm *CloudkitRequestManager) requestWithHeaders(method string, url string, body []byte, keyID string, date string, signature string) (request *http.Request, err error) { 91 | request, err = http.NewRequest(method, url, bytes.NewBuffer(body)) 92 | request.Header.Set("X-Apple-CloudKit-Request-KeyID", keyID) 93 | request.Header.Set("X-Apple-CloudKit-Request-ISO8601Date", date) 94 | request.Header.Set("X-Apple-CloudKit-Request-SignatureV1", signature) 95 | log.WithField("url", request.URL).Debug("Added headers to request") 96 | return request, err 97 | } 98 | 99 | // SignatureForMessage returns the signature for the given message 100 | func (cm *CloudkitRequestManager) SignatureForMessage(message []byte) (signature []byte) { 101 | priv := cm.keyManager.PrivateKey() 102 | rand := rand.Reader 103 | 104 | h := sha256.New() 105 | h.Write([]byte(message)) 106 | 107 | opts := crypto.SHA256 108 | if priv != nil { 109 | signature, err := priv.Sign(rand, h.Sum(nil), opts) 110 | if err != nil { 111 | log.WithError(err).Error("Unable to sign message") 112 | } 113 | 114 | return signature 115 | } 116 | 117 | log.Fatal("Can't sign without a private key") 118 | 119 | return nil 120 | } 121 | 122 | func (cm *CloudkitRequestManager) subpath(path string) string { 123 | version := cm.Config.Version 124 | containerID := cm.Config.ContainerID 125 | components := []string{"/database", version, containerID, "development", cm.Config.Database, "records", path} 126 | return strings.Join(components, "/") 127 | } 128 | 129 | func (cm *CloudkitRequestManager) formattedTime(t time.Time) string { 130 | date := t.UTC().Format(time.RFC3339) 131 | return date 132 | } 133 | 134 | func (cm *CloudkitRequestManager) message(date string, payload string, path string) string { 135 | components := []string{date, payload, path} 136 | message := strings.Join(components, ":") 137 | return message 138 | } 139 | 140 | // HashedBody takes the given body, hashes it, using sha256 and returns the base64 encoded result 141 | func (cm *CloudkitRequestManager) HashedBody(body string) string { 142 | h := sha256.New() 143 | h.Write([]byte(body)) 144 | return base64.StdEncoding.EncodeToString([]byte(h.Sum(nil))) 145 | } 146 | -------------------------------------------------------------------------------- /keymanager/keymanager_test.go: -------------------------------------------------------------------------------- 1 | package keymanager 2 | 3 | import ( 4 | "math/big" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNew(t *testing.T) { 12 | manager := New() 13 | assert.NotNil(t, manager, "A newly created key manager should not be nil") 14 | } 15 | 16 | func TestStoredKeyID(t *testing.T) { 17 | pathToFixtures := "./fixtures" 18 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 19 | assert.Equal(t, "abc\n", manager.KeyID(), "A stored key id should get found") 20 | } 21 | 22 | func TestPrivateKey(t *testing.T) { 23 | pathToFixtures := "./fixtures" 24 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 25 | 26 | expectedD := new(big.Int) 27 | expectedD.SetString("57359333433306843951573675484597381433848383364258304847182053853963006392866", 10) 28 | assert.Equal(t, expectedD, manager.PrivateKey().D, "The private key does not match the certificate") 29 | } 30 | 31 | func TestPrivateKeyFromMemory(t *testing.T) { 32 | pathToFixtures := "./fixtures" 33 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 34 | 35 | privateKey := manager.PrivateKey() 36 | privateKey = manager.PrivateKey() 37 | expectedD := new(big.Int) 38 | expectedD.SetString("57359333433306843951573675484597381433848383364258304847182053853963006392866", 10) 39 | assert.Equal(t, expectedD, privateKey.D, "The private key does not match the certificate") 40 | } 41 | 42 | func TestPublicKey(t *testing.T) { 43 | pathToFixtures := "./fixtures" 44 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 45 | 46 | publicKey := manager.PublicKey() 47 | expectedX := new(big.Int) 48 | expectedX.SetString("83764337057786748884235593670888306280068598385338703097790983192099570881279", 10) 49 | assert.Equal(t, expectedX, publicKey.X, "The public key's X does not match the certificate") 50 | 51 | expectedY := new(big.Int) 52 | expectedY.SetString("52205868503601725522780045291819081986668283346034058232083577220213552372588", 10) 53 | assert.Equal(t, expectedY, publicKey.Y, "The public key's Y does not match the certificate") 54 | } 55 | 56 | func TestPublicKeyFromMemory(t *testing.T) { 57 | pathToFixtures := "./fixtures" 58 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 59 | 60 | publicKey := manager.PublicKey() 61 | publicKey = manager.PublicKey() 62 | expectedX := new(big.Int) 63 | expectedX.SetString("83764337057786748884235593670888306280068598385338703097790983192099570881279", 10) 64 | assert.Equal(t, expectedX, publicKey.X, "The public key's X does not match the certificate") 65 | } 66 | 67 | func TestStoreKeyID(t *testing.T) { 68 | pathToFixtures := "./testFiles" 69 | manager := NewWithSecretsFolder(pathToFixtures, "keyidCreateTest.txt", "eckeyTest.pem") 70 | 71 | manager.StoreKeyID("key") 72 | assert.Equal(t, "key", manager.KeyID(), "The key id should be stored correctly") 73 | 74 | _ = os.RemoveAll("./testFiles") 75 | } 76 | 77 | func TestSigningIdentityExists(t *testing.T) { 78 | pathToFixtures := "./fixtures" 79 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 80 | exists, _ := manager.SigningIdentityExists() 81 | assert.True(t, exists, "The signing identity should exist in this fixture") 82 | } 83 | 84 | func TestECKey(t *testing.T) { 85 | pathToFixtures := "./fixtures" 86 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 87 | expected := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuTDvRjrfQW7CHI68iJBpBpRkhT0J\nKMKA7vaSPvkckv9za3l1ji2L0VsFSOIvSlgpUyC96pRxcIBR/E2gqmLbbA==\n-----END PUBLIC KEY-----\n" 88 | assert.Equal(t, expected, manager.ECKey(), "The public key is not correct") 89 | } 90 | 91 | func TestCreateIdentity(t *testing.T) { 92 | pathToFixtures := "./testFiles" 93 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 94 | err := manager.CreateSigningIdentity() 95 | assert.Nil(t, err, "It should be possible to create a signing identity") 96 | 97 | _ = os.RemoveAll("./testFiles") 98 | } 99 | 100 | func TestCreateIdentityPEMError(t *testing.T) { 101 | pathToFixtures := "./testFiles" 102 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "") 103 | err := manager.CreateSigningIdentity() 104 | assert.NotNil(t, err, "An error should occur when creating the signing identity fails due to a false pem file name") 105 | 106 | _ = os.RemoveAll("./testFiles") 107 | } 108 | 109 | func TestRemovesECKeyKeyWhenRemovingSigningIdentity(t *testing.T) { 110 | pathToFixtures := "./testFiles" 111 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 112 | _ = manager.CreateSigningIdentity() 113 | 114 | manager.RemoveSigningIdentity() 115 | 116 | assert.Equal(t, "", manager.ECKey(), "The public key should be gone after deleting the signing identity") // TODO get this right 117 | 118 | _ = os.RemoveAll("./testFiles") 119 | } 120 | 121 | func TestRemovesPublicKeyWhenRemovingSigningIdentity(t *testing.T) { 122 | pathToFixtures := "./testFiles" 123 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 124 | _ = manager.CreateSigningIdentity() 125 | 126 | manager.RemoveSigningIdentity() 127 | 128 | assert.Nil(t, manager.PublicKey(), "The public key should be gone after deleting the signing identity") 129 | 130 | _ = os.RemoveAll("./testFiles") 131 | } 132 | 133 | func TestRemovesPrivateKeyWhenRemovngSigningIdentity(t *testing.T) { 134 | pathToFixtures := "./testFiles" 135 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem") 136 | _ = manager.CreateSigningIdentity() 137 | 138 | manager.RemoveSigningIdentity() 139 | 140 | assert.Nil(t, manager.PrivateKey(), "The private key should be gone after deleting the signing identity") 141 | 142 | _ = os.RemoveAll("./testFiles") 143 | } 144 | -------------------------------------------------------------------------------- /keymanager/keymanager.go: -------------------------------------------------------------------------------- 1 | package keymanager 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "errors" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "os/user" 12 | "strings" 13 | 14 | log "github.com/apex/log" 15 | ) 16 | 17 | // KeyIDEnvironmentVariableName is the constant used for identifying the Key ID environment variable 18 | const KeyIDEnvironmentVariableName = string("SPUTNIK_CLOUDKIT_KEYID") 19 | 20 | // KeyManager exposes methods for creating, reading and removing signing identity relevant keys and IDs 21 | type KeyManager interface { 22 | PublicKey() *ecdsa.PublicKey 23 | PrivateKey() *ecdsa.PrivateKey 24 | KeyID() string 25 | RemoveSigningIdentity() error 26 | StoreKeyID(key string) error 27 | } 28 | 29 | // CloudKitKeyManager is a concrete KeyManager 30 | type CloudKitKeyManager struct { 31 | secretsFolder string 32 | pemFileName string 33 | keyIDFileName string 34 | inMemoryKeyID string 35 | inMemoryPrivateKey *ecdsa.PrivateKey 36 | inMemoryPublicKey *ecdsa.PublicKey 37 | } 38 | 39 | // New returns a CloudKitKeyManager with the default secrets folder 40 | // By default, a CloudKitKeyManager expects the secrets in the .sputnik folder of the home directory 41 | func New() CloudKitKeyManager { 42 | homeDir := homeDir() 43 | components := []string{homeDir, ".sputnik", "secrets"} 44 | secretsFolder := strings.Join(components, "/") 45 | 46 | keyIDFileName := "keyid.txt" 47 | pemFileName := "eckey.pem" 48 | 49 | return NewWithSecretsFolder(secretsFolder, keyIDFileName, pemFileName) 50 | } 51 | 52 | // NewWithSecretsFolder returns a CloudKitKeyManager with a specific secrets folder 53 | // Use this to specify a different storage location from the default 54 | func NewWithSecretsFolder(secretsFolder string, keyIDFileName string, pemFileName string) CloudKitKeyManager { 55 | return CloudKitKeyManager{ 56 | secretsFolder: secretsFolder, 57 | pemFileName: pemFileName, 58 | keyIDFileName: keyIDFileName} 59 | } 60 | 61 | // KeyID looks up the CloudKit Key ID 62 | func (c *CloudKitKeyManager) KeyID() string { 63 | keyID := os.Getenv("SPUTNIK_CLOUDKIT_KEYID") 64 | if len(keyID) <= 0 { 65 | // no KeyID found in environment variables 66 | keyIDFromFile, err := c.storedKeyID() 67 | if err != nil { 68 | 69 | } 70 | return keyIDFromFile 71 | } 72 | return keyID 73 | } 74 | 75 | // StoreKeyID stores the given ID to a file in Sputnik's secrets folder 76 | func (c *CloudKitKeyManager) StoreKeyID(key string) error { 77 | path := c.keyIDFilePath() 78 | keyBytes := []byte(key) 79 | return ioutil.WriteFile(path, keyBytes, 0644) 80 | } 81 | 82 | // storedKeyID looks up the Key ID in a file 83 | func (c *CloudKitKeyManager) storedKeyID() (string, error) { 84 | if len(c.inMemoryKeyID) > 0 { 85 | return c.inMemoryKeyID, nil 86 | } 87 | 88 | path := c.keyIDFilePath() 89 | keyBytes, err := ioutil.ReadFile(path) 90 | if err != nil { 91 | return "", err 92 | } 93 | c.inMemoryKeyID = string(keyBytes) 94 | 95 | return c.inMemoryKeyID, nil 96 | } 97 | 98 | // PrivateKey returns the x509 private key that was generated when creating the signing identity 99 | func (c *CloudKitKeyManager) PrivateKey() *ecdsa.PrivateKey { 100 | if c.inMemoryPrivateKey != nil { 101 | return c.inMemoryPrivateKey 102 | } 103 | 104 | inPathPem := c.pemFilePath() 105 | command := exec.Command("openssl", "ec", "-outform", "der", "-in", inPathPem) 106 | bytes, _ := command.Output() 107 | 108 | privateKey, err := x509.ParseECPrivateKey(bytes) 109 | if err != nil { 110 | log.Error("Failed to parse private ec key from pem:") 111 | log.Errorf("%s", err) 112 | return nil 113 | } 114 | 115 | c.inMemoryPrivateKey = privateKey 116 | return c.inMemoryPrivateKey 117 | } 118 | 119 | // PublicKey returns the public key that was generated when creating the signing identity 120 | func (c *CloudKitKeyManager) PublicKey() *ecdsa.PublicKey { 121 | if c.inMemoryPublicKey != nil { 122 | return c.inMemoryPublicKey 123 | } 124 | 125 | var err error 126 | var pub interface{} 127 | pemString := c.PublicKeyString() 128 | pemData := []byte(pemString) 129 | block, _ := pem.Decode(pemData) 130 | if block == nil || block.Type != "PUBLIC KEY" { 131 | err = errors.New("failed to decode PEM block containing public key") 132 | } else { 133 | pub, err = x509.ParsePKIXPublicKey(block.Bytes) 134 | 135 | switch pub := pub.(type) { 136 | case *ecdsa.PublicKey: 137 | c.inMemoryPublicKey = pub 138 | return pub 139 | } 140 | } 141 | 142 | if err != nil { 143 | log.Error("unable to parse public key from certificate:") 144 | log.Errorf("%s", err) 145 | } 146 | 147 | return nil 148 | } 149 | 150 | // PublicKeyString should be named differently. It reads and returns the public part of the PEM encoded signing identity 151 | func (c *CloudKitKeyManager) PublicKeyString() string { 152 | ecKeyPath := c.pemFilePath() 153 | 154 | command := exec.Command("openssl", "ec", "-in", ecKeyPath, "-pubout") 155 | bytes, err := command.Output() 156 | if err != nil { 157 | log.Error("PublicKeyString") 158 | log.Error("Failed to read the public key from PEM") 159 | log.Errorf("%s", err) 160 | } 161 | return string(bytes) 162 | } 163 | 164 | // SigningIdentityExists checks if a signing identity has been created 165 | func (c *CloudKitKeyManager) SigningIdentityExists() (bool, error) { 166 | ecKeyPath := c.pemFilePath() 167 | 168 | file, openError := os.Open(ecKeyPath) 169 | defer file.Close() 170 | 171 | return file != nil && openError == nil, openError 172 | } 173 | 174 | // keyIdFilePath represents the path to the Key ID file 175 | func (c *CloudKitKeyManager) keyIDFilePath() string { 176 | secretsFolder := c.SecretsFolder() 177 | return secretsFolder + "/" + c.keyIDFileName 178 | } 179 | 180 | // pemFilePath represents the path to the PEM encoded certificate 181 | func (c *CloudKitKeyManager) pemFilePath() string { 182 | secretsFolder := c.SecretsFolder() 183 | return secretsFolder + "/" + c.pemFileName 184 | } 185 | 186 | // ECKey should be named differently. It returns the public part of the PEM encoded signing identity 187 | func (c *CloudKitKeyManager) ECKey() string { 188 | ecKeyPath := c.pemFilePath() 189 | 190 | command := exec.Command("openssl", "ec", "-in", ecKeyPath, "-pubout") 191 | 192 | bytes, err := command.Output() 193 | if err != nil { 194 | log.Error("ECKey") 195 | log.Error("Failed to read the public key from PEM") 196 | log.Errorf("%s", err) 197 | } 198 | return string(bytes) 199 | } 200 | 201 | // CreateSigningIdentity creates a new signing identity. 202 | // 203 | // You can paste the signing identity to your iCloud Dashboard when creating a new API Access Key. 204 | func (c *CloudKitKeyManager) CreateSigningIdentity() error { 205 | return c.createPemEncodedCertificate() 206 | } 207 | 208 | // RemoveSigningIdentity removes the existing signing identity 209 | func (c *CloudKitKeyManager) RemoveSigningIdentity() error { 210 | c.inMemoryPrivateKey = nil 211 | c.inMemoryPublicKey = nil 212 | 213 | removePemCommand := exec.Command("rm", c.pemFilePath()) 214 | err := removePemCommand.Run() 215 | if err != nil { 216 | log.Error("Unable to remove PEM:") 217 | log.Errorf("%s", err) 218 | } 219 | 220 | removeKeyIDCommand := exec.Command("rm", c.keyIDFilePath()) 221 | err = removeKeyIDCommand.Run() 222 | 223 | return err 224 | } 225 | 226 | // createPemEncodedCertificate creates the PEM encoded certificate and stores it 227 | func (c *CloudKitKeyManager) createPemEncodedCertificate() error { 228 | log.Debug("Creating PEM...") 229 | pemFilePath := c.pemFilePath() 230 | 231 | command := exec.Command("openssl", "ecparam", "-name", "prime256v1", "-genkey", "-noout", "-out", pemFilePath) 232 | err := command.Start() 233 | if err != nil { 234 | log.Error("Failed to create pem encoded certificate") 235 | log.Errorf("%s", err) 236 | } 237 | 238 | err = command.Wait() 239 | if err != nil { 240 | log.Error("Error executing `openssl`") 241 | log.Errorf("%s", err) 242 | } 243 | 244 | log.Info("Done creating PEM") 245 | 246 | return err 247 | } 248 | 249 | // SecretsFolder returns the path to Sputnik's secrets folder 250 | func (c *CloudKitKeyManager) SecretsFolder() string { 251 | file, err := os.Open(c.secretsFolder) 252 | defer file.Close() 253 | 254 | if err != nil { 255 | // secrets folder doesn't exist yet, so create it 256 | path, createErr := createSecretsFolder(c.secretsFolder) 257 | if createErr != nil { 258 | log.Debugf("%s", createErr) 259 | } 260 | return path 261 | } 262 | 263 | return file.Name() 264 | } 265 | 266 | func createSecretsFolder(in string) (string, error) { 267 | mode := int(0755) 268 | return in, os.MkdirAll(in, os.FileMode(mode)) 269 | } 270 | 271 | func homeDir() string { 272 | usr, err := user.Current() 273 | if err != nil { 274 | // can't get current user 275 | log.Errorf("%s", err) 276 | } 277 | 278 | return usr.HomeDir 279 | } 280 | --------------------------------------------------------------------------------