From 8db942e66976d4c72b8f7fb8688e433b1defa0d5 Mon Sep 17 00:00:00 2001 From: Maxim Baz Date: Tue, 17 Apr 2018 00:18:05 +0200 Subject: [PATCH] Implement `fetch` action, normalize error response params (#13) --- PROTOCOL.md | 29 ++++---- errors/errors.go | 57 ++++++++++++--- request/configure.go | 56 +++++++++------ request/fetch.go | 164 +++++++++++++++++++++++++++++++++++++++++++ request/list.go | 37 ++++++---- request/process.go | 55 ++++++++------- response/response.go | 24 ++++--- 7 files changed, 335 insertions(+), 87 deletions(-) create mode 100644 request/fetch.go diff --git a/PROTOCOL.md b/PROTOCOL.md index c4f8835..bfe93c1 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -41,18 +41,23 @@ should be supplied as a `message` parameter. ## List of Error Codes -| Code | Description | Parameters | -| ---- | ----------------------------------------------------------------------- | ----------------------- | -| 10 | Unable to parse browser request length | error | -| 11 | Unable to parse browser request | error | -| 12 | Invalid request action | action | -| 13 | Inaccessible user-configured password store | error, name, path | -| 14 | Inaccessible default password store | error, path | -| 15 | Unable to determine the location of the default password store | error | -| 16 | Unable to read the default settings of a user-configured password store | error, name, path | -| 17 | Unable to read the default settings of the default password store | error, path | -| 18 | Unable to list files in a password store | error, name, path | -| 19 | Unable to determine a relative path for a file in a password store | error, file, name, path | +| Code | Description | Parameters | +| ---- | ----------------------------------------------------------------------- | -------------------------------------------------- | +| 10 | Unable to parse browser request length | message, error | +| 11 | Unable to parse browser request | message, error | +| 12 | Invalid request action | message, action | +| 13 | Inaccessible user-configured password store | message, action, error, storePath, storeName | +| 14 | Inaccessible default password store | message, action, error, storePath | +| 15 | Unable to determine the location of the default password store | message, action, error | +| 16 | Unable to read the default settings of a user-configured password store | message, action, error, storePath, storeName | +| 17 | Unable to read the default settings of the default password store | message, action, error, storePath | +| 18 | Unable to list files in a password store | message, action, error, storePath, storeName | +| 19 | Unable to determine a relative path for a file in a password store | message, action, error, storePath, storeName, file | +| 20 | Invalid password store name | message, action, storeName | +| 21 | Invalid gpg path | message, action, error, gpgPath | +| 22 | Unable to detect the location of the gpg binary | message, action, error | +| 23 | Invalid password file extension | message, action, file | +| 24 | Unable to decrypt the password file | message, action, error, storePath, storeName, file | ## Settings diff --git a/errors/errors.go b/errors/errors.go index 34a375f..a8ef633 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -17,28 +17,69 @@ const ( CodeParseRequest Code = 11 // CodeInvalidRequestAction error parsing a request action - CodeInvalidRequestAction = 12 + CodeInvalidRequestAction Code = 12 // CodeInaccessiblePasswordStore error accessing a user-configured password store - CodeInaccessiblePasswordStore = 13 + CodeInaccessiblePasswordStore Code = 13 // CodeInaccessibleDefaultPasswordStore error accessing the default password store - CodeInaccessibleDefaultPasswordStore = 14 + CodeInaccessibleDefaultPasswordStore Code = 14 // CodeUnknownDefaultPasswordStoreLocation error determining the location of the default password store - CodeUnknownDefaultPasswordStoreLocation = 15 + CodeUnknownDefaultPasswordStoreLocation Code = 15 // CodeUnreadablePasswordStoreDefaultSettings error reading the default settings of a user-configured password store - CodeUnreadablePasswordStoreDefaultSettings = 16 + CodeUnreadablePasswordStoreDefaultSettings Code = 16 // CodeUnreadableDefaultPasswordStoreDefaultSettings error reading the default settings of the default password store - CodeUnreadableDefaultPasswordStoreDefaultSettings = 17 + CodeUnreadableDefaultPasswordStoreDefaultSettings Code = 17 // CodeUnableToListFilesInPasswordStore error listing files in a password store - CodeUnableToListFilesInPasswordStore = 18 + CodeUnableToListFilesInPasswordStore Code = 18 // CodeUnableToDetermineRelativeFilePathInPasswordStore error determining a relative path for a file in a password store - CodeUnableToDetermineRelativeFilePathInPasswordStore = 19 + CodeUnableToDetermineRelativeFilePathInPasswordStore Code = 19 + + // CodeInvalidPasswordStore error looking for a password store with the given name + CodeInvalidPasswordStore Code = 20 + + // CodeInvalidGpgPath error looking for a gpg binary at the given path + CodeInvalidGpgPath Code = 21 + + // CodeUnableToDetectGpgPath error detecting the location of the gpg binary + CodeUnableToDetectGpgPath Code = 22 + + // CodeInvalidPasswordFileExtension error unexpected password file extension + CodeInvalidPasswordFileExtension Code = 23 + + // CodeUnableToDecryptPasswordFile error decrypting a password file + CodeUnableToDecryptPasswordFile Code = 24 +) + +// Field extra field in the error response params +type Field string + +const ( + // FieldMessage a user-friendly error message, always present + FieldMessage Field = "message" + + // FieldAction a browser request action that resulted in a failure + FieldAction Field = "action" + + // FieldError an error message returned from an external system + FieldError Field = "error" + + // FieldStoreName a password store name + FieldStoreName Field = "storeName" + + // FieldStorePath a password store path + FieldStorePath Field = "storePath" + + // FieldFile a password file + FieldFile Field = "file" + + // FieldGpgPath a path to the gpg binary + FieldGpgPath Field = "gpgPath" ) // ExitWithCode exit with error code diff --git a/request/configure.go b/request/configure.go index 2a99e66..2217279 100644 --- a/request/configure.go +++ b/request/configure.go @@ -23,12 +23,16 @@ func configure(request request) { "The password store '%v' is not accessible at the location '%v': %+v", store.Name, store.Path, err, ) - response.SendError( + response.SendErrorAndExit( errors.CodeInaccessiblePasswordStore, - "The password store is not accessible", - &map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, + &map[errors.Field]string{ + errors.FieldMessage: "The password store is not accessible", + errors.FieldAction: "configure", + errors.FieldError: err.Error(), + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, ) - errors.ExitWithCode(errors.CodeInaccessiblePasswordStore) } store.Path = normalizedStorePath @@ -39,12 +43,16 @@ func configure(request request) { "Unable to read the default settings of the user-configured password store '%v' in '%v': %+v", store.Name, store.Path, err, ) - response.SendError( + response.SendErrorAndExit( errors.CodeUnreadablePasswordStoreDefaultSettings, - "Unable to read the default settings of the password store", - &map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to read the default settings of the password store", + errors.FieldAction: "configure", + errors.FieldError: err.Error(), + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, ) - errors.ExitWithCode(errors.CodeUnreadablePasswordStoreDefaultSettings) } } @@ -55,12 +63,14 @@ func configure(request request) { if err != nil { if len(request.Settings.Stores) == 0 { log.Error("Unable to determine the location of the default password store: ", err) - response.SendError( + response.SendErrorAndExit( errors.CodeUnknownDefaultPasswordStoreLocation, - "Unable to determine the location of the default password store", - &map[string]string{"error": err.Error()}, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to determine the location of the default password store", + errors.FieldAction: "configure", + errors.FieldError: err.Error(), + }, ) - errors.ExitWithCode(errors.CodeUnknownDefaultPasswordStoreLocation) } } else { responseData.DefaultStore.Path, err = normalizePasswordStorePath(possibleDefaultStorePath) @@ -70,12 +80,15 @@ func configure(request request) { "The default password store is not accessible at the location '%v': %+v", possibleDefaultStorePath, err, ) - response.SendError( + response.SendErrorAndExit( errors.CodeInaccessibleDefaultPasswordStore, - "The default password store is not accessible", - &map[string]string{"error": err.Error(), "path": possibleDefaultStorePath}, + &map[errors.Field]string{ + errors.FieldMessage: "The default password store is not accessible", + errors.FieldAction: "configure", + errors.FieldError: err.Error(), + errors.FieldStorePath: possibleDefaultStorePath, + }, ) - errors.ExitWithCode(errors.CodeInaccessibleDefaultPasswordStore) } } } @@ -87,12 +100,15 @@ func configure(request request) { "Unable to read the default settings of the default password store in '%v': %+v", responseData.DefaultStore.Path, err, ) - response.SendError( + response.SendErrorAndExit( errors.CodeUnreadableDefaultPasswordStoreDefaultSettings, - "Unable to read the default settings of the default password store", - &map[string]string{"error": err.Error(), "path": responseData.DefaultStore.Path}, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to read the default settings of the default password store", + errors.FieldAction: "configure", + errors.FieldError: err.Error(), + errors.FieldStorePath: responseData.DefaultStore.Path, + }, ) - errors.ExitWithCode(errors.CodeUnreadableDefaultPasswordStoreDefaultSettings) } } diff --git a/request/fetch.go b/request/fetch.go new file mode 100644 index 0000000..6bf7d41 --- /dev/null +++ b/request/fetch.go @@ -0,0 +1,164 @@ +package request + +import ( + "bytes" + goerrors "errors" + "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 fetchDecryptedContents(request request) { + responseData := response.MakeFetchResponse() + + if !strings.HasSuffix(request.File, ".gpg") { + log.Errorf("The requested password file '%v' does not have the expected '.gpg' extension", request.File) + response.SendErrorAndExit( + errors.CodeInvalidPasswordFileExtension, + &map[errors.Field]string{ + errors.FieldMessage: "The requested password file does not have the expected '.gpg' extension", + errors.FieldAction: "fetch", + errors.FieldFile: request.File, + }, + ) + } + + store, ok := request.Settings.Stores[request.Store] + if !ok { + log.Errorf( + "The password store '%v' is not present in the list of stores '%v'", + request.Store, 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: "fetch", + errors.FieldStoreName: request.Store, + }, + ) + } + + normalizedStorePath, err := normalizePasswordStorePath(store.Path) + if err != nil { + log.Errorf( + "The password store '%v' is not accessible at the location '%v': %+v", + store.Name, store.Path, err, + ) + response.SendErrorAndExit( + errors.CodeInaccessiblePasswordStore, + &map[errors.Field]string{ + errors.FieldMessage: "The password store is not accessible", + errors.FieldAction: "fetch", + errors.FieldError: err.Error(), + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + store.Path = normalizedStorePath + + gpgPath := request.Settings.GpgPath + if gpgPath != "" { + err = validateGpgBinary(gpgPath) + if err != nil { + log.Errorf( + "The provided gpg binary path '%v' is invalid: %+v", + gpgPath, err, + ) + response.SendErrorAndExit( + errors.CodeInvalidGpgPath, + &map[errors.Field]string{ + errors.FieldMessage: "The provided gpg binary path is invalid", + errors.FieldAction: "fetch", + errors.FieldError: err.Error(), + errors.FieldGpgPath: gpgPath, + }, + ) + } + } else { + gpgPath, err = detectGpgBinary() + if err != nil { + log.Error("Unable to detect the location of the gpg binary: ", err) + response.SendErrorAndExit( + errors.CodeUnableToDetectGpgPath, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to detect the location of the gpg binary", + errors.FieldAction: "fetch", + errors.FieldError: err.Error(), + }, + ) + } + } + + responseData.Contents, err = decryptFile(store, request.File, gpgPath) + if err != nil { + log.Errorf( + "Unable to decrypt the password file '%v' in the password store '%v' located in '%v': %+v", + request.File, store.Name, store.Path, err, + ) + response.SendErrorAndExit( + errors.CodeUnableToDecryptPasswordFile, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to decrypt the password file", + errors.FieldAction: "fetch", + errors.FieldError: err.Error(), + errors.FieldFile: request.File, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + + 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 "", goerrors.New("Unable to detect the location of the gpg binary to use") +} + +func validateGpgBinary(gpgPath string) error { + return exec.Command(gpgPath, "--version").Run() +} + +func decryptFile(store store, file string, gpgPath string) (string, error) { + passwordFilePath := filepath.Join(store.Path, file) + passwordFile, err := os.Open(passwordFilePath) + if err != nil { + return "", err + } + + var stdout, stderr bytes.Buffer + gpgOptions := []string{"--decrypt", "--yes", "--quiet", "--batch", "-"} + + cmd := exec.Command(gpgPath, gpgOptions...) + cmd.Stdin = passwordFile + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return "", goerrors.New(fmt.Sprintf("Error: %s, Stderr: %s", err.Error(), stderr.String())) + } + + return stdout.String(), nil +} diff --git a/request/list.go b/request/list.go index bd90cbd..144ab39 100644 --- a/request/list.go +++ b/request/list.go @@ -20,12 +20,16 @@ func listFiles(request request) { "The password store '%v' is not accessible at the location '%v': %+v", store.Name, store.Path, err, ) - response.SendError( + response.SendErrorAndExit( errors.CodeInaccessiblePasswordStore, - "The password store is not accessible", - &map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, + &map[errors.Field]string{ + errors.FieldMessage: "The password store is not accessible", + errors.FieldAction: "list", + errors.FieldError: err.Error(), + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, ) - errors.ExitWithCode(errors.CodeInaccessiblePasswordStore) } store.Path = normalizedStorePath @@ -36,12 +40,16 @@ func listFiles(request request) { "Unable to list the files in the password store '%v' at the location '%v': %+v", store.Name, store.Path, err, ) - response.SendError( + response.SendErrorAndExit( errors.CodeUnableToListFilesInPasswordStore, - "Unable to list the files in the password store", - &map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to list the files in the password store", + errors.FieldAction: "list", + errors.FieldError: err.Error(), + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, ) - errors.ExitWithCode(errors.CodeUnableToListFilesInPasswordStore) } for i, file := range files { @@ -51,12 +59,17 @@ func listFiles(request request) { "Unable to determine the relative path for a file '%v' in the password store '%v' at the location '%v': %+v", file, store.Name, store.Path, err, ) - response.SendError( + response.SendErrorAndExit( errors.CodeUnableToDetermineRelativeFilePathInPasswordStore, - "Unable to determine the relative path for a file in the password store", - &map[string]string{"error": err.Error(), "file": file, "name": store.Name, "path": store.Path}, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to determine the relative path for a file in the password store", + errors.FieldAction: "list", + errors.FieldError: err.Error(), + errors.FieldFile: file, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, ) - errors.ExitWithCode(errors.CodeUnableToDetermineRelativeFilePathInPasswordStore) } files[i] = relativePath } diff --git a/request/process.go b/request/process.go index d4b99b4..fe77b93 100644 --- a/request/process.go +++ b/request/process.go @@ -11,15 +11,21 @@ import ( log "github.com/sirupsen/logrus" ) +type store struct { + Name string `json:"name"` + Path string `json:"path"` +} + +type settings struct { + GpgPath string `json:"gpgPath"` + Stores map[string]store `json:"stores"` +} + type request struct { - Action string `json:"action"` - Settings struct { - GpgPath string `json:"gpgPath"` - Stores map[string]struct { - Name string `json:"name"` - Path string `json:"path"` - } - } `json:"settings"` + Action string `json:"action"` + Settings settings `json:"settings"` + File string `json:"file"` + Store string `json:"store"` } // Process handles browser request @@ -33,15 +39,16 @@ func Process() { case "list": listFiles(request) case "fetch": - break + fetchDecryptedContents(request) default: log.Errorf("Received a browser request with an unknown action: %+v", request) - response.SendError( + response.SendErrorAndExit( errors.CodeInvalidRequestAction, - "Invalid request action", - &map[string]string{"action": request.Action}, + &map[errors.Field]string{ + errors.FieldMessage: "Invalid request action", + errors.FieldAction: request.Action, + }, ) - errors.ExitWithCode(errors.CodeInvalidRequestAction) } } @@ -49,17 +56,14 @@ func Process() { func parseRequestLength() uint32 { var length uint32 if err := binary.Read(os.Stdin, binary.LittleEndian, &length); err != nil { - // TODO: Original browserpass ignores EOF as if it is expected, is it true? - // if err == io.EOF { - // return - // } log.Error("Unable to parse the length of the browser request: ", err) - response.SendError( + response.SendErrorAndExit( errors.CodeParseRequestLength, - "Unable to parse the length of the browser request", - &map[string]string{"error": err.Error()}, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to parse the length of the browser request", + errors.FieldError: err.Error(), + }, ) - errors.ExitWithCode(errors.CodeParseRequestLength) } return length } @@ -70,12 +74,13 @@ func parseRequest(messageLength uint32) request { reader := &io.LimitedReader{R: os.Stdin, N: int64(messageLength)} if err := json.NewDecoder(reader).Decode(&parsed); err != nil { log.Error("Unable to parse the browser request: ", err) - response.SendError( + response.SendErrorAndExit( errors.CodeParseRequest, - "Unable to parse the browser request", - &map[string]string{"error": err.Error()}, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to parse the browser request", + errors.FieldError: err.Error(), + }, ) - errors.ExitWithCode(errors.CodeParseRequest) } return parsed } diff --git a/response/response.go b/response/response.go index 230de23..b684bc2 100644 --- a/response/response.go +++ b/response/response.go @@ -52,6 +52,16 @@ func MakeListResponse() *ListResponse { } } +// FetchResponse a response format for the "fetch" request +type FetchResponse struct { + Contents string `json:"contents"` +} + +// MakeFetchResponse initializes an empty fetch response +func MakeFetchResponse() *FetchResponse { + return &FetchResponse{} +} + // SendOk sends a success response to the browser extension in the predefined json format func SendOk(data interface{}) { send(&okResponse{ @@ -61,22 +71,16 @@ func SendOk(data interface{}) { }) } -// SendError sends an error response to the browser extension in the predefined json format -func SendError(errorCode errors.Code, errorMsg string, extraParams *map[string]string) { - params := map[string]string{ - "message": errorMsg, - } - if extraParams != nil { - for key, value := range *extraParams { - params[key] = value - } - } +// SendErrorAndExit sends an error response to the browser extension in the predefined json format and exits with the specified exit code +func SendErrorAndExit(errorCode errors.Code, params *map[errors.Field]string) { send(&errorResponse{ Status: "error", Code: errorCode, Version: version.Code, Params: params, }) + + errors.ExitWithCode(errorCode) } func send(data interface{}) {