diff --git a/LICENSE b/LICENSE index 0ae1364..da17cc4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2018, Maxim Baz & Steve Gilberd +Copyright (c) 2018-2019, Maxim Baz & Steve Gilberd Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/errors/errors.go b/errors/errors.go index f1e281f..981b61c 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -25,6 +25,8 @@ const ( CodeUnableToDetectGpgPath Code = 22 CodeInvalidPasswordFileExtension Code = 23 CodeUnableToDecryptPasswordFile Code = 24 + CodeUnableToEncryptPasswordFile Code = 25 + CodeUnableToGitCommit Code = 26 ) // Field extra field in the error response params diff --git a/request/check.go b/request/check.go new file mode 100644 index 0000000..3be508b --- /dev/null +++ b/request/check.go @@ -0,0 +1,32 @@ +package request + +import ( + "os" + "path/filepath" + + "github.com/browserpass/browserpass-native/response" +) + +type existsResponse struct { + Exists bool `json:"exists"` +} + +func checkFile(request *request) { + exists := false + + for _, store := range request.Settings.Stores { + normalizedStorePath, err := normalizePasswordStorePath(store.Path) + if err != nil { + continue // TODO: should respond with error + } + + absoluteFilePath := filepath.Join(normalizedStorePath, request.File) + _, err = os.Stat(absoluteFilePath) + + if err == nil { + exists = true + } + } + + response.SendOk(&existsResponse{exists}) +} diff --git a/request/common.go b/request/common.go index 88f1676..f9ca9bd 100644 --- a/request/common.go +++ b/request/common.go @@ -1,10 +1,16 @@ package request import ( + "bufio" "errors" + "fmt" "os" "path/filepath" "strings" + + bpErrors "github.com/browserpass/browserpass-native/errors" + "github.com/browserpass/browserpass-native/response" + log "github.com/sirupsen/logrus" ) func normalizePasswordStorePath(storePath string) (string, error) { @@ -32,3 +38,78 @@ func normalizePasswordStorePath(storePath string) (string, error) { } return storePath, nil } + +func detectGpgBinary() (string, error) { + // Look in $PATH first, then check common locations - the first successful result wins + gpgBinaryPriorityList := []string{ + "gpg2", "gpg", + "/bin/gpg2", "/usr/bin/gpg2", "/usr/local/bin/gpg2", + "/bin/gpg", "/usr/bin/gpg", "/usr/local/bin/gpg", + } + + for _, binary := range gpgBinaryPriorityList { + err := validateGpgBinary(binary) + if err == nil { + return binary, nil + } + } + return "", fmt.Errorf("Unable to detect the location of the gpg binary to use") +} + +func getGpgPath(request *request) (string, error) { + var gpgPath string + var err error + if request.Settings.GpgPath != "" { + gpgPath = request.Settings.GpgPath + err = validateGpgBinary(gpgPath) + if err != nil { + log.Errorf( + "The provided gpg binary path '%v' is invalid: %+v", + gpgPath, err, + ) + response.SendErrorAndExit( + bpErrors.CodeInvalidGpgPath, + &map[bpErrors.Field]string{ + bpErrors.FieldMessage: "The provided gpg binary path is invalid", + bpErrors.FieldAction: request.Action, + bpErrors.FieldError: err.Error(), + bpErrors.FieldGpgPath: gpgPath, + }, + ) + return "", err + } + } else { + gpgPath, err = detectGpgBinary() + if err != nil { + log.Error("Unable to detect the location of the gpg binary: ", err) + response.SendErrorAndExit( + bpErrors.CodeUnableToDetectGpgPath, + &map[bpErrors.Field]string{ + bpErrors.FieldMessage: "Unable to detect the location of the gpg binary", + bpErrors.FieldAction: request.Action, + bpErrors.FieldError: err.Error(), + }, + ) + return "", err + } + } + + return gpgPath, nil +} + +func readGPGIDs(storePath string) []string { + IDs := make([]string, 0) + IDFilePath := filepath.Join(storePath, ".gpg-id") + IDFile, err := os.Open(IDFilePath) + if err != nil { + return IDs + } + defer IDFile.Close() + + scanner := bufio.NewScanner(IDFile) + for scanner.Scan() { + IDs = append(IDs, scanner.Text()) + } + + return IDs +} diff --git a/request/create.go b/request/create.go new file mode 100644 index 0000000..4c99e30 --- /dev/null +++ b/request/create.go @@ -0,0 +1,127 @@ +package request + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/browserpass/browserpass-native/errors" + "github.com/browserpass/browserpass-native/response" + log "github.com/sirupsen/logrus" +) + +func createFile(request *request) { + store, ok := request.Settings.Stores[request.StoreID] + if !ok { + log.Errorf( + "The password store with ID '%v' is not present in the list of stores '%+v'", + request.StoreID, request.Settings.Stores, + ) + response.SendErrorAndExit( + errors.CodeInvalidPasswordStore, + &map[errors.Field]string{ + errors.FieldMessage: "The password store is not present in the list of stores", + errors.FieldAction: "create", + errors.FieldStoreID: request.StoreID, + }, + ) + } + storePath, err := normalizePasswordStorePath(store.Path) + if err != nil { + return // TODO + } + + gpgPath, err := getGpgPath(request) + if err != nil { + return // TODO + } + + credentials := request.Credentials + fileString := fmt.Sprintf("%s\nlogin: %s\n", credentials.Password, credentials.Login) + + if credentials.Email != "" { + fileString = fileString + fmt.Sprintf("email: %s\n", credentials.Email) + } + + err = encryptContent(storePath, request.File, fileString, gpgPath) + if err != nil { + response.SendErrorAndExit( + errors.CodeUnableToEncryptPasswordFile, + &map[errors.Field]string{ + errors.FieldMessage: "Could not encrypt new password file", + errors.FieldAction: "create", + errors.FieldError: err.Error(), + errors.FieldStoreID: request.StoreID, + }, + ) + } + + err = gitAddAndCommit(storePath, request.File) + if err != nil { + response.SendErrorAndExit( + errors.CodeUnableToGitCommit, + &map[errors.Field]string{ + errors.FieldMessage: "Could not commit file to git repository", + errors.FieldAction: "create", + errors.FieldError: err.Error(), + errors.FieldStoreID: request.StoreID, + errors.FieldFile: request.File, + }, + ) + } + + response.SendOk(nil) +} + +func encryptContent(storePath, file, content, gpgPath string) error { + IDs := readGPGIDs(storePath) + passwordFilePath := filepath.Join(storePath, file) + err := os.MkdirAll(filepath.Dir(passwordFilePath), os.ModePerm) + if err != nil { + return err + } + + var stderr bytes.Buffer + gpgOptions := []string{"--encrypt", "-o", passwordFilePath, "--quiet"} + + for _, id := range IDs { + gpgOptions = append(gpgOptions, "-r") + gpgOptions = append(gpgOptions, id) + } + + contentReader := strings.NewReader(content) + cmd := exec.Command(gpgPath, gpgOptions...) + cmd.Stdin = contentReader + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String()) + } + + return nil +} + +func gitAddAndCommit(storePath, file string) error { + gitBaseOptions := []string{"-C", storePath} + + var stderr bytes.Buffer + cmd := exec.Command("git", append(gitBaseOptions, []string{"add", file}...)...) + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String()) + } + + commitMessage := fmt.Sprintf("Add password %s from browserpass", file) + cmd = exec.Command("git", append(gitBaseOptions, []string{"commit", "-m", commitMessage}...)...) + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String()) + } + + return nil +} diff --git a/request/fetch.go b/request/fetch.go index fd35610..aff9da6 100644 --- a/request/fetch.go +++ b/request/fetch.go @@ -125,23 +125,6 @@ func fetchDecryptedContents(request *request) { response.SendOk(responseData) } -func detectGpgBinary() (string, error) { - // Look in $PATH first, then check common locations - the first successful result wins - gpgBinaryPriorityList := []string{ - "gpg2", "gpg", - "/bin/gpg2", "/usr/bin/gpg2", "/usr/local/bin/gpg2", - "/bin/gpg", "/usr/bin/gpg", "/usr/local/bin/gpg", - } - - for _, binary := range gpgBinaryPriorityList { - err := validateGpgBinary(binary) - if err == nil { - return binary, nil - } - } - return "", fmt.Errorf("Unable to detect the location of the gpg binary to use") -} - func validateGpgBinary(gpgPath string) error { return exec.Command(gpgPath, "--version").Run() } diff --git a/request/process.go b/request/process.go index 4c53068..5e20b73 100644 --- a/request/process.go +++ b/request/process.go @@ -27,10 +27,17 @@ type settings struct { Stores map[string]store `json:"stores"` } +type credentials struct { + Login string `json:"login"` + Password string `json:"password"` + Email string `json:"email"` +} + type request struct { Action string `json:"action"` Settings settings `json:"settings"` File string `json:"file"` + Credentials credentials `json:"credentials"` StoreID string `json:"storeId"` EchoResponse interface{} `json:"echoResponse"` } @@ -68,6 +75,10 @@ func Process() { listFiles(request) case "fetch": fetchDecryptedContents(request) + case "exists": + checkFile(request) + case "create": + createFile(request) case "echo": response.SendRaw(request.EchoResponse) default: