diff --git a/.travis.yml b/.travis.yml index 565e54ab2..50848a293 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,8 @@ before_install: - source ~/.cargo/env script: - - make + - 'if [ "$TRAVIS_REPO_SLUG" != "mozilla/sops" ]; then make; fi' + - 'if [ "$TRAVIS_REPO_SLUG" = "mozilla/sops" ]; then make origin-build; fi' - bash <(curl -s https://codecov.io/bash) before_deploy: @@ -27,7 +28,7 @@ before_deploy: - GOOS=linux CGO_ENABLED=0 go build -o dist/sops-${TRAVIS_TAG}.linux go.mozilla.org/sops/cmd/sops - | if [ ! -z "$TRAVIS_TAG" ]; then - version="$(grep '^const version' cmd/sops/version.go |cut -d '"' -f 2)" + version="$(grep '^const Version' version/version.go |cut -d '"' -f 2)" if [ "$version" != "$TRAVIS_TAG" ]; then echo "Git tag $TRAVIS_TAG does not match version $version, update the source!" exit 1 diff --git a/Makefile b/Makefile index 553fe256a..99f634354 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ GO := GO15VENDOREXPERIMENT=1 go GOLINT := golint all: test vet generate install functional-tests +origin-build: test vet generate install functional-tests-all install: $(GO) install go.mozilla.org/sops/cmd/sops @@ -40,6 +41,12 @@ functional-tests: $(GO) build -o functional-tests/sops go.mozilla.org/sops/cmd/sops cd functional-tests && cargo test +# Ignored tests are ones that require external services (e.g. AWS KMS) +# TODO: Once `--include-ignored` lands in rust stable, switch to that. +functional-tests-all: + $(GO) build -o functional-tests/sops go.mozilla.org/sops/cmd/sops + cd functional-tests && cargo test && cargo test -- --ignored + deb-pkg: install rm -rf tmppkg mkdir -p tmppkg/usr/local/bin diff --git a/cmd/sops/common/common.go b/cmd/sops/common/common.go index fb3282617..56501c855 100644 --- a/cmd/sops/common/common.go +++ b/cmd/sops/common/common.go @@ -3,17 +3,22 @@ package common import ( "fmt" "io/ioutil" + "os" "path/filepath" "strings" "time" + wordwrap "github.com/mitchellh/go-wordwrap" "go.mozilla.org/sops" "go.mozilla.org/sops/cmd/sops/codes" "go.mozilla.org/sops/keyservice" - "go.mozilla.org/sops/stores/json" - "go.mozilla.org/sops/stores/yaml" + "go.mozilla.org/sops/kms" "go.mozilla.org/sops/stores/dotenv" "go.mozilla.org/sops/stores/ini" + "go.mozilla.org/sops/stores/json" + "go.mozilla.org/sops/stores/yaml" + "go.mozilla.org/sops/version" + "golang.org/x/crypto/ssh/terminal" "gopkg.in/urfave/cli.v1" ) @@ -26,7 +31,6 @@ type Store interface { ExampleFileEmitter } - // DecryptTreeOpts are the options needed to decrypt a tree type DecryptTreeOpts struct { // Tree is the tree to be decrypted @@ -136,3 +140,202 @@ func DefaultStoreForPath(path string) Store { } return &json.BinaryStore{} } + +const KMS_ENC_CTX_BUG_FIXED_VERSION = "3.3.0" + +func DetectKMSEncryptionContextBug(tree *sops.Tree) (bool, error) { + versionCheck, err := version.AIsNewerThanB(KMS_ENC_CTX_BUG_FIXED_VERSION, tree.Metadata.Version) + if err != nil { + return false, err + } + + if versionCheck { + _, _, key := GetKMSKeyWithEncryptionCtx(tree) + if key != nil { + return true, nil + } + } + + return false, nil +} + +func GetKMSKeyWithEncryptionCtx(tree *sops.Tree) (keyGroupIndex int, keyIndex int, key *kms.MasterKey) { + for i, kg := range tree.Metadata.KeyGroups { + for n, k := range kg { + kmsKey, ok := k.(*kms.MasterKey) + if ok { + if kmsKey.EncryptionContext != nil && len(kmsKey.EncryptionContext) >= 2 { + duplicateValues := map[string]int{} + for _, v := range kmsKey.EncryptionContext { + duplicateValues[*v] = duplicateValues[*v] + 1 + } + if len(duplicateValues) > 1 { + return i, n, kmsKey + } + } + } + } + } + return 0, 0, nil +} + +type GenericDecryptOpts struct { + Cipher sops.Cipher + InputStore sops.Store + InputPath string + IgnoreMAC bool + KeyServices []keyservice.KeyServiceClient +} + +// LoadEncryptedFileWithBugFixes is a wrapper around LoadEncryptedFile which includes +// check for the issue described in https://github.com/mozilla/sops/pull/435 +func LoadEncryptedFileWithBugFixes(opts GenericDecryptOpts) (*sops.Tree, error) { + tree, err := LoadEncryptedFile(opts.InputStore, opts.InputPath) + if err != nil { + return nil, err + } + + encCtxBug, err := DetectKMSEncryptionContextBug(tree) + if err != nil { + return nil, err + } + if encCtxBug { + tree, err = FixAWSKMSEncryptionContextBug(opts, tree) + if err != nil { + return nil, err + } + } + + return tree, nil +} + +// FixAWSKMSEncryptionContextBug is used to fix the issue described in https://github.com/mozilla/sops/pull/435 +func FixAWSKMSEncryptionContextBug(opts GenericDecryptOpts, tree *sops.Tree) (*sops.Tree, error) { + message := "Up until version 3.3.0 of sops there was a bug surrounding the " + + "use of encryption context with AWS KMS." + + "\nYou can read the full description of the issue here:" + + "\nhttps://github.com/mozilla/sops/pull/435" + + "\n\nIf a TTY is detected, sops will ask you if you'd like for this issue to be " + + "automatically fixed, which will require re-encrypting the data keys used by " + + "each key." + + "\n\nIf you are not using a TTY, sops will fix the issue for this run.\n\n" + fmt.Println(wordwrap.WrapString(message, 75)) + + persistFix := false + + if terminal.IsTerminal(int(os.Stdout.Fd())) { + var response string + for response != "y" && response != "n" { + fmt.Println("Would you like sops to automatically fix this issue? (y/n): ") + _, err := fmt.Scanln(&response) + if err != nil { + return nil, err + } + } + if response == "n" { + return nil, fmt.Errorf("Exiting. User responded no.") + } else { + persistFix = true + } + } + + dataKey := []byte{} + // If there is another key, then we should be able to just decrypt + // without having to try different variations of the encryption context. + dataKey, err := DecryptTree(DecryptTreeOpts{ + Cipher: opts.Cipher, + IgnoreMac: opts.IgnoreMAC, + Tree: tree, + KeyServices: opts.KeyServices, + }) + if err != nil { + dataKey = RecoverDataKeyFromBuggyKMS(opts, tree) + } + + if dataKey == nil { + return nil, NewExitError(fmt.Sprintf("Failed to decrypt, meaning there is likely another problem from the encryption context bug: %s", err), codes.ErrorDecryptingTree) + } + + errs := tree.Metadata.UpdateMasterKeysWithKeyServices(dataKey, opts.KeyServices) + if len(errs) > 0 { + err = fmt.Errorf("Could not re-encrypt data key: %s", errs) + return nil, err + } + + err = EncryptTree(EncryptTreeOpts{ + DataKey: dataKey, + Tree: tree, + Cipher: opts.Cipher, + }) + if err != nil { + return nil, err + } + + // If we are not going to persist the fix, just return the re-encrypted tree. + if !persistFix { + return tree, nil + } + + encryptedFile, err := opts.InputStore.EmitEncryptedFile(*tree) + if err != nil { + return nil, NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + } + + file, err := os.Create(opts.InputPath) + defer file.Close() + if err != nil { + return nil, NewExitError(fmt.Sprintf("Could not open file for writing: %s", err), codes.CouldNotWriteOutputFile) + } + _, err = file.Write(encryptedFile) + if err != nil { + return nil, err + } + + newTree, err := LoadEncryptedFile(opts.InputStore, opts.InputPath) + if err != nil { + return nil, err + } + + return newTree, nil +} + +// RecoverDataKeyFromBuggyKMS loops through variations on Encryption Context to +// recover the datakey. This is used to fix the issue described in https://github.com/mozilla/sops/pull/435 +func RecoverDataKeyFromBuggyKMS(opts GenericDecryptOpts, tree *sops.Tree) []byte { + kgndx, kndx, originalKey := GetKMSKeyWithEncryptionCtx(tree) + + keyToEdit := *originalKey + + encCtxVals := map[string]interface{}{} + for _, v := range keyToEdit.EncryptionContext { + encCtxVals[*v] = "" + } + + encCtxVariations := []map[string]*string{} + for ctxVal := range encCtxVals { + encCtxVariation := map[string]*string{} + for key := range keyToEdit.EncryptionContext { + val := ctxVal + encCtxVariation[key] = &val + } + encCtxVariations = append(encCtxVariations, encCtxVariation) + } + + for _, encCtxVar := range encCtxVariations { + keyToEdit.EncryptionContext = encCtxVar + tree.Metadata.KeyGroups[kgndx][kndx] = &keyToEdit + dataKey, err := DecryptTree(DecryptTreeOpts{ + Cipher: opts.Cipher, + IgnoreMac: opts.IgnoreMAC, + Tree: tree, + KeyServices: opts.KeyServices, + }) + if err == nil { + tree.Metadata.KeyGroups[kgndx][kndx] = originalKey + tree.Metadata.Version = version.Version + return dataKey + } + } + + return nil +} diff --git a/cmd/sops/decrypt.go b/cmd/sops/decrypt.go index 8e077b075..7624bf62d 100644 --- a/cmd/sops/decrypt.go +++ b/cmd/sops/decrypt.go @@ -20,7 +20,13 @@ type decryptOpts struct { } func decrypt(opts decryptOpts) (decryptedFile []byte, err error) { - tree, err := common.LoadEncryptedFile(opts.InputStore, opts.InputPath) + tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{ + Cipher: opts.Cipher, + InputStore: opts.InputStore, + InputPath: opts.InputPath, + IgnoreMAC: opts.IgnoreMAC, + KeyServices: opts.KeyServices, + }) if err != nil { return nil, err } diff --git a/cmd/sops/edit.go b/cmd/sops/edit.go index 8e180e5a8..2f88f9e41 100644 --- a/cmd/sops/edit.go +++ b/cmd/sops/edit.go @@ -22,6 +22,7 @@ import ( "go.mozilla.org/sops/cmd/sops/codes" "go.mozilla.org/sops/cmd/sops/common" "go.mozilla.org/sops/keyservice" + "go.mozilla.org/sops/version" ) type editOpts struct { @@ -66,7 +67,7 @@ func editExample(opts editExampleOpts) ([]byte, error) { KeyGroups: opts.KeyGroups, UnencryptedSuffix: opts.UnencryptedSuffix, EncryptedSuffix: opts.EncryptedSuffix, - Version: version, + Version: version.Version, ShamirThreshold: opts.GroupThreshold, }, FilePath: path, @@ -83,7 +84,13 @@ func editExample(opts editExampleOpts) ([]byte, error) { func edit(opts editOpts) ([]byte, error) { // Load the file - tree, err := common.LoadEncryptedFile(opts.InputStore, opts.InputPath) + tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{ + Cipher: opts.Cipher, + InputStore: opts.InputStore, + InputPath: opts.InputPath, + IgnoreMAC: opts.IgnoreMAC, + KeyServices: opts.KeyServices, + }) if err != nil { return nil, err } @@ -201,12 +208,12 @@ func runEditorUntilOk(opts runEditorUntilOkOpts) error { opts.Tree = &t } opts.Tree.Branches = newBranches - needVersionUpdated, err := AIsNewerThanB(version, opts.Tree.Metadata.Version) + needVersionUpdated, err := version.AIsNewerThanB(version.Version, opts.Tree.Metadata.Version) if err != nil { - return common.NewExitError(fmt.Sprintf("Failed to compare document version %q with program version %q: %v", opts.Tree.Metadata.Version, version, err), codes.FailedToCompareVersions) + return common.NewExitError(fmt.Sprintf("Failed to compare document version %q with program version %q: %v", opts.Tree.Metadata.Version, version.Version, err), codes.FailedToCompareVersions) } if needVersionUpdated { - opts.Tree.Metadata.Version = version + opts.Tree.Metadata.Version = version.Version } if opts.Tree.Metadata.MasterKeyCount() == 0 { log.Error("No master keys were provided, so sops can't " + diff --git a/cmd/sops/encrypt.go b/cmd/sops/encrypt.go index df2cf54f3..f34924c90 100644 --- a/cmd/sops/encrypt.go +++ b/cmd/sops/encrypt.go @@ -11,6 +11,7 @@ import ( "go.mozilla.org/sops/cmd/sops/codes" "go.mozilla.org/sops/cmd/sops/common" "go.mozilla.org/sops/keyservice" + "go.mozilla.org/sops/version" ) type encryptOpts struct { @@ -75,7 +76,7 @@ func encrypt(opts encryptOpts) (encryptedFile []byte, err error) { KeyGroups: opts.KeyGroups, UnencryptedSuffix: opts.UnencryptedSuffix, EncryptedSuffix: opts.EncryptedSuffix, - Version: version, + Version: version.Version, ShamirThreshold: opts.GroupThreshold, }, FilePath: path, diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 528dda2f2..18701f05d 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -32,6 +32,7 @@ import ( "go.mozilla.org/sops/stores/ini" "go.mozilla.org/sops/stores/json" yamlstores "go.mozilla.org/sops/stores/yaml" + "go.mozilla.org/sops/version" "google.golang.org/grpc" "gopkg.in/urfave/cli.v1" ) @@ -43,7 +44,7 @@ func init() { } func main() { - cli.VersionPrinter = printVersion + cli.VersionPrinter = version.PrintVersion app := cli.NewApp() keyserviceFlags := []cli.Flag{ @@ -59,7 +60,7 @@ func main() { app.Name = "sops" app.Usage = "sops - encrypted file editor with AWS KMS, GCP KMS, Azure Key Vault and GPG support" app.ArgsUsage = "sops [options] file" - app.Version = version + app.Version = version.Version app.Authors = []cli.Author{ {Name: "Julien Vehent", Email: "jvehent@mozilla.com"}, {Name: "Adrian Utrilla", Email: "adrianutrilla@gmail.com"}, diff --git a/cmd/sops/rotate.go b/cmd/sops/rotate.go index d36721a5f..373785a52 100644 --- a/cmd/sops/rotate.go +++ b/cmd/sops/rotate.go @@ -23,7 +23,13 @@ type rotateOpts struct { } func rotate(opts rotateOpts) ([]byte, error) { - tree, err := common.LoadEncryptedFile(opts.InputStore, opts.InputPath) + tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{ + Cipher: opts.Cipher, + InputStore: opts.InputStore, + InputPath: opts.InputPath, + IgnoreMAC: opts.IgnoreMAC, + KeyServices: opts.KeyServices, + }) if err != nil { return nil, err } diff --git a/cmd/sops/set.go b/cmd/sops/set.go index cd5bdd51f..63e82581f 100644 --- a/cmd/sops/set.go +++ b/cmd/sops/set.go @@ -23,7 +23,13 @@ type setOpts struct { func set(opts setOpts) ([]byte, error) { // Load the file // TODO: Issue #173: if the file does not exist, create it with the contents passed in as opts.Value - tree, err := common.LoadEncryptedFile(opts.InputStore, opts.InputPath) + tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{ + Cipher: opts.Cipher, + InputStore: opts.InputStore, + InputPath: opts.InputPath, + IgnoreMAC: opts.IgnoreMAC, + KeyServices: opts.KeyServices, + }) if err != nil { return nil, err } diff --git a/functional-tests/src/lib.rs b/functional-tests/src/lib.rs index 7f0f60a73..01ec5379e 100644 --- a/functional-tests/src/lib.rs +++ b/functional-tests/src/lib.rs @@ -13,6 +13,7 @@ mod tests { extern crate serde_json; extern crate serde_yaml; + use std::env; use std::fs::File; use std::io::{Write, Read}; use tempdir::TempDir; @@ -20,6 +21,7 @@ mod tests { use serde_yaml::Value; use std::path::Path; const SOPS_BINARY_PATH: &'static str = "./sops"; + const KMS_KEY: &'static str = "FUNCTIONAL_TEST_KMS_ARN"; macro_rules! assert_encrypted { ($object:expr, $key:expr) => { @@ -72,6 +74,38 @@ mod tests { } } + #[test] + #[ignore] + fn encrypt_json_file_kms() { + let kms_arn = env::var(KMS_KEY).expect("Expected $FUNCTIONAL_TEST_KMS_ARN env var to be set"); + + let file_path = prepare_temp_file("test_encrypt_kms.json", + b"{ + \"foo\": 2, + \"bar\": \"baz\" +}"); + + let output = Command::new(SOPS_BINARY_PATH) + .arg("--kms") + .arg(kms_arn) + .arg("-e") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!(output.status.success(), "sops didn't exit successfully"); + let json = &String::from_utf8_lossy(&output.stdout); + let data: Value = serde_json::from_str(json).expect("Error parsing sops's JSON output"); + match data.into() { + Value::Mapping(m) => { + assert!(m.get(&Value::String("sops".to_owned())).is_some(), + "sops metadata branch not found"); + assert_encrypted!(&m, Value::String("foo".to_owned())); + assert_encrypted!(&m, Value::String("bar".to_owned())); + } + _ => panic!("sops's JSON output is not an object"), + } + } + #[test] fn encrypt_yaml_file() { let file_path = prepare_temp_file("test_encrypt.yaml", @@ -410,6 +444,41 @@ b: ba"# assert_eq!(output.stdout, data); } + #[test] + #[ignore] + fn roundtrip_kms_encryption_context() { + let kms_arn = env::var(KMS_KEY).expect("Expected $FUNCTIONAL_TEST_KMS_ARN env var to be set"); + + let file_path = prepare_temp_file("test_roundtrip_kms_encryption_context.json", + b"{ + \"foo\": 2, + \"bar\": \"baz\" +}"); + + let output = Command::new(SOPS_BINARY_PATH) + .arg("--kms") + .arg(kms_arn) + .arg("--encryption-context") + .arg("foo:bar,one:two") + .arg("-i") + .arg("-e") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!(output.status.success(), "sops didn't exit successfully"); + + let output = Command::new(SOPS_BINARY_PATH) + .arg("-d") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!(output.status + .success(), + "SOPS failed to decrypt a file with KMS Encryption Context"); + assert!(String::from_utf8_lossy(&output.stdout).contains("foo")); + assert!(String::from_utf8_lossy(&output.stdout).contains("baz")); + } + #[test] fn output_flag() { let input_path = prepare_temp_file("test_output_flag.binary", b"foo"); diff --git a/keyservice/server.go b/keyservice/server.go index 35849c6e9..b8603fe41 100644 --- a/keyservice/server.go +++ b/keyservice/server.go @@ -29,16 +29,7 @@ func (ks *Server) encryptWithPgp(key *PgpKey, plaintext []byte) ([]byte, error) } func (ks *Server) encryptWithKms(key *KmsKey, plaintext []byte) ([]byte, error) { - ctx := make(map[string]*string) - for k, v := range key.Context { - ctx[k] = &v - } - kmsKey := kms.MasterKey{ - Arn: key.Arn, - Role: key.Role, - EncryptionContext: ctx, - AwsProfile: key.AwsProfile, - } + kmsKey := kmsKeyToMasterKey(key) err := kmsKey.Encrypt(plaintext) if err != nil { return nil, err @@ -78,16 +69,7 @@ func (ks *Server) decryptWithPgp(key *PgpKey, ciphertext []byte) ([]byte, error) } func (ks *Server) decryptWithKms(key *KmsKey, ciphertext []byte) ([]byte, error) { - ctx := make(map[string]*string) - for k, v := range key.Context { - ctx[k] = &v - } - kmsKey := kms.MasterKey{ - Arn: key.Arn, - Role: key.Role, - EncryptionContext: ctx, - AwsProfile: key.AwsProfile, - } + kmsKey := kmsKeyToMasterKey(key) kmsKey.EncryptedKey = string(ciphertext) plaintext, err := kmsKey.Decrypt() return []byte(plaintext), err @@ -249,3 +231,17 @@ func (ks Server) Decrypt(ctx context.Context, } return response, nil } + +func kmsKeyToMasterKey(key *KmsKey) kms.MasterKey { + ctx := make(map[string]*string) + for k, v := range key.Context { + value := v // Allocate a new string to prevent the pointer below from referring to only the last iteration value + ctx[k] = &value + } + return kms.MasterKey{ + Arn: key.Arn, + Role: key.Role, + EncryptionContext: ctx, + AwsProfile: key.AwsProfile, + } +} diff --git a/keyservice/server_test.go b/keyservice/server_test.go new file mode 100644 index 000000000..147a69a27 --- /dev/null +++ b/keyservice/server_test.go @@ -0,0 +1,81 @@ +package keyservice + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestKmsKeyToMasterKey(t *testing.T) { + + cases := []struct { + description string + expectedArn string + expectedRole string + expectedCtx map[string]string + expectedAwsProfile string + }{ + { + description: "empty context", + expectedArn: "arn:aws:kms:eu-west-1:123456789012:key/d5c90a06-f824-4628-922b-12424571ed4d", + expectedRole: "ExampleRole", + expectedCtx: map[string]string{}, + expectedAwsProfile: "", + }, + { + description: "context with one key-value pair", + expectedArn: "arn:aws:kms:eu-west-1:123456789012:key/d5c90a06-f824-4628-922b-12424571ed4d", + expectedRole: "", + expectedCtx: map[string]string{ + "firstKey": "first value", + }, + expectedAwsProfile: "ExampleProfile", + }, + { + description: "context with three key-value pairs", + expectedArn: "arn:aws:kms:eu-west-1:123456789012:key/d5c90a06-f824-4628-922b-12424571ed4d", + expectedRole: "", + expectedCtx: map[string]string{ + "firstKey": "first value", + "secondKey": "second value", + "thirdKey": "third value", + }, + expectedAwsProfile: "", + }, + } + + for _, c := range cases { + + t.Run(c.description, func(t *testing.T) { + + inputCtx := make(map[string]string) + for k, v := range c.expectedCtx { + inputCtx[k] = v + } + + key := &KmsKey{ + Arn: c.expectedArn, + Role: c.expectedRole, + Context: inputCtx, + AwsProfile: c.expectedAwsProfile, + } + + masterKey := kmsKeyToMasterKey(key) + foundCtx := masterKey.EncryptionContext + + for k, _ := range c.expectedCtx { + require.Containsf(t, foundCtx, k, "Context does not contain expected key '%s'", k) + } + for k, _ := range foundCtx { + require.Containsf(t, c.expectedCtx, k, "Context contains an unexpected key '%s' which cannot be found from expected map", k) + } + for k, expected := range c.expectedCtx { + foundVal := *foundCtx[k] + assert.Equalf(t, expected, foundVal, "Context key '%s' value '%s' does not match expected value '%s'", k, foundVal, expected) + } + assert.Equalf(t, c.expectedArn, masterKey.Arn, "Expected ARN to be '%s', but found '%s'", c.expectedArn, masterKey.Arn) + assert.Equalf(t, c.expectedRole, masterKey.Role, "Expected Role to be '%s', but found '%s'", c.expectedRole, masterKey.Role) + assert.Equalf(t, c.expectedAwsProfile, masterKey.AwsProfile, "Expected AWS profile to be '%s', but found '%s'", c.expectedAwsProfile, masterKey.AwsProfile) + }) + } +} diff --git a/cmd/sops/version.go b/version/version.go similarity index 83% rename from cmd/sops/version.go rename to version/version.go index af81ff4c3..8f6a2a43a 100644 --- a/cmd/sops/version.go +++ b/version/version.go @@ -1,4 +1,4 @@ -package main +package version import ( "bufio" @@ -10,15 +10,15 @@ import ( "gopkg.in/urfave/cli.v1" ) -const version = "3.2.0" +const Version = "3.3.0" -func printVersion(c *cli.Context) { +func PrintVersion(c *cli.Context) { out := fmt.Sprintf("%s %s", c.App.Name, c.App.Version) - upstreamVersion, err := retrieveLatestVersionFromUpstream() + upstreamVersion, err := RetrieveLatestVersionFromUpstream() if err != nil { out += fmt.Sprintf("\n[warning] failed to retrieve latest version from upstream: %v\n", err) } - outdated, err := AIsNewerThanB(upstreamVersion, version) + outdated, err := AIsNewerThanB(upstreamVersion, Version) if err != nil { out += fmt.Sprintf("\n[warning] failed to compare current version with latest: %v\n", err) } @@ -54,9 +54,9 @@ func AIsNewerThanB(A, B string) (bool, error) { return false, nil } -// retrieveLatestVersionFromUpstream gets the latest version from the source code at Github -func retrieveLatestVersionFromUpstream() (string, error) { - resp, err := http.Get("https://raw.githubusercontent.com/mozilla/sops/master/cmd/sops/version.go") +// RetrieveLatestVersionFromUpstream gets the latest version from the source code at Github +func RetrieveLatestVersionFromUpstream() (string, error) { + resp, err := http.Get("https://raw.githubusercontent.com/mozilla/sops/master/version/version.go") if err != nil { return "", err } @@ -64,7 +64,7 @@ func retrieveLatestVersionFromUpstream() (string, error) { scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() - if strings.HasPrefix(line, `const version = "`) { + if strings.HasPrefix(line, `const Version = "`) { comps := strings.Split(line, `"`) if len(comps) < 2 { return "", fmt.Errorf("Failed to parse version from upstream source")