Implement fetch action, normalize error response params (#13)

This commit is contained in:
Maxim Baz
2018-04-17 00:18:05 +02:00
committed by GitHub
parent bd6ab67d3a
commit 8db942e669
7 changed files with 335 additions and 87 deletions

View File

@@ -41,18 +41,23 @@ should be supplied as a `message` parameter.
## List of Error Codes ## List of Error Codes
| Code | Description | Parameters | | Code | Description | Parameters |
| ---- | ----------------------------------------------------------------------- | ----------------------- | | ---- | ----------------------------------------------------------------------- | -------------------------------------------------- |
| 10 | Unable to parse browser request length | error | | 10 | Unable to parse browser request length | message, error |
| 11 | Unable to parse browser request | error | | 11 | Unable to parse browser request | message, error |
| 12 | Invalid request action | action | | 12 | Invalid request action | message, action |
| 13 | Inaccessible user-configured password store | error, name, path | | 13 | Inaccessible user-configured password store | message, action, error, storePath, storeName |
| 14 | Inaccessible default password store | error, path | | 14 | Inaccessible default password store | message, action, error, storePath |
| 15 | Unable to determine the location of the default password store | error | | 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 | error, name, path | | 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 | error, path | | 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 | error, name, path | | 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 | error, file, name, path | | 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 ## Settings

View File

@@ -17,28 +17,69 @@ const (
CodeParseRequest Code = 11 CodeParseRequest Code = 11
// CodeInvalidRequestAction error parsing a request action // CodeInvalidRequestAction error parsing a request action
CodeInvalidRequestAction = 12 CodeInvalidRequestAction Code = 12
// CodeInaccessiblePasswordStore error accessing a user-configured password store // CodeInaccessiblePasswordStore error accessing a user-configured password store
CodeInaccessiblePasswordStore = 13 CodeInaccessiblePasswordStore Code = 13
// CodeInaccessibleDefaultPasswordStore error accessing the default password store // CodeInaccessibleDefaultPasswordStore error accessing the default password store
CodeInaccessibleDefaultPasswordStore = 14 CodeInaccessibleDefaultPasswordStore Code = 14
// CodeUnknownDefaultPasswordStoreLocation error determining the location of the default password store // 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 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 error reading the default settings of the default password store
CodeUnreadableDefaultPasswordStoreDefaultSettings = 17 CodeUnreadableDefaultPasswordStoreDefaultSettings Code = 17
// CodeUnableToListFilesInPasswordStore error listing files in a password store // 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 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 // ExitWithCode exit with error code

View File

@@ -23,12 +23,16 @@ func configure(request request) {
"The password store '%v' is not accessible at the location '%v': %+v", "The password store '%v' is not accessible at the location '%v': %+v",
store.Name, store.Path, err, store.Name, store.Path, err,
) )
response.SendError( response.SendErrorAndExit(
errors.CodeInaccessiblePasswordStore, errors.CodeInaccessiblePasswordStore,
"The password store is not accessible", &map[errors.Field]string{
&map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, 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 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", "Unable to read the default settings of the user-configured password store '%v' in '%v': %+v",
store.Name, store.Path, err, store.Name, store.Path, err,
) )
response.SendError( response.SendErrorAndExit(
errors.CodeUnreadablePasswordStoreDefaultSettings, errors.CodeUnreadablePasswordStoreDefaultSettings,
"Unable to read the default settings of the password store", &map[errors.Field]string{
&map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, 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 err != nil {
if len(request.Settings.Stores) == 0 { if len(request.Settings.Stores) == 0 {
log.Error("Unable to determine the location of the default password store: ", err) log.Error("Unable to determine the location of the default password store: ", err)
response.SendError( response.SendErrorAndExit(
errors.CodeUnknownDefaultPasswordStoreLocation, errors.CodeUnknownDefaultPasswordStoreLocation,
"Unable to determine the location of the default password store", &map[errors.Field]string{
&map[string]string{"error": err.Error()}, errors.FieldMessage: "Unable to determine the location of the default password store",
errors.FieldAction: "configure",
errors.FieldError: err.Error(),
},
) )
errors.ExitWithCode(errors.CodeUnknownDefaultPasswordStoreLocation)
} }
} else { } else {
responseData.DefaultStore.Path, err = normalizePasswordStorePath(possibleDefaultStorePath) 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", "The default password store is not accessible at the location '%v': %+v",
possibleDefaultStorePath, err, possibleDefaultStorePath, err,
) )
response.SendError( response.SendErrorAndExit(
errors.CodeInaccessibleDefaultPasswordStore, errors.CodeInaccessibleDefaultPasswordStore,
"The default password store is not accessible", &map[errors.Field]string{
&map[string]string{"error": err.Error(), "path": possibleDefaultStorePath}, 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", "Unable to read the default settings of the default password store in '%v': %+v",
responseData.DefaultStore.Path, err, responseData.DefaultStore.Path, err,
) )
response.SendError( response.SendErrorAndExit(
errors.CodeUnreadableDefaultPasswordStoreDefaultSettings, errors.CodeUnreadableDefaultPasswordStoreDefaultSettings,
"Unable to read the default settings of the default password store", &map[errors.Field]string{
&map[string]string{"error": err.Error(), "path": responseData.DefaultStore.Path}, 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)
} }
} }

164
request/fetch.go Normal file
View File

@@ -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
}

View File

@@ -20,12 +20,16 @@ func listFiles(request request) {
"The password store '%v' is not accessible at the location '%v': %+v", "The password store '%v' is not accessible at the location '%v': %+v",
store.Name, store.Path, err, store.Name, store.Path, err,
) )
response.SendError( response.SendErrorAndExit(
errors.CodeInaccessiblePasswordStore, errors.CodeInaccessiblePasswordStore,
"The password store is not accessible", &map[errors.Field]string{
&map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, 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 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", "Unable to list the files in the password store '%v' at the location '%v': %+v",
store.Name, store.Path, err, store.Name, store.Path, err,
) )
response.SendError( response.SendErrorAndExit(
errors.CodeUnableToListFilesInPasswordStore, errors.CodeUnableToListFilesInPasswordStore,
"Unable to list the files in the password store", &map[errors.Field]string{
&map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, 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 { 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", "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, file, store.Name, store.Path, err,
) )
response.SendError( response.SendErrorAndExit(
errors.CodeUnableToDetermineRelativeFilePathInPasswordStore, errors.CodeUnableToDetermineRelativeFilePathInPasswordStore,
"Unable to determine the relative path for a file in the password store", &map[errors.Field]string{
&map[string]string{"error": err.Error(), "file": file, "name": store.Name, "path": store.Path}, 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 files[i] = relativePath
} }

View File

@@ -11,15 +11,21 @@ import (
log "github.com/sirupsen/logrus" 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 { type request struct {
Action string `json:"action"` Action string `json:"action"`
Settings struct { Settings settings `json:"settings"`
GpgPath string `json:"gpgPath"` File string `json:"file"`
Stores map[string]struct { Store string `json:"store"`
Name string `json:"name"`
Path string `json:"path"`
}
} `json:"settings"`
} }
// Process handles browser request // Process handles browser request
@@ -33,15 +39,16 @@ func Process() {
case "list": case "list":
listFiles(request) listFiles(request)
case "fetch": case "fetch":
break fetchDecryptedContents(request)
default: default:
log.Errorf("Received a browser request with an unknown action: %+v", request) log.Errorf("Received a browser request with an unknown action: %+v", request)
response.SendError( response.SendErrorAndExit(
errors.CodeInvalidRequestAction, errors.CodeInvalidRequestAction,
"Invalid request action", &map[errors.Field]string{
&map[string]string{"action": request.Action}, errors.FieldMessage: "Invalid request action",
errors.FieldAction: request.Action,
},
) )
errors.ExitWithCode(errors.CodeInvalidRequestAction)
} }
} }
@@ -49,17 +56,14 @@ func Process() {
func parseRequestLength() uint32 { func parseRequestLength() uint32 {
var length uint32 var length uint32
if err := binary.Read(os.Stdin, binary.LittleEndian, &length); err != nil { 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) log.Error("Unable to parse the length of the browser request: ", err)
response.SendError( response.SendErrorAndExit(
errors.CodeParseRequestLength, errors.CodeParseRequestLength,
"Unable to parse the length of the browser request", &map[errors.Field]string{
&map[string]string{"error": err.Error()}, errors.FieldMessage: "Unable to parse the length of the browser request",
errors.FieldError: err.Error(),
},
) )
errors.ExitWithCode(errors.CodeParseRequestLength)
} }
return length return length
} }
@@ -70,12 +74,13 @@ func parseRequest(messageLength uint32) request {
reader := &io.LimitedReader{R: os.Stdin, N: int64(messageLength)} reader := &io.LimitedReader{R: os.Stdin, N: int64(messageLength)}
if err := json.NewDecoder(reader).Decode(&parsed); err != nil { if err := json.NewDecoder(reader).Decode(&parsed); err != nil {
log.Error("Unable to parse the browser request: ", err) log.Error("Unable to parse the browser request: ", err)
response.SendError( response.SendErrorAndExit(
errors.CodeParseRequest, errors.CodeParseRequest,
"Unable to parse the browser request", &map[errors.Field]string{
&map[string]string{"error": err.Error()}, errors.FieldMessage: "Unable to parse the browser request",
errors.FieldError: err.Error(),
},
) )
errors.ExitWithCode(errors.CodeParseRequest)
} }
return parsed return parsed
} }

View File

@@ -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 // SendOk sends a success response to the browser extension in the predefined json format
func SendOk(data interface{}) { func SendOk(data interface{}) {
send(&okResponse{ send(&okResponse{
@@ -61,22 +71,16 @@ func SendOk(data interface{}) {
}) })
} }
// SendError sends an error response to the browser extension in the predefined json format // SendErrorAndExit sends an error response to the browser extension in the predefined json format and exits with the specified exit code
func SendError(errorCode errors.Code, errorMsg string, extraParams *map[string]string) { func SendErrorAndExit(errorCode errors.Code, params *map[errors.Field]string) {
params := map[string]string{
"message": errorMsg,
}
if extraParams != nil {
for key, value := range *extraParams {
params[key] = value
}
}
send(&errorResponse{ send(&errorResponse{
Status: "error", Status: "error",
Code: errorCode, Code: errorCode,
Version: version.Code, Version: version.Code,
Params: params, Params: params,
}) })
errors.ExitWithCode(errorCode)
} }
func send(data interface{}) { func send(data interface{}) {