Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add passwords #68

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
ISC License

Copyright (c) 2018, Maxim Baz & Steve Gilberd
Copyright (c) 2018-2019, Maxim Baz & Steve Gilberd

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
Expand Down
2 changes: 2 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const (
CodeUnableToDetectGpgPath Code = 22
CodeInvalidPasswordFileExtension Code = 23
CodeUnableToDecryptPasswordFile Code = 24
CodeUnableToEncryptPasswordFile Code = 25
CodeUnableToGitCommit Code = 26
)

// Field extra field in the error response params
Expand Down
32 changes: 32 additions & 0 deletions request/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package request

import (
"os"
"path/filepath"

"github.com/browserpass/browserpass-native/response"
)

type existsResponse struct {
Exists bool `json:"exists"`
}

func checkFile(request *request) {
exists := false

for _, store := range request.Settings.Stores {
normalizedStorePath, err := normalizePasswordStorePath(store.Path)
if err != nil {
continue // TODO: should respond with error
}

absoluteFilePath := filepath.Join(normalizedStorePath, request.File)
_, err = os.Stat(absoluteFilePath)

if err == nil {
exists = true
}
}

response.SendOk(&existsResponse{exists})
}
81 changes: 81 additions & 0 deletions request/common.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package request

import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strings"

bpErrors "github.com/browserpass/browserpass-native/errors"
"github.com/browserpass/browserpass-native/response"
log "github.com/sirupsen/logrus"
)

func normalizePasswordStorePath(storePath string) (string, error) {
Expand Down Expand Up @@ -32,3 +38,78 @@ func normalizePasswordStorePath(storePath string) (string, error) {
}
return storePath, nil
}

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 getGpgPath(request *request) (string, error) {
var gpgPath string
var err error
if request.Settings.GpgPath != "" {
gpgPath = request.Settings.GpgPath
err = validateGpgBinary(gpgPath)
if err != nil {
log.Errorf(
"The provided gpg binary path '%v' is invalid: %+v",
gpgPath, err,
)
response.SendErrorAndExit(
bpErrors.CodeInvalidGpgPath,
&map[bpErrors.Field]string{
bpErrors.FieldMessage: "The provided gpg binary path is invalid",
bpErrors.FieldAction: request.Action,
bpErrors.FieldError: err.Error(),
bpErrors.FieldGpgPath: gpgPath,
},
)
return "", err
}
} else {
gpgPath, err = detectGpgBinary()
if err != nil {
log.Error("Unable to detect the location of the gpg binary: ", err)
response.SendErrorAndExit(
bpErrors.CodeUnableToDetectGpgPath,
&map[bpErrors.Field]string{
bpErrors.FieldMessage: "Unable to detect the location of the gpg binary",
bpErrors.FieldAction: request.Action,
bpErrors.FieldError: err.Error(),
},
)
return "", err
}
}

return gpgPath, nil
}

func readGPGIDs(storePath string) []string {
IDs := make([]string, 0)
IDFilePath := filepath.Join(storePath, ".gpg-id")
IDFile, err := os.Open(IDFilePath)
if err != nil {
return IDs
}
defer IDFile.Close()

scanner := bufio.NewScanner(IDFile)
for scanner.Scan() {
IDs = append(IDs, scanner.Text())
}

return IDs
}
127 changes: 127 additions & 0 deletions request/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package request

import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/browserpass/browserpass-native/errors"
"github.com/browserpass/browserpass-native/response"
log "github.com/sirupsen/logrus"
)

func createFile(request *request) {
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: "create",
errors.FieldStoreID: request.StoreID,
},
)
}
storePath, err := normalizePasswordStorePath(store.Path)
if err != nil {
return // TODO
}

gpgPath, err := getGpgPath(request)
if err != nil {
return // TODO
}

credentials := request.Credentials
fileString := fmt.Sprintf("%s\nlogin: %s\n", credentials.Password, credentials.Login)

if credentials.Email != "" {
fileString = fileString + fmt.Sprintf("email: %s\n", credentials.Email)
}

err = encryptContent(storePath, request.File, fileString, gpgPath)
if err != nil {
response.SendErrorAndExit(
errors.CodeUnableToEncryptPasswordFile,
&map[errors.Field]string{
errors.FieldMessage: "Could not encrypt new password file",
errors.FieldAction: "create",
errors.FieldError: err.Error(),
errors.FieldStoreID: request.StoreID,
},
)
}

err = gitAddAndCommit(storePath, request.File)
if err != nil {
response.SendErrorAndExit(
errors.CodeUnableToGitCommit,
&map[errors.Field]string{
errors.FieldMessage: "Could not commit file to git repository",
errors.FieldAction: "create",
errors.FieldError: err.Error(),
errors.FieldStoreID: request.StoreID,
errors.FieldFile: request.File,
},
)
}

response.SendOk(nil)
}

func encryptContent(storePath, file, content, gpgPath string) error {
IDs := readGPGIDs(storePath)
passwordFilePath := filepath.Join(storePath, file)
err := os.MkdirAll(filepath.Dir(passwordFilePath), os.ModePerm)
if err != nil {
return err
}

var stderr bytes.Buffer
gpgOptions := []string{"--encrypt", "-o", passwordFilePath, "--quiet"}

for _, id := range IDs {
gpgOptions = append(gpgOptions, "-r")
gpgOptions = append(gpgOptions, id)
}

contentReader := strings.NewReader(content)
cmd := exec.Command(gpgPath, gpgOptions...)
cmd.Stdin = contentReader
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
return fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String())
}

return nil
}

func gitAddAndCommit(storePath, file string) error {
gitBaseOptions := []string{"-C", storePath}

var stderr bytes.Buffer
cmd := exec.Command("git", append(gitBaseOptions, []string{"add", file}...)...)
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
return fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String())
}

commitMessage := fmt.Sprintf("Add password %s from browserpass", file)
cmd = exec.Command("git", append(gitBaseOptions, []string{"commit", "-m", commitMessage}...)...)
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
return fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String())
}

return nil
}
17 changes: 0 additions & 17 deletions request/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,23 +125,6 @@ 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()
}
Expand Down
11 changes: 11 additions & 0 deletions request/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,17 @@ type settings struct {
Stores map[string]store `json:"stores"`
}

type credentials struct {
Login string `json:"login"`
Password string `json:"password"`
Email string `json:"email"`
}

type request struct {
Action string `json:"action"`
Settings settings `json:"settings"`
File string `json:"file"`
Credentials credentials `json:"credentials"`
StoreID string `json:"storeId"`
EchoResponse interface{} `json:"echoResponse"`
}
Expand Down Expand Up @@ -68,6 +75,10 @@ func Process() {
listFiles(request)
case "fetch":
fetchDecryptedContents(request)
case "exists":
checkFile(request)
case "create":
createFile(request)
case "echo":
response.SendRaw(request.EchoResponse)
default:
Expand Down