Implement tree/save/delete requests (#99)
This commit is contained in:
120
PROTOCOL.md
120
PROTOCOL.md
@@ -41,23 +41,31 @@ 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 | message, error |
|
| 10 | Unable to parse browser request length | message, error |
|
||||||
| 11 | Unable to parse browser request | message, error |
|
| 11 | Unable to parse browser request | message, error |
|
||||||
| 12 | Invalid request action | message, action |
|
| 12 | Invalid request action | message, action |
|
||||||
| 13 | Inaccessible user-configured password store | message, action, error, storeId, storePath, storeName |
|
| 13 | Inaccessible user-configured password store | message, action, error, storeId, storePath, storeName |
|
||||||
| 14 | Inaccessible default password store | message, action, error, storePath |
|
| 14 | Inaccessible default password store | message, action, error, storePath |
|
||||||
| 15 | Unable to determine the location of the default password store | message, action, 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 | message, action, error, storeId, storePath, storeName |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 20 | Invalid password store ID | message, action, storeId |
|
||||||
| 21 | Invalid gpg path | message, action, error, gpgPath |
|
| 21 | Invalid gpg path | message, action, error, gpgPath |
|
||||||
| 22 | Unable to detect the location of the gpg binary | message, action, error |
|
| 22 | Unable to detect the location of the gpg binary | message, action, error |
|
||||||
| 23 | Invalid password file extension | message, action, file |
|
| 23 | Invalid password file extension | message, action, file |
|
||||||
| 24 | Unable to decrypt the password file | message, action, error, storeId, storePath, storeName, 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 |
|
||||||
|
| 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
|
## Settings
|
||||||
|
|
||||||
@@ -157,6 +165,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": <settings object>,
|
||||||
|
"action": "tree"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"version": <int>,
|
||||||
|
"data": {
|
||||||
|
"directories": {
|
||||||
|
"storeN": ["<storeNPath/directory1>", "<...>"],
|
||||||
|
"storeN+1": ["<storeN+1Path/directory1>", "<...>"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Fetch
|
### Fetch
|
||||||
|
|
||||||
Get the decrypted contents of a specific file.
|
Get the decrypted contents of a specific file.
|
||||||
@@ -184,6 +221,55 @@ Get the decrypted contents of a specific file.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Save
|
||||||
|
|
||||||
|
Encrypt the given contents and save to a specific file.
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"settings": <settings object>,
|
||||||
|
"action": "save",
|
||||||
|
"storeId": "<storeId>",
|
||||||
|
"file": "relative/path/to/file.gpg",
|
||||||
|
"contents": "<contents to encrypt and save>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"version": <int>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete
|
||||||
|
|
||||||
|
Delete a specific file and empty parent directories caused by the deletion, if any.
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"settings": <settings object>,
|
||||||
|
"action": "delete",
|
||||||
|
"storeId": "<storeId>",
|
||||||
|
"file": "relative/path/to/file.gpg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"version": <int>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Echo
|
### Echo
|
||||||
|
|
||||||
Send the `echoResponse` in the request as a response.
|
Send the `echoResponse` in the request as a response.
|
||||||
|
@@ -10,21 +10,29 @@ type Code int
|
|||||||
// Error codes that are sent to the browser extension and used as exit codes in the app.
|
// 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.
|
// DO NOT MODIFY THE VALUES, always append new error codes to the bottom.
|
||||||
const (
|
const (
|
||||||
CodeParseRequestLength Code = 10
|
CodeParseRequestLength Code = 10
|
||||||
CodeParseRequest Code = 11
|
CodeParseRequest Code = 11
|
||||||
CodeInvalidRequestAction Code = 12
|
CodeInvalidRequestAction Code = 12
|
||||||
CodeInaccessiblePasswordStore Code = 13
|
CodeInaccessiblePasswordStore Code = 13
|
||||||
CodeInaccessibleDefaultPasswordStore Code = 14
|
CodeInaccessibleDefaultPasswordStore Code = 14
|
||||||
CodeUnknownDefaultPasswordStoreLocation Code = 15
|
CodeUnknownDefaultPasswordStoreLocation Code = 15
|
||||||
CodeUnreadablePasswordStoreDefaultSettings Code = 16
|
CodeUnreadablePasswordStoreDefaultSettings Code = 16
|
||||||
CodeUnreadableDefaultPasswordStoreDefaultSettings Code = 17
|
CodeUnreadableDefaultPasswordStoreDefaultSettings Code = 17
|
||||||
CodeUnableToListFilesInPasswordStore Code = 18
|
CodeUnableToListFilesInPasswordStore Code = 18
|
||||||
CodeUnableToDetermineRelativeFilePathInPasswordStore Code = 19
|
CodeUnableToDetermineRelativeFilePathInPasswordStore Code = 19
|
||||||
CodeInvalidPasswordStore Code = 20
|
CodeInvalidPasswordStore Code = 20
|
||||||
CodeInvalidGpgPath Code = 21
|
CodeInvalidGpgPath Code = 21
|
||||||
CodeUnableToDetectGpgPath Code = 22
|
CodeUnableToDetectGpgPath Code = 22
|
||||||
CodeInvalidPasswordFileExtension Code = 23
|
CodeInvalidPasswordFileExtension Code = 23
|
||||||
CodeUnableToDecryptPasswordFile Code = 24
|
CodeUnableToDecryptPasswordFile Code = 24
|
||||||
|
CodeUnableToListDirectoriesInPasswordStore Code = 25
|
||||||
|
CodeUnableToDetermineRelativeDirectoryPathInPasswordStore Code = 26
|
||||||
|
CodeEmptyContents Code = 27
|
||||||
|
CodeUnableToDetermineGpgRecipients Code = 28
|
||||||
|
CodeUnableToEncryptPasswordFile Code = 29
|
||||||
|
CodeUnableToDeletePasswordFile Code = 30
|
||||||
|
CodeUnableToDetermineIsDirectoryEmpty Code = 31
|
||||||
|
CodeUnableToDeleteEmptyDirectory Code = 32
|
||||||
)
|
)
|
||||||
|
|
||||||
// Field extra field in the error response params
|
// Field extra field in the error response params
|
||||||
@@ -40,6 +48,7 @@ const (
|
|||||||
FieldStoreName Field = "storeName"
|
FieldStoreName Field = "storeName"
|
||||||
FieldStorePath Field = "storePath"
|
FieldStorePath Field = "storePath"
|
||||||
FieldFile Field = "file"
|
FieldFile Field = "file"
|
||||||
|
FieldDirectory Field = "directory"
|
||||||
FieldGpgPath Field = "gpgPath"
|
FieldGpgPath Field = "gpgPath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
114
helpers/helpers.go
Normal file
114
helpers/helpers.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.ReplaceAll(strings.TrimSpace(string(file)), "\r\n", "\n"), "\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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/browserpass/browserpass-native/errors"
|
"github.com/browserpass/browserpass-native/errors"
|
||||||
|
"github.com/browserpass/browserpass-native/helpers"
|
||||||
"github.com/browserpass/browserpass-native/response"
|
"github.com/browserpass/browserpass-native/response"
|
||||||
log "github.com/sirupsen/logrus"
|
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
|
// User configured gpgPath in the browser, check if it is a valid binary to use
|
||||||
if request.Settings.GpgPath != "" {
|
if request.Settings.GpgPath != "" {
|
||||||
err := validateGpgBinary(request.Settings.GpgPath)
|
err := helpers.ValidateGpgBinary(request.Settings.GpgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
"The provided gpg binary path '%v' is invalid: %+v",
|
"The provided gpg binary path '%v' is invalid: %+v",
|
||||||
|
132
request/delete.go
Normal file
132
request/delete.go
Normal file
@@ -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)
|
||||||
|
}
|
@@ -1,14 +1,11 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/browserpass/browserpass-native/errors"
|
"github.com/browserpass/browserpass-native/errors"
|
||||||
|
"github.com/browserpass/browserpass-native/helpers"
|
||||||
"github.com/browserpass/browserpass-native/response"
|
"github.com/browserpass/browserpass-native/response"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -71,7 +68,7 @@ func fetchDecryptedContents(request *request) {
|
|||||||
} else {
|
} else {
|
||||||
gpgPath = store.Settings.GpgPath
|
gpgPath = store.Settings.GpgPath
|
||||||
}
|
}
|
||||||
err = validateGpgBinary(gpgPath)
|
err = helpers.ValidateGpgBinary(gpgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
"The provided gpg binary path '%v' is invalid: %+v",
|
"The provided gpg binary path '%v' is invalid: %+v",
|
||||||
@@ -88,7 +85,7 @@ func fetchDecryptedContents(request *request) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gpgPath, err = detectGpgBinary()
|
gpgPath, err = helpers.DetectGpgBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to detect the location of the gpg binary: ", err)
|
log.Error("Unable to detect the location of the gpg binary: ", err)
|
||||||
response.SendErrorAndExit(
|
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 {
|
if err != nil {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
"Unable to decrypt the password file '%v' in the password store '%+v': %+v",
|
"Unable to decrypt the password file '%v' in the password store '%+v': %+v",
|
||||||
@@ -124,46 +121,3 @@ func fetchDecryptedContents(request *request) {
|
|||||||
|
|
||||||
response.SendOk(responseData)
|
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
|
|
||||||
}
|
|
||||||
|
@@ -31,6 +31,7 @@ type request struct {
|
|||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Settings settings `json:"settings"`
|
Settings settings `json:"settings"`
|
||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
|
Contents string `json:"contents"`
|
||||||
StoreID string `json:"storeId"`
|
StoreID string `json:"storeId"`
|
||||||
EchoResponse interface{} `json:"echoResponse"`
|
EchoResponse interface{} `json:"echoResponse"`
|
||||||
}
|
}
|
||||||
@@ -66,8 +67,14 @@ func Process() {
|
|||||||
configure(request)
|
configure(request)
|
||||||
case "list":
|
case "list":
|
||||||
listFiles(request)
|
listFiles(request)
|
||||||
|
case "tree":
|
||||||
|
listDirectories(request)
|
||||||
case "fetch":
|
case "fetch":
|
||||||
fetchDecryptedContents(request)
|
fetchDecryptedContents(request)
|
||||||
|
case "save":
|
||||||
|
saveEncryptedContents(request)
|
||||||
|
case "delete":
|
||||||
|
deleteFile(request)
|
||||||
case "echo":
|
case "echo":
|
||||||
response.SendRaw(request.EchoResponse)
|
response.SendRaw(request.EchoResponse)
|
||||||
default:
|
default:
|
||||||
|
153
request/save.go
Normal file
153
request/save.go
Normal file
@@ -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 entry contents is missing")
|
||||||
|
response.SendErrorAndExit(
|
||||||
|
errors.CodeEmptyContents,
|
||||||
|
&map[errors.Field]string{
|
||||||
|
errors.FieldMessage: "The entry contents is missing",
|
||||||
|
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 determine recipients for the gpg encryption: ", err)
|
||||||
|
response.SendErrorAndExit(
|
||||||
|
errors.CodeUnableToDetermineGpgRecipients,
|
||||||
|
&map[errors.Field]string{
|
||||||
|
errors.FieldMessage: "Unable to determine recipients for the gpg 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)
|
||||||
|
}
|
112
request/tree.go
Normal file
112
request/tree.go
Normal file
@@ -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)
|
||||||
|
}
|
@@ -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
|
// FetchResponse a response format for the "fetch" request
|
||||||
type FetchResponse struct {
|
type FetchResponse struct {
|
||||||
Contents string `json:"contents"`
|
Contents string `json:"contents"`
|
||||||
@@ -62,6 +74,24 @@ func MakeFetchResponse() *FetchResponse {
|
|||||||
return &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{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// SendOk sends a success response to the browser extension in the predefined json format
|
||||||
func SendOk(data interface{}) {
|
func SendOk(data interface{}) {
|
||||||
SendRaw(&okResponse{
|
SendRaw(&okResponse{
|
||||||
|
Reference in New Issue
Block a user