Skip to content

Commit

Permalink
Add new feature to decryption secrets from file. (stuartcryan#52)
Browse files Browse the repository at this point in the history
* Add custom path for data.json and move the load function into main/config.go

* update cli help text

* Add new feature to decrypt secrets from data.json file.

* Increment version to 2.2.0
  • Loading branch information
blacs30 authored Oct 30, 2020
1 parent 7154505 commit 07e4383
Show file tree
Hide file tree
Showing 11 changed files with 665 additions and 203 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ You can change the search-/filtermode yourself easily. This gif shows the 3 step
| 2FA_ENABLED | enables or disables 2FA for login (can be set via .bwconfig ) | true |
| 2FA_NODE | sets the mode for the 2FA (can be set via .bwconfig ), 0 app, 1, email (not tested), 2 duo (not tested), 3 yubikey (not tested), 4 U2F (not tested) | 0 |
| BW_EXEC | defines the binary/executable for the Bitwarden CLI command | bw |
| BW_DATA_PATH | sets the path to the Bitwarden Cli data.json | "~/Library/Application Support/Bitwarden CLI/data.json"" |
| bw_keyword | defines the keyword which opens the Bitwarden Alfred Workflow | .bw |
| bwf_keyword | defines the keyword which opens the folder search of the Bitwarden Alfred Workflow | .bwf |
| bwauth_keyword | defines the keyword which opens the Bitwarden authentications of the Alfred Workflow | .bwauth |
Expand Down
49 changes: 5 additions & 44 deletions alfred/config.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package alfred

import (
"encoding/json"
"fmt"
aw "github.com/deanishe/awgo"
"github.com/jychri/tilde"
"io/ioutil"
"log"
"os"
"strings"
Expand All @@ -27,24 +25,16 @@ func GetOutputFolder(wf *aw.Workflow, folder string) string {
return folder
}

func GetEmail(wf *aw.Workflow, email string) string {
if email == "" {
var bwData BwData
succ, err := OpenBitwardenData(&bwData)
func GetEmail(wf *aw.Workflow, configEmail string, bwEmail string) string {
if configEmail == "" {
err := SetEmail(wf, bwEmail)
if err != nil {
log.Println(err)
return ""
}
if succ {
err := SetEmail(wf, bwData.UserEmail)
if err != nil {
log.Println(err)
return ""
}
email = bwData.UserEmail
}
configEmail = bwEmail
}
return email
return configEmail
}

//Set keys
Expand All @@ -63,32 +53,3 @@ func SetSfa(wf *aw.Workflow, enabled string) error {
func SetSfaMode(wf *aw.Workflow, id string) error {
return wf.Config.Set("2FA_MODE", id, true).Do()
}

func OpenBitwardenData(bwData interface{}) (bool, error) {
homedir, err := os.UserHomeDir()
if err != nil {
return false, err
}
bwDataPath := fmt.Sprintf("%s/Library/Application Support/Bitwarden CLI/data.json", homedir)
log.Println("BW DataPath", bwDataPath)
if _, err := os.Stat(bwDataPath); err != nil {
log.Println("Couldn't find the Bitwarden data.json ", err)
return false, err
}
data, err := ioutil.ReadFile(bwDataPath)
if err != nil {
return false, err
}
if err := json.Unmarshal(data, &bwData); err != nil {
log.Printf("Couldn't load the items cache, error: %s", err)
return false, err
}
log.Println("Got existing Bitwarden CLI data")
return true, nil
}

type BwData struct {
InstalledVersion string `json:"installedVersion"`
UserEmail string `json:"userEmail"`
Unused map[string]interface{} `json:"-"`
}
200 changes: 129 additions & 71 deletions bitwarden.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,22 @@ import (
"github.com/blacs30/bitwarden-alfred-workflow/alfred"
aw "github.com/deanishe/awgo"
"github.com/oliveagle/jsonpath"
"github.com/tidwall/gjson"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"time"
)

var (
const (
NOT_LOGGED_IN_MSG = "Not logged in. Need to login first."
NOT_UNLOCKED_MSG = "Not unlocked. Need to unlock first."
)

// Scan for projects and cache results
func runSync(force bool, last bool) {
log.Println("Clearing items cache.")
err := wf.Cache.StoreJSON(CACHE_NAME, nil)
if err != nil {
log.Println(err)
}
err = wf.Cache.StoreJSON(FOLDER_CACHE_NAME, nil)
if err != nil {
log.Println(err)
}
err = wf.Cache.StoreJSON(AUTO_FETCH_CACHE, nil)
if err != nil {
log.Println(err)
}

wf.Configure(aw.TextErrors(true))
email := conf.Email
Expand All @@ -55,9 +44,10 @@ func runSync(force bool, last bool) {
return
}

log.Println("Background?", opts.Background)
if opts.Background {
log.Println("Runnung sync in background")
if !wf.IsRunning("sync") {
log.Printf("Starting sync job.")
cmd := exec.Command(os.Args[0], "-sync", "-force")
log.Println("Sync cmd: ", cmd)
if err := wf.RunInBackground("sync", cmd); err != nil {
Expand Down Expand Up @@ -88,6 +78,14 @@ func runSync(force bool, last bool) {
args = fmt.Sprintf("%s sync --session %s", conf.BwExec, token)
}

// Clear the cache only if not getting --last sync date
if !last {
err := clearCache()
if err != nil {
log.Print("Error while deleting Caches ", err)
}
}

result, err := runCmd(args, message)
if err != nil {
wf.FatalError(err)
Expand All @@ -101,14 +99,21 @@ func runSync(force bool, last bool) {
}
output = fmt.Sprintf("Last sync date:\n%s", formattedTime)
}
// Printing the "Last sync date" or the message "synced"
fmt.Println(output)

// run these steps only if not getting just the last sync date
if !last {
getItems()
}
fmt.Println(output)
err = wf.Cache.Store(SYNC_CACHE_NAME, []byte(string("sync-cache")))
if err != nil {
log.Println(err)

// Writing the sync-cache
err = wf.Cache.Store(SYNC_CACHE_NAME, []byte(string("sync-cache")))
if err != nil {
log.Println(err)
}

// Creating the items cache
runCache()
}
return
}
Expand Down Expand Up @@ -153,6 +158,7 @@ func getItems() {
popuplateCacheFolders(folders)
}

// runGetItems uses the Bitwarden CLI to get all items and returns them to the calling function
func runGetItems(token string) []Item {
message := "Failed to get Bitwarden items."
args := fmt.Sprintf("%s list items --pretty --session %s", conf.BwExec, token)
Expand Down Expand Up @@ -185,83 +191,144 @@ func runGetItems(token string) []Item {
return items
}

// runGetItem gets a particular item from Bitwarden.
// It first tries to read it directly from the data.json
// if that fails it will use the Bitwarden CLI
func runGetItem() {
wf.Configure(aw.TextErrors(true))

// checking if -id was sent together with -getitem
if opts.Id == "" {
wf.Fatal("No id sent.")
return
}
id := opts.Id
log.Println("Id is: ", id)

// checking if -jsonpath was sent together with -getitem and -id
jsonPath := ""
if opts.Query != "" {
jsonPath = opts.Query
log.Println("Query is: ", jsonPath)
}
totp := opts.Totp
attachment := opts.Attachment
// first check if Bitwarden is logged in or locked
loginErr, unlockErr := BitwardenAuthChecks()
if loginErr != nil {

// this assumes that the data.json was read successfully at loadBitwardenJSON()
if bwData.UserId == "" {
searchAlfred(fmt.Sprintf("%s login", conf.BwauthKeyword))
wf.Fatal(NOT_LOGGED_IN_MSG)
return
}
if unlockErr != nil {

// this assumes that the data.json was read successfully at loadBitwardenJSON()
if bwData.UserId != "" && bwData.ProtectedKey == "" {
searchAlfred(fmt.Sprintf("%s unlock", conf.BwauthKeyword))
wf.Fatal(NOT_UNLOCKED_MSG)
return
}

// get a token
// get the token from keychain
wf.Configure(aw.TextErrors(true))
token, err := alfred.GetToken(wf)
if err != nil {
wf.Fatal("Get Token error")
return
}

message := "Failed to get Bitwarden item."
args := fmt.Sprintf("%s get item %s --pretty --session %s", conf.BwExec, id, token)
if totp {
args = fmt.Sprintf("%s get totp %s --session %s", conf.BwExec, id, token)
} else if attachment != "" {
args = fmt.Sprintf("%s get attachment %s --itemid %s --output %s --session %s --raw", conf.BwExec, attachment, id, conf.OutputFolder, token)
}
log.Println("Read item ", id)
receivedItem := ""
isDecryptSecretFromJsonFailed := false

result, err := runCmd(args, message)
if err != nil {
log.Printf("Error is:\n%s", err)
wf.FatalError(err)
return
}
// block here and return if no items (secrets) are found
if len(result) <= 0 {
log.Println("No items found.")
return
}
// handle attachments later, via Bitwarden CLI
// this decrypts the secrets in the data.json
if bwData.UserId != "" && (attachment == "") {
log.Printf("Getting item for id %s", id)
sourceKey, err := MakeDecryptKeyFromSession(bwData.ProtectedKey, token)
if err != nil {
log.Printf("Error making source key is:\n%s", err)
isDecryptSecretFromJsonFailed = true
}

receivedItem := ""
if jsonPath != "" {
// jsonpath operation to get only required part of the item
singleString := strings.Join(result, " ")
var item interface{}
err = json.Unmarshal([]byte(singleString), &item)
encryptedSecret := ""
if bwData.path != "" {
data, err := ioutil.ReadFile(bwData.path)
if err != nil {
log.Print("Error reading file ", bwData.path)
isDecryptSecretFromJsonFailed = true
}
// replace starting bracket with dot as gsub uses a dot for the first group in an array
jsonPath = strings.Replace(jsonPath, "[", ".", -1)
jsonPath = strings.Replace(jsonPath, "]", "", -1)
if totp {
jsonPath = "login.totp"
}
value := gjson.Get(string(data), fmt.Sprintf("ciphers_%s.%s.%s", bwData.UserId, id, jsonPath))
if value.Exists() {
encryptedSecret = value.String()
} else {
log.Print("Error, value for gjson not found.")
}
}

decryptedString, err := DecryptString(encryptedSecret, sourceKey)
if err != nil {
log.Println(err)
log.Print(err)
isDecryptSecretFromJsonFailed = true
}
if totp {
decryptedString, err = otpKey(decryptedString)
if err != nil {
log.Print("Error getting topt key, ", err)
}
}
receivedItem = decryptedString
}
if bwData.UserId == "" || isDecryptSecretFromJsonFailed || attachment != "" {
// Run the Bitwarden CLI to get the secret
// Use it also for getting attachments
if attachment != "" {
log.Printf("Getting attachment %s for id %s", attachment, id)
}
res, err := jsonpath.JsonPathLookup(item, fmt.Sprintf("$.%s", jsonPath))

message := "Failed to get Bitwarden item."
args := fmt.Sprintf("%s get item %s --pretty --session %s", conf.BwExec, id, token)
if totp {
args = fmt.Sprintf("%s get totp %s --session %s", conf.BwExec, id, token)
} else if attachment != "" {
args = fmt.Sprintf("%s get attachment %s --itemid %s --output %s --session %s --raw", conf.BwExec, attachment, id, conf.OutputFolder, token)
}

result, err := runCmd(args, message)
if err != nil {
log.Println(err)
log.Printf("Error is:\n%s", err)
wf.FatalError(err)
return
}
receivedItem = fmt.Sprintf("%v", res)
if wf.Debug() {
log.Println(fmt.Sprintf("Received key is: %s*", receivedItem[0:2]))
// block here and return if no items (secrets) are found
if len(result) <= 0 {
log.Println("No items found.")
return
}

receivedItem = ""
if jsonPath != "" {
// jsonpath operation to get only required part of the item
singleString := strings.Join(result, " ")
var item interface{}
err = json.Unmarshal([]byte(singleString), &item)
if err != nil {
log.Println(err)
}
res, err := jsonpath.JsonPathLookup(item, fmt.Sprintf("$.%s", jsonPath))
if err != nil {
log.Println(err)
return
}
receivedItem = fmt.Sprintf("%v", res)
if wf.Debug() {
log.Println(fmt.Sprintf("Received key is: %s*", receivedItem[0:2]))
}
} else {
receivedItem = strings.Join(result, " ")
}
} else {
receivedItem = strings.Join(result, " ")
}
fmt.Print(receivedItem)
}
Expand Down Expand Up @@ -480,18 +547,9 @@ func runLogout() {
}

func runCache() {
log.Println("Clearing items cache.")
err := wf.Cache.StoreJSON(CACHE_NAME, nil)
if err != nil {
log.Println(err)
}
err = wf.Cache.StoreJSON(FOLDER_CACHE_NAME, nil)
err := clearCache()
if err != nil {
log.Println(err)
}
err = wf.Cache.StoreJSON(AUTO_FETCH_CACHE, nil)
if err != nil {
log.Println(err)
log.Print("Error while deleting Caches ", err)
}

wf.Configure(aw.TextErrors(true))
Expand Down
Loading

0 comments on commit 07e4383

Please sign in to comment.