Skip to content

Commit

Permalink
Implement tree/save/delete requests (browserpass#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
maximbaz authored Mar 4, 2023
1 parent 10c6b4e commit b72a73d
Show file tree
Hide file tree
Showing 10 changed files with 681 additions and 83 deletions.
120 changes: 103 additions & 17 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,31 @@ 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 |
| 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

Expand Down Expand Up @@ -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

Get the decrypted contents of a specific file.
Expand Down Expand Up @@ -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

Send the `echoResponse` in the request as a response.
Expand Down
39 changes: 24 additions & 15 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +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 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
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
Expand All @@ -40,6 +48,7 @@ const (
FieldStoreName Field = "storeName"
FieldStorePath Field = "storePath"
FieldFile Field = "file"
FieldDirectory Field = "directory"
FieldGpgPath Field = "gpgPath"
)

Expand Down
114 changes: 114 additions & 0 deletions helpers/helpers.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion request/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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",
Expand Down
Loading

0 comments on commit b72a73d

Please sign in to comment.