diff --git a/PROTOCOL.md b/PROTOCOL.md index f072d6f..c06e7ec 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -41,11 +41,16 @@ should be supplied as a `message` parameter. ## List of Error Codes -| Code | Description | Parameters | -| ---- | -------------------------------------- | ---------- | -| 10 | Unable to parse browser request length | | -| 11 | Unable to parse browser request | | -| 12 | Invalid request action | action | +| 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 | ## Settings @@ -106,7 +111,7 @@ is alive, determine the version at startup, and provide per-store defaults. "data": { "defaultStore": { "path": "/path/to/default/store", - "defaultSettings": "", + "settings": "", }, “storeSettings”: { “storeName”: "" diff --git a/errors/errors.go b/errors/errors.go index 93e0c62..cca95a8 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -10,14 +10,29 @@ type Code int // Error codes that are sent to the browser extension and used as exit codes in the app. // DO NOT MODIFY THE VALUES, always append new error codes to the bottom. const ( - // CodeParseRequestLength error parsing request length + // CodeParseRequestLength error parsing a request length CodeParseRequestLength Code = 10 - // CodeParseRequest error parsing request + // CodeParseRequest error parsing a request CodeParseRequest Code = 11 - // CodeInvalidRequestAction error parsing request action + // CodeInvalidRequestAction error parsing a request action CodeInvalidRequestAction = 12 + + // CodeInaccessiblePasswordStore error accessing a user-configured password store + CodeInaccessiblePasswordStore = 13 + + // CodeInaccessibleDefaultPasswordStore error accessing the default password store + CodeInaccessibleDefaultPasswordStore = 14 + + // CodeUnknownDefaultPasswordStoreLocation error determining the location of the default password store + CodeUnknownDefaultPasswordStoreLocation = 15 + + // CodeUnreadablePasswordStoreDefaultSettings error reading the default settings of a user-configured password store + CodeUnreadablePasswordStoreDefaultSettings = 16 + + // CodeUnreadableDefaultPasswordStoreDefaultSettings error reading the default settings of the default password store + CodeUnreadableDefaultPasswordStoreDefaultSettings = 17 ) // ExitWithCode exit with error code diff --git a/request/common.go b/request/common.go new file mode 100644 index 0000000..4de4c98 --- /dev/null +++ b/request/common.go @@ -0,0 +1,30 @@ +package request + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +func normalizePasswordStorePath(storePath string) (string, error) { + if strings.HasPrefix(storePath, "~/") { + storePath = filepath.Join("$HOME", storePath[2:]) + } + storePath = os.ExpandEnv(storePath) + + directStorePath, err := filepath.EvalSymlinks(storePath) + if err != nil { + return "", err + } + storePath = directStorePath + + stat, err := os.Stat(storePath) + if err != nil { + return "", err + } + if !stat.IsDir() { + return "", errors.New("The specified path exists, but is not a directory") + } + return storePath, nil +} diff --git a/request/configure.go b/request/configure.go new file mode 100644 index 0000000..2a99e66 --- /dev/null +++ b/request/configure.go @@ -0,0 +1,126 @@ +package request + +import ( + "io/ioutil" + "os" + "os/user" + "path/filepath" + + "github.com/browserpass/browserpass-native/errors" + "github.com/browserpass/browserpass-native/response" + log "github.com/sirupsen/logrus" +) + +func configure(request request) { + responseData := response.MakeConfigureResponse() + + // Check that each and every store in the settings exists and is accessible. + // Then read the default configuration for these stores (if available). + for _, store := range request.Settings.Stores { + 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.SendError( + errors.CodeInaccessiblePasswordStore, + "The password store is not accessible", + &map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, + ) + errors.ExitWithCode(errors.CodeInaccessiblePasswordStore) + } + + store.Path = normalizedStorePath + + responseData.StoreSettings[store.Name], err = readDefaultSettings(store.Path) + if err != nil { + log.Errorf( + "Unable to read the default settings of the user-configured password store '%v' in '%v': %+v", + store.Name, store.Path, err, + ) + response.SendError( + errors.CodeUnreadablePasswordStoreDefaultSettings, + "Unable to read the default settings of the password store", + &map[string]string{"error": err.Error(), "name": store.Name, "path": store.Path}, + ) + errors.ExitWithCode(errors.CodeUnreadablePasswordStoreDefaultSettings) + } + } + + // Check whether a store in the default location exists and is accessible. + // If there is at least one store in the settings, it is expected that there might be no store in the default location => do not return errors. + // However, if there are no stores in the settings, user expects to use the default password store => return an error if it is not accessible. + possibleDefaultStorePath, err := getDefaultPasswordStorePath() + if err != nil { + if len(request.Settings.Stores) == 0 { + log.Error("Unable to determine the location of the default password store: ", err) + response.SendError( + errors.CodeUnknownDefaultPasswordStoreLocation, + "Unable to determine the location of the default password store", + &map[string]string{"error": err.Error()}, + ) + errors.ExitWithCode(errors.CodeUnknownDefaultPasswordStoreLocation) + } + } else { + responseData.DefaultStore.Path, err = normalizePasswordStorePath(possibleDefaultStorePath) + if err != nil { + if len(request.Settings.Stores) == 0 { + log.Errorf( + "The default password store is not accessible at the location '%v': %+v", + possibleDefaultStorePath, err, + ) + response.SendError( + errors.CodeInaccessibleDefaultPasswordStore, + "The default password store is not accessible", + &map[string]string{"error": err.Error(), "path": possibleDefaultStorePath}, + ) + errors.ExitWithCode(errors.CodeInaccessibleDefaultPasswordStore) + } + } + } + + if responseData.DefaultStore.Path != "" { + responseData.DefaultStore.Settings, err = readDefaultSettings(responseData.DefaultStore.Path) + if err != nil { + log.Errorf( + "Unable to read the default settings of the default password store in '%v': %+v", + responseData.DefaultStore.Path, err, + ) + response.SendError( + errors.CodeUnreadableDefaultPasswordStoreDefaultSettings, + "Unable to read the default settings of the default password store", + &map[string]string{"error": err.Error(), "path": responseData.DefaultStore.Path}, + ) + errors.ExitWithCode(errors.CodeUnreadableDefaultPasswordStoreDefaultSettings) + } + } + + response.SendOk(responseData) +} + +func getDefaultPasswordStorePath() (string, error) { + path := os.Getenv("PASSWORD_STORE_DIR") + if path != "" { + return path, nil + } + + usr, err := user.Current() + if err != nil { + return "", err + } + + path = filepath.Join(usr.HomeDir, ".password-store") + return path, nil +} + +func readDefaultSettings(storePath string) (string, error) { + content, err := ioutil.ReadFile(filepath.Join(storePath, ".browserpass.json")) + if err == nil { + return string(content), nil + } + if os.IsNotExist(err) { + return "", nil + } + return "", err +} diff --git a/request/request.go b/request/process.go similarity index 68% rename from request/request.go rename to request/process.go index 8411624..d2f712f 100644 --- a/request/request.go +++ b/request/process.go @@ -12,8 +12,14 @@ import ( ) type request struct { - Action string `json:"action"` - Settings interface{} `json:"settings"` + Action string `json:"action"` + Settings struct { + GpgPath string `json:"gpgPath"` + Stores map[string]struct { + Name string `json:"name"` + Path string `json:"path"` + } + } `json:"settings"` } // Process handles browser request @@ -23,14 +29,18 @@ func Process() { switch request.Action { case "configure": - break + configure(request) case "list": break case "fetch": break default: log.Errorf("Received a browser request with an unknown action: %+v", request) - response.SendError(errors.CodeInvalidRequestAction, "Invalid request action", &map[string]string{"action": request.Action}) + response.SendError( + errors.CodeInvalidRequestAction, + "Invalid request action", + &map[string]string{"action": request.Action}, + ) errors.ExitWithCode(errors.CodeInvalidRequestAction) } } @@ -44,7 +54,11 @@ func parseRequestLength() uint32 { // return // } log.Error("Unable to parse the length of the browser request: ", err) - response.SendError(errors.CodeParseRequestLength, "Unable to parse the length of the browser request", nil) + response.SendError( + errors.CodeParseRequestLength, + "Unable to parse the length of the browser request", + &map[string]string{"error": err.Error()}, + ) errors.ExitWithCode(errors.CodeParseRequestLength) } return length @@ -56,7 +70,11 @@ 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(errors.CodeParseRequest, "Unable to parse the browser request", nil) + response.SendError( + errors.CodeParseRequest, + "Unable to parse the browser request", + &map[string]string{"error": err.Error()}, + ) errors.ExitWithCode(errors.CodeParseRequest) } return parsed diff --git a/response/response.go b/response/response.go index 1171ba7..ac12473 100644 --- a/response/response.go +++ b/response/response.go @@ -24,6 +24,31 @@ type errorResponse struct { Params interface{} `json:"params"` } +// ConfigureResponse a response format for the "configure" request +type ConfigureResponse struct { + DefaultStore struct { + Path string `json:"path"` + Settings string `json:"settings"` + } `json:"defaultStore"` + StoreSettings map[string]string `json:"storeSettings"` +} + +// MakeConfigureResponse initializes an empty configure response +func MakeConfigureResponse() *ConfigureResponse { + return &ConfigureResponse{ + StoreSettings: make(map[string]string), + } +} + +// SendOk sends a success response to the browser extension in the predefined json format +func SendOk(data interface{}) { + send(&okResponse{ + Status: "ok", + Version: version.Code, + Data: data, + }) +} + // 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{