From b0be74472997d2caf181691ea9cb97281a23fcf3 Mon Sep 17 00:00:00 2001 From: Maxim Baz Date: Fri, 11 Sep 2020 19:49:24 +0200 Subject: [PATCH 1/5] Implement "tree" request --- PROTOCOL.md | 65 ++++++++++++++++++------- errors/errors.go | 33 +++++++------ request/process.go | 2 + request/tree.go | 112 +++++++++++++++++++++++++++++++++++++++++++ response/response.go | 12 +++++ 5 files changed, 192 insertions(+), 32 deletions(-) create mode 100644 request/tree.go diff --git a/PROTOCOL.md b/PROTOCOL.md index c0d81d5..c187e45 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -41,23 +41,25 @@ should be supplied as a `message` parameter. ## List of Error Codes -| 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, storeId, 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, storeId, 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, storeId, storePath, storeName | -| 19 | Unable to determine a relative path for a file in a password store | message, action, error, storeId, storePath, storeName, file | -| 20 | Invalid password store ID | message, action, storeId | -| 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, storeId, storePath, storeName, file | +| 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, storeId, 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, storeId, 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, storeId, storePath, storeName | +| 19 | Unable to determine a relative path for a file in a password store | message, action, error, storeId, storePath, storeName, file | +| 20 | Invalid password store ID | message, action, storeId | +| 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, storeId, storePath, storeName, file | +| 25 | Unable to list directories in a password store | message, action, error, storeId, storePath, storeName | +| 26 | Unable to determine a relative path for a directory in a password store | message, action, error, storeId, storePath, storeName, directory | ## Settings @@ -157,6 +159,35 @@ is the ID of a password store, the key in `"settings.stores"` object. } ``` +### Tree + +Get a list of all nested directories for each of a provided array of directory paths. The `storeN` +is the ID of a password store, the key in `"settings.stores"` object. + +#### Request + +``` +{ + "settings": , + "action": "tree" +} +``` + +#### Response + +``` +{ + "status": "ok", + "version": , + "data": { + "directories": { + "storeN": ["", "<...>"], + "storeN+1": ["", "<...>"] + } + } +} +``` + ### Fetch Get the decrypted contents of a specific file. diff --git a/errors/errors.go b/errors/errors.go index f1e281f..8e842a1 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -10,21 +10,23 @@ 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 Code = 10 - CodeParseRequest Code = 11 - CodeInvalidRequestAction Code = 12 - CodeInaccessiblePasswordStore Code = 13 - CodeInaccessibleDefaultPasswordStore Code = 14 - CodeUnknownDefaultPasswordStoreLocation Code = 15 - CodeUnreadablePasswordStoreDefaultSettings Code = 16 - CodeUnreadableDefaultPasswordStoreDefaultSettings Code = 17 - CodeUnableToListFilesInPasswordStore Code = 18 - CodeUnableToDetermineRelativeFilePathInPasswordStore Code = 19 - CodeInvalidPasswordStore Code = 20 - CodeInvalidGpgPath Code = 21 - CodeUnableToDetectGpgPath Code = 22 - CodeInvalidPasswordFileExtension Code = 23 - CodeUnableToDecryptPasswordFile Code = 24 + CodeParseRequestLength Code = 10 + CodeParseRequest Code = 11 + CodeInvalidRequestAction Code = 12 + CodeInaccessiblePasswordStore Code = 13 + CodeInaccessibleDefaultPasswordStore Code = 14 + CodeUnknownDefaultPasswordStoreLocation Code = 15 + CodeUnreadablePasswordStoreDefaultSettings Code = 16 + CodeUnreadableDefaultPasswordStoreDefaultSettings Code = 17 + CodeUnableToListFilesInPasswordStore Code = 18 + CodeUnableToDetermineRelativeFilePathInPasswordStore Code = 19 + CodeInvalidPasswordStore Code = 20 + CodeInvalidGpgPath Code = 21 + CodeUnableToDetectGpgPath Code = 22 + CodeInvalidPasswordFileExtension Code = 23 + CodeUnableToDecryptPasswordFile Code = 24 + CodeUnableToListDirectoriesInPasswordStore Code = 25 + CodeUnableToDetermineRelativeDirectoryPathInPasswordStore Code = 26 ) // Field extra field in the error response params @@ -40,6 +42,7 @@ const ( FieldStoreName Field = "storeName" FieldStorePath Field = "storePath" FieldFile Field = "file" + FieldDirectory Field = "directory" FieldGpgPath Field = "gpgPath" ) diff --git a/request/process.go b/request/process.go index 4c53068..81d3e1f 100644 --- a/request/process.go +++ b/request/process.go @@ -66,6 +66,8 @@ func Process() { configure(request) case "list": listFiles(request) + case "tree": + listDirectories(request) case "fetch": fetchDecryptedContents(request) case "echo": diff --git a/request/tree.go b/request/tree.go new file mode 100644 index 0000000..79dd1bf --- /dev/null +++ b/request/tree.go @@ -0,0 +1,112 @@ +package request + +import ( + "os" + "path/filepath" + "sort" + "strings" + "sync" + + "github.com/browserpass/browserpass-native/errors" + "github.com/browserpass/browserpass-native/response" + "github.com/mattn/go-zglob/fastwalk" + log "github.com/sirupsen/logrus" +) + +func listDirectories(request *request) { + responseData := response.MakeTreeResponse() + + for _, store := range request.Settings.Stores { + normalizedStorePath, err := normalizePasswordStorePath(store.Path) + if err != nil { + log.Errorf( + "The password store '%+v' is not accessible at its location: %+v", + store, err, + ) + response.SendErrorAndExit( + errors.CodeInaccessiblePasswordStore, + &map[errors.Field]string{ + errors.FieldMessage: "The password store is not accessible", + errors.FieldAction: "tree", + errors.FieldError: err.Error(), + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + + store.Path = normalizedStorePath + + var mu sync.Mutex + directories := []string{} + err = fastwalk.FastWalk(store.Path, func(path string, typ os.FileMode) error { + if typ == os.ModeSymlink { + followedPath, err := filepath.EvalSymlinks(path) + if err == nil { + fi, err := os.Lstat(followedPath) + if err == nil && fi.IsDir() { + return fastwalk.TraverseLink + } + } + } + + if typ.IsDir() && path != store.Path { + if filepath.Base(path) == ".git" { + return filepath.SkipDir + } + mu.Lock() + directories = append(directories, path) + mu.Unlock() + } + + return nil + }) + + if err != nil { + log.Errorf( + "Unable to list the directory tree in the password store '%+v' at its location: %+v", + store, err, + ) + response.SendErrorAndExit( + errors.CodeUnableToListDirectoriesInPasswordStore, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to list the directory tree in the password store", + errors.FieldAction: "tree", + errors.FieldError: err.Error(), + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + + for i, directory := range directories { + relativePath, err := filepath.Rel(store.Path, directory) + if err != nil { + log.Errorf( + "Unable to determine the relative path for a file '%v' in the password store '%+v': %+v", + directory, store, err, + ) + response.SendErrorAndExit( + errors.CodeUnableToDetermineRelativeDirectoryPathInPasswordStore, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to determine the relative path for a directory in the password store", + errors.FieldAction: "tree", + errors.FieldError: err.Error(), + errors.FieldDirectory: directory, + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + directories[i] = strings.Replace(relativePath, "\\", "/", -1) // normalize Windows paths + } + + sort.Strings(directories) + responseData.Directories[store.ID] = directories + } + + response.SendOk(responseData) +} diff --git a/response/response.go b/response/response.go index 5493de6..e590f10 100644 --- a/response/response.go +++ b/response/response.go @@ -52,6 +52,18 @@ func MakeListResponse() *ListResponse { } } +// TreeResponse a response format for the "tree" request +type TreeResponse struct { + Directories map[string][]string `json:"directories"` +} + +// MakeTreeResponse initializes an empty tree response +func MakeTreeResponse() *TreeResponse { + return &TreeResponse{ + Directories: make(map[string][]string), + } +} + // FetchResponse a response format for the "fetch" request type FetchResponse struct { Contents string `json:"contents"` From 09f5cbc8f56095afb810720238bb2937f3a692cd Mon Sep 17 00:00:00 2001 From: Maxim Baz Date: Fri, 11 Sep 2020 21:45:41 +0200 Subject: [PATCH 2/5] Extract GPG helpers into a separate package --- helpers/helpers.go | 50 ++++++++++++++++++++++++++++++++++++++++ request/configure.go | 3 ++- request/fetch.go | 54 ++++---------------------------------------- 3 files changed, 56 insertions(+), 51 deletions(-) create mode 100644 helpers/helpers.go diff --git a/helpers/helpers.go b/helpers/helpers.go new file mode 100644 index 0000000..d597eef --- /dev/null +++ b/helpers/helpers.go @@ -0,0 +1,50 @@ +package helpers + +import ( + "bytes" + "fmt" + "os" + "os/exec" +) + +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() +} + +func GpgDecryptFile(filePath string, gpgPath string) (string, error) { + passwordFile, err := os.Open(filePath) + 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 "", fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String()) + } + + return stdout.String(), nil +} diff --git a/request/configure.go b/request/configure.go index 897e03b..24a00ae 100644 --- a/request/configure.go +++ b/request/configure.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/browserpass/browserpass-native/errors" + "github.com/browserpass/browserpass-native/helpers" "github.com/browserpass/browserpass-native/response" log "github.com/sirupsen/logrus" ) @@ -16,7 +17,7 @@ func configure(request *request) { // User configured gpgPath in the browser, check if it is a valid binary to use if request.Settings.GpgPath != "" { - err := validateGpgBinary(request.Settings.GpgPath) + err := helpers.ValidateGpgBinary(request.Settings.GpgPath) if err != nil { log.Errorf( "The provided gpg binary path '%v' is invalid: %+v", diff --git a/request/fetch.go b/request/fetch.go index fd35610..134a3c8 100644 --- a/request/fetch.go +++ b/request/fetch.go @@ -1,14 +1,11 @@ package request import ( - "bytes" - "fmt" - "os" - "os/exec" "path/filepath" "strings" "github.com/browserpass/browserpass-native/errors" + "github.com/browserpass/browserpass-native/helpers" "github.com/browserpass/browserpass-native/response" log "github.com/sirupsen/logrus" ) @@ -71,7 +68,7 @@ func fetchDecryptedContents(request *request) { } else { gpgPath = store.Settings.GpgPath } - err = validateGpgBinary(gpgPath) + err = helpers.ValidateGpgBinary(gpgPath) if err != nil { log.Errorf( "The provided gpg binary path '%v' is invalid: %+v", @@ -88,7 +85,7 @@ func fetchDecryptedContents(request *request) { ) } } else { - gpgPath, err = detectGpgBinary() + gpgPath, err = helpers.DetectGpgBinary() if err != nil { log.Error("Unable to detect the location of the gpg binary: ", err) response.SendErrorAndExit( @@ -102,7 +99,7 @@ func fetchDecryptedContents(request *request) { } } - responseData.Contents, err = decryptFile(&store, request.File, gpgPath) + responseData.Contents, err = helpers.GpgDecryptFile(filepath.Join(store.Path, request.File), gpgPath) if err != nil { log.Errorf( "Unable to decrypt the password file '%v' in the password store '%+v': %+v", @@ -124,46 +121,3 @@ 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() -} - -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 "", fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String()) - } - - return stdout.String(), nil -} From a4a90dc5ac577d77d0f1fa35a6d6943e7034e71a Mon Sep 17 00:00:00 2001 From: Maxim Baz Date: Fri, 11 Sep 2020 23:27:42 +0200 Subject: [PATCH 3/5] Implement `save` request --- PROTOCOL.md | 25 +++++++ errors/errors.go | 3 + helpers/helpers.go | 48 ++++++++++++++ request/process.go | 3 + request/save.go | 153 +++++++++++++++++++++++++++++++++++++++++++ response/response.go | 9 +++ 6 files changed, 241 insertions(+) create mode 100644 request/save.go diff --git a/PROTOCOL.md b/PROTOCOL.md index c187e45..9422795 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -215,6 +215,31 @@ Get the decrypted contents of a specific file. } ``` +### Save + +Encrypt the given contents and save to a specific file. + +#### Request + +``` +{ + "settings": , + "action": "save", + "storeId": "", + "file": "relative/path/to/file.gpg", + "contents": "" +} +``` + +#### Response + +``` +{ + "status": "ok", + "version": +} +``` + ### Echo Send the `echoResponse` in the request as a response. diff --git a/errors/errors.go b/errors/errors.go index 8e842a1..69f88e3 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -27,6 +27,9 @@ const ( CodeUnableToDecryptPasswordFile Code = 24 CodeUnableToListDirectoriesInPasswordStore Code = 25 CodeUnableToDetermineRelativeDirectoryPathInPasswordStore Code = 26 + CodeEmptyContents Code = 27 + CodeUnableToDetectGpgRecipients Code = 28 + CodeUnableToEncryptPasswordFile Code = 29 ) // Field extra field in the error response params diff --git a/helpers/helpers.go b/helpers/helpers.go index d597eef..9554f23 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -3,8 +3,11 @@ package helpers import ( "bytes" "fmt" + "io/ioutil" "os" "os/exec" + "path/filepath" + "strings" ) func DetectGpgBinary() (string, error) { @@ -48,3 +51,48 @@ func GpgDecryptFile(filePath string, gpgPath string) (string, error) { return stdout.String(), nil } + +func GpgEncryptFile(filePath string, contents string, recipients []string, gpgPath string) error { + err := os.MkdirAll(filepath.Dir(filePath), 0755) + if err != nil { + return fmt.Errorf("Unable to create directory structure: %s", err.Error()) + } + + var stdout, stderr bytes.Buffer + gpgOptions := []string{"--encrypt", "--yes", "--quiet", "--batch", "--output", filePath} + for _, recipient := range recipients { + gpgOptions = append(gpgOptions, "--recipient", recipient) + } + + cmd := exec.Command(gpgPath, gpgOptions...) + cmd.Stdin = strings.NewReader(contents) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err = cmd.Run(); err != nil { + return fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String()) + } + + return nil +} + +func DetectGpgRecipients(filePath string) ([]string, error) { + dir := filepath.Dir(filePath) + for { + file, err := ioutil.ReadFile(filepath.Join(dir, ".gpg-id")) + if err == nil { + return strings.Split(strings.TrimSpace(string(file)), "\n"), nil + } + + if !os.IsNotExist(err) { + return nil, fmt.Errorf("Unable to open `.gpg-id` file: %s", err.Error()) + } + + parentDir := filepath.Dir(dir) + if parentDir == dir { + return nil, fmt.Errorf("Unable to find '.gpg-id' file") + } + + dir = parentDir + } +} diff --git a/request/process.go b/request/process.go index 81d3e1f..a05acc1 100644 --- a/request/process.go +++ b/request/process.go @@ -31,6 +31,7 @@ type request struct { Action string `json:"action"` Settings settings `json:"settings"` File string `json:"file"` + Contents string `json:"contents"` StoreID string `json:"storeId"` EchoResponse interface{} `json:"echoResponse"` } @@ -70,6 +71,8 @@ func Process() { listDirectories(request) case "fetch": fetchDecryptedContents(request) + case "save": + saveEncryptedContents(request) case "echo": response.SendRaw(request.EchoResponse) default: diff --git a/request/save.go b/request/save.go new file mode 100644 index 0000000..55a7ff3 --- /dev/null +++ b/request/save.go @@ -0,0 +1,153 @@ +package request + +import ( + "path/filepath" + "strings" + + "github.com/browserpass/browserpass-native/errors" + "github.com/browserpass/browserpass-native/helpers" + "github.com/browserpass/browserpass-native/response" + log "github.com/sirupsen/logrus" +) + +func saveEncryptedContents(request *request) { + responseData := response.MakeSaveResponse() + + 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: "save", + errors.FieldFile: request.File, + }, + ) + } + + if request.Contents == "" { + log.Errorf("The provided contents is empty") + response.SendErrorAndExit( + errors.CodeEmptyContents, + &map[errors.Field]string{ + errors.FieldMessage: "The provided contents is empty", + errors.FieldAction: "save", + }, + ) + } + + 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: "save", + errors.FieldStoreID: request.StoreID, + }, + ) + } + + normalizedStorePath, err := normalizePasswordStorePath(store.Path) + if err != nil { + log.Errorf( + "The password store '%+v' is not accessible at its location: %+v", + store, err, + ) + response.SendErrorAndExit( + errors.CodeInaccessiblePasswordStore, + &map[errors.Field]string{ + errors.FieldMessage: "The password store is not accessible", + errors.FieldAction: "save", + errors.FieldError: err.Error(), + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + store.Path = normalizedStorePath + + var gpgPath string + if request.Settings.GpgPath != "" || store.Settings.GpgPath != "" { + if request.Settings.GpgPath != "" { + gpgPath = request.Settings.GpgPath + } else { + gpgPath = store.Settings.GpgPath + } + err = helpers.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: "save", + errors.FieldError: err.Error(), + errors.FieldGpgPath: gpgPath, + }, + ) + } + } else { + gpgPath, err = helpers.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: "save", + errors.FieldError: err.Error(), + }, + ) + } + } + + filePath := filepath.Join(store.Path, request.File) + + recipients, err := helpers.DetectGpgRecipients(filePath) + if err != nil { + log.Error("Unable to detect gpg recipients for encryption: ", err) + response.SendErrorAndExit( + errors.CodeUnableToDetectGpgRecipients, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to detect gpg recipients for encryption", + errors.FieldAction: "save", + errors.FieldError: err.Error(), + errors.FieldFile: request.File, + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + + err = helpers.GpgEncryptFile(filePath, request.Contents, recipients, gpgPath) + if err != nil { + log.Errorf( + "Unable to encrypt the password file '%v' in the password store '%+v': %+v", + request.File, store, err, + ) + response.SendErrorAndExit( + errors.CodeUnableToEncryptPasswordFile, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to encrypt the password file", + errors.FieldAction: "save", + errors.FieldError: err.Error(), + errors.FieldFile: request.File, + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + + response.SendOk(responseData) +} diff --git a/response/response.go b/response/response.go index e590f10..5825244 100644 --- a/response/response.go +++ b/response/response.go @@ -74,6 +74,15 @@ func MakeFetchResponse() *FetchResponse { return &FetchResponse{} } +// SaveResponse a response format for the "save" request +type SaveResponse struct { +} + +// MakeSaveResponse initializes an empty save response +func MakeSaveResponse() *SaveResponse { + return &SaveResponse{} +} + // SendOk sends a success response to the browser extension in the predefined json format func SendOk(data interface{}) { SendRaw(&okResponse{ From 1854ad901db6e3ad1aecee61c215d04f151a8d38 Mon Sep 17 00:00:00 2001 From: Maxim Baz Date: Sat, 12 Sep 2020 00:34:16 +0200 Subject: [PATCH 4/5] Implement `delete` request --- PROTOCOL.md | 30 ++++++++++ errors/errors.go | 5 +- helpers/helpers.go | 16 ++++++ request/delete.go | 132 +++++++++++++++++++++++++++++++++++++++++++ request/process.go | 2 + request/save.go | 10 ++-- response/response.go | 9 +++ 7 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 request/delete.go diff --git a/PROTOCOL.md b/PROTOCOL.md index 9422795..6528b86 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -60,6 +60,12 @@ should be supplied as a `message` parameter. | 24 | Unable to decrypt the password file | message, action, error, storeId, storePath, storeName, file | | 25 | Unable to list directories in a password store | message, action, error, storeId, storePath, storeName | | 26 | Unable to determine a relative path for a directory in a password store | message, action, error, storeId, storePath, storeName, directory | +| 27 | The entry contents is missing | message, action | +| 28 | Unable to determine the recepients for the gpg encryption | message, action, error, storeId, storePath, storeName, file | +| 29 | Unable to encrypt the password file | message, action, error, storeId, storePath, storeName, file | +| 30 | Unable to delete the password file | message, action, error, storeId, storePath, storeName, file | +| 31 | Unable to determine if directory is empty and can be deleted | message, action, error, storeId, storePath, storeName, directory | +| 32 | Unable to delete the empty directory | message, action, error, storeId, storePath, storeName, directory | ## Settings @@ -240,6 +246,30 @@ Encrypt the given contents and save to a specific file. } ``` +### Delete + +Delete a specific file and empty parent directories caused by the deletion, if any. + +#### Request + +``` +{ + "settings": , + "action": "delete", + "storeId": "", + "file": "relative/path/to/file.gpg" +} +``` + +#### Response + +``` +{ + "status": "ok", + "version": +} +``` + ### Echo Send the `echoResponse` in the request as a response. diff --git a/errors/errors.go b/errors/errors.go index 69f88e3..4c2b277 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -28,8 +28,11 @@ const ( CodeUnableToListDirectoriesInPasswordStore Code = 25 CodeUnableToDetermineRelativeDirectoryPathInPasswordStore Code = 26 CodeEmptyContents Code = 27 - CodeUnableToDetectGpgRecipients Code = 28 + CodeUnableToDetermineGpgRecipients Code = 28 CodeUnableToEncryptPasswordFile Code = 29 + CodeUnableToDeletePasswordFile Code = 30 + CodeUnableToDetermineIsDirectoryEmpty Code = 31 + CodeUnableToDeleteEmptyDirectory Code = 32 ) // Field extra field in the error response params diff --git a/helpers/helpers.go b/helpers/helpers.go index 9554f23..d342dd1 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -3,6 +3,7 @@ package helpers import ( "bytes" "fmt" + "io" "io/ioutil" "os" "os/exec" @@ -96,3 +97,18 @@ func DetectGpgRecipients(filePath string) ([]string, error) { dir = parentDir } } + +func IsDirectoryEmpty(dirPath string) (bool, error) { + f, err := os.Open(dirPath) + if err != nil { + return false, err + } + defer f.Close() + + _, err = f.Readdirnames(1) + if err == io.EOF { + return true, nil + } + + return false, err +} diff --git a/request/delete.go b/request/delete.go new file mode 100644 index 0000000..5505aba --- /dev/null +++ b/request/delete.go @@ -0,0 +1,132 @@ +package request + +import ( + "os" + "path/filepath" + "strings" + + "github.com/browserpass/browserpass-native/errors" + "github.com/browserpass/browserpass-native/helpers" + "github.com/browserpass/browserpass-native/response" + log "github.com/sirupsen/logrus" +) + +func deleteFile(request *request) { + responseData := response.MakeDeleteResponse() + + 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: "delete", + errors.FieldFile: request.File, + }, + ) + } + + 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: "delete", + errors.FieldStoreID: request.StoreID, + }, + ) + } + + normalizedStorePath, err := normalizePasswordStorePath(store.Path) + if err != nil { + log.Errorf( + "The password store '%+v' is not accessible at its location: %+v", + store, err, + ) + response.SendErrorAndExit( + errors.CodeInaccessiblePasswordStore, + &map[errors.Field]string{ + errors.FieldMessage: "The password store is not accessible", + errors.FieldAction: "delete", + errors.FieldError: err.Error(), + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + store.Path = normalizedStorePath + + filePath := filepath.Join(store.Path, request.File) + + err = os.Remove(filePath) + if err != nil { + log.Error("Unable to delete the password file: ", err) + response.SendErrorAndExit( + errors.CodeUnableToDeletePasswordFile, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to delete the password file", + errors.FieldAction: "delete", + errors.FieldError: err.Error(), + errors.FieldFile: request.File, + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + + parentDir := filepath.Dir(filePath) + for { + if parentDir == store.Path { + break + } + + isEmpty, err := helpers.IsDirectoryEmpty(parentDir) + if err != nil { + log.Error("Unable to determine if directory is empty and can be deleted: ", err) + response.SendErrorAndExit( + errors.CodeUnableToDetermineIsDirectoryEmpty, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to determine if directory is empty and can be deleted", + errors.FieldAction: "delete", + errors.FieldError: err.Error(), + errors.FieldDirectory: parentDir, + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + + if !isEmpty { + break + } + + err = os.Remove(parentDir) + if err != nil { + log.Error("Unable to delete the empty directory: ", err) + response.SendErrorAndExit( + errors.CodeUnableToDeleteEmptyDirectory, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to delete the empty directory", + errors.FieldAction: "delete", + errors.FieldError: err.Error(), + errors.FieldDirectory: parentDir, + errors.FieldStoreID: store.ID, + errors.FieldStoreName: store.Name, + errors.FieldStorePath: store.Path, + }, + ) + } + + parentDir = filepath.Dir(parentDir) + } + + response.SendOk(responseData) +} diff --git a/request/process.go b/request/process.go index a05acc1..9452d8b 100644 --- a/request/process.go +++ b/request/process.go @@ -73,6 +73,8 @@ func Process() { fetchDecryptedContents(request) case "save": saveEncryptedContents(request) + case "delete": + deleteFile(request) case "echo": response.SendRaw(request.EchoResponse) default: diff --git a/request/save.go b/request/save.go index 55a7ff3..12f4b78 100644 --- a/request/save.go +++ b/request/save.go @@ -26,11 +26,11 @@ func saveEncryptedContents(request *request) { } if request.Contents == "" { - log.Errorf("The provided contents is empty") + log.Errorf("The entry contents is missing") response.SendErrorAndExit( errors.CodeEmptyContents, &map[errors.Field]string{ - errors.FieldMessage: "The provided contents is empty", + errors.FieldMessage: "The entry contents is missing", errors.FieldAction: "save", }, ) @@ -114,11 +114,11 @@ func saveEncryptedContents(request *request) { recipients, err := helpers.DetectGpgRecipients(filePath) if err != nil { - log.Error("Unable to detect gpg recipients for encryption: ", err) + log.Error("Unable to determine recipients for the gpg encryption: ", err) response.SendErrorAndExit( - errors.CodeUnableToDetectGpgRecipients, + errors.CodeUnableToDetermineGpgRecipients, &map[errors.Field]string{ - errors.FieldMessage: "Unable to detect gpg recipients for encryption", + errors.FieldMessage: "Unable to determine recipients for the gpg encryption", errors.FieldAction: "save", errors.FieldError: err.Error(), errors.FieldFile: request.File, diff --git a/response/response.go b/response/response.go index 5825244..8b8c724 100644 --- a/response/response.go +++ b/response/response.go @@ -83,6 +83,15 @@ func MakeSaveResponse() *SaveResponse { return &SaveResponse{} } +// DeleteResponse a response format for the "delete" request +type DeleteResponse struct { +} + +// MakeDeleteResponse initializes an empty delete response +func MakeDeleteResponse() *DeleteResponse { + return &DeleteResponse{} +} + // SendOk sends a success response to the browser extension in the predefined json format func SendOk(data interface{}) { SendRaw(&okResponse{ From fbc6b645a8f7f70fd2a4af956fa9dd1080829e14 Mon Sep 17 00:00:00 2001 From: Maxim Baz Date: Sat, 4 Mar 2023 16:12:26 +0100 Subject: [PATCH 5/5] Fix split by newline on Windows --- helpers/helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/helpers.go b/helpers/helpers.go index d342dd1..9877a1c 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -82,7 +82,7 @@ func DetectGpgRecipients(filePath string) ([]string, error) { for { file, err := ioutil.ReadFile(filepath.Join(dir, ".gpg-id")) if err == nil { - return strings.Split(strings.TrimSpace(string(file)), "\n"), nil + return strings.Split(strings.ReplaceAll(strings.TrimSpace(string(file)), "\r\n", "\n"), "\n"), nil } if !os.IsNotExist(err) {