Skip to content

Commit

Permalink
Fix/encryption and sync (stuartcryan#58)
Browse files Browse the repository at this point in the history
* Fix encryption process and disable sync per default.
  • Loading branch information
blacs30 authored Dec 17, 2020
1 parent 07e4383 commit cb0c15f
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 77 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ workflow/bitwarden-alfred-workflow
workflow/icons
workflow/assets
workflow/README.html
workflow/List Filter Images
workflow/bw_cache_update.sh
65 changes: 33 additions & 32 deletions README.md

Large diffs are not rendered by default.

22 changes: 14 additions & 8 deletions bitwarden.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,6 @@ 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 Down Expand Up @@ -265,6 +257,7 @@ func runGetItem() {
encryptedSecret = value.String()
} else {
log.Print("Error, value for gjson not found.")
isDecryptSecretFromJsonFailed = true
}
}

Expand All @@ -277,6 +270,7 @@ func runGetItem() {
decryptedString, err = otpKey(decryptedString)
if err != nil {
log.Print("Error getting topt key, ", err)
isDecryptSecretFromJsonFailed = true
}
}
receivedItem = decryptedString
Expand Down Expand Up @@ -520,6 +514,12 @@ func runLogin() {
}
searchAlfred(conf.BwKeyword)
fmt.Println("Logged In.")

// reset sync-cache
err = wf.Cache.StoreJSON(CACHE_NAME, nil)
if err != nil {
fmt.Println("Error cleaning cache..")
}
}

// Logout from Bitwarden
Expand All @@ -544,6 +544,12 @@ func runLogout() {
wf.FatalError(err)
}
fmt.Println("Logged Out")

// reset sync-cache
err = wf.Cache.StoreJSON(CACHE_NAME, nil)
if err != nil {
fmt.Println("Error cleaning cache..")
}
}

func runCache() {
Expand Down
8 changes: 4 additions & 4 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,10 +541,10 @@ func runSearch(folderSearch bool, itemId string) {
}
}

// Check the sync cache, if it expired or doesn't exist do a sync.
// Check the sync cache, if it expired.
// don't sync if age is set to 0
// this cache is just a control to automatically trigger the sync, the data itself is stored in the data cache (CACHE_NAME and FOLDER_CACHE_NAME)
if (conf.SyncCacheAge != 0 && wf.Cache.Expired(SYNC_CACHE_NAME, conf.SyncMaxCacheAge)) || !wf.Cache.Exists(SYNC_CACHE_NAME) {
if conf.SyncCacheAge != 0 && wf.Cache.Expired(SYNC_CACHE_NAME, conf.SyncMaxCacheAge) {
if !wf.IsRunning("sync") {
cmd := exec.Command(os.Args[0], "-sync", "-force")
log.Println("Sync cmd: ", cmd)
Expand All @@ -565,7 +565,7 @@ func runSearch(folderSearch bool, itemId string) {
// If the cache has expired, set Rerun (which tells Alfred to re-run the
// workflow), and start the background update process if it isn't already
// running.
if wf.Cache.Expired(CACHE_NAME, conf.MaxCacheAge) || wf.Cache.Expired(FOLDER_CACHE_NAME, conf.MaxCacheAge) {
if conf.CacheAge != 0 && (wf.Cache.Expired(CACHE_NAME, conf.MaxCacheAge) || wf.Cache.Expired(FOLDER_CACHE_NAME, conf.MaxCacheAge)) {
wf.Rerun(0.3)
if !wf.IsRunning("cache") {
var wg sync.WaitGroup
Expand Down Expand Up @@ -651,7 +651,7 @@ func runSearch(folderSearch bool, itemId string) {

if len(items) == 0 && len(folders) == 0 {
addRefreshCacheItem()
wf.WarnEmpty("No Secrets Found", "Try a different query or refresh the cache manually.")
wf.NewItem("No Secrets Found").Subtitle("Try a different query or refresh the cache or sync manually.").Icon(iconWarning).Valid(false)
}

if !folderSearch && itemId == "" {
Expand Down
16 changes: 13 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type config struct {
// BwDataPath default is set in loadBitwardenJSON()
BwDataPath string `envconfig:"BW_DATA_PATH"`
CacheAge int `default:"1440" split_words:"true"`
Debug bool `envconfig:"DEBUG" default:"false"`
Email string
EmptyDetailResults bool `default:"false" split_words:"true"`
IconCacheAge int `default:"43200" split_words:"true"`
Expand Down Expand Up @@ -138,7 +139,7 @@ func loadBitwardenJSON() error {
return err
}
bwDataPath = fmt.Sprintf("%s/Library/Application Support/Bitwarden CLI/data.json", homedir)
log.Println("BW DataPath", bwDataPath)
debugLog(fmt.Sprintf("bwDataPath is: %s", bwDataPath))
}
if err := loadDataFile(bwDataPath); err != nil {
return err
Expand Down Expand Up @@ -178,7 +179,11 @@ func loadConfig() {
conf.OutputFolder = alfred.GetOutputFolder(wf, conf.OutputFolder)

// Set a few cache timeout durations
cacheAgeDuration := time.Duration(conf.CacheAge)
setItemCacheAge := conf.CacheAge
if conf.CacheAge < 30 && conf.CacheAge != 0 {
setItemCacheAge = 30
}
cacheAgeDuration := time.Duration(setItemCacheAge)
conf.MaxCacheAge = cacheAgeDuration * time.Minute

iconCacheAgeDuration := time.Duration(conf.IconCacheAge)
Expand All @@ -187,7 +192,12 @@ func loadConfig() {
autoFetchIconCacheAgeDuration := time.Duration(conf.AutoFetchIconCacheAge)
conf.AutoFetchIconMaxCacheAge = autoFetchIconCacheAgeDuration * time.Minute

syncCacheAgeDuration := time.Duration(conf.SyncCacheAge)
// if SYNC_CACHE_AGE is lower than 30 but not 0 set to 30
setSyncCacheAge := conf.SyncCacheAge
if conf.SyncCacheAge < 30 && conf.SyncCacheAge != 0 {
setSyncCacheAge = 30
}
syncCacheAgeDuration := time.Duration(setSyncCacheAge)
conf.SyncMaxCacheAge = syncCacheAgeDuration * time.Minute

conf.BwauthKeyword = os.Getenv("bwauth_keyword")
Expand Down
63 changes: 45 additions & 18 deletions crypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func Encrypt(message []byte) (string, bool) {
}

func Decrypt() ([]byte, error) {
log.Println("Decrypting data now.")
log.Println("Decrypting data.")
encryptedHex, err := wf.Cache.Load(CACHE_NAME)
if err != nil {
log.Println(err)
Expand Down Expand Up @@ -124,30 +124,47 @@ type CryptoKey struct {

// TODO: split up into functions
func MakeDecryptKeyFromSession(protectedKey string, sessionKey string) (CryptoKey, error) {
// the key which will be returned later, or empty in case of error
ck := CryptoKey{}

debugLog("base64 decode protected key")
pt, err := base64.StdEncoding.DecodeString(protectedKey)
if err != nil {
log.Print("Error decoding protectedKey, ", err)
return ck, fmt.Errorf("error decoding protectedKey, %s", err)
}
// following every step from here:
// https://github.com/attie/bitwarden-decrypt#protected-session-data
debugLog(fmt.Sprintf("protected Key length is: %d", len(pt)))
if len(pt) > 1 {
debugLog(fmt.Sprintf("protected Key encryption type is: %d", int(pt[0])))
}

// check length, return error if they key is probably to short so that we continue using the normal bw cli client
if len(pt) < 51 {
log.Print("protected key length is probably too short, returning with error. length is: ", len(pt))
return ck, fmt.Errorf("protected key length is probably too short")
}
encryptionType := pt[:1]
encryptionTypeInt := int(encryptionType[0])
iv := pt[1:17]
pkmac := pt[17:49]
ct := pt[49:]

// and now here:
// https://github.com/attie/bitwarden-decrypt#derive-source-key-from-protected-session-data
debugLog("base64 decode session key")
ses, err := base64.StdEncoding.DecodeString(sessionKey)
if err != nil {
log.Print("Error decoding sessionKey, ", err)
return ck, fmt.Errorf("error decoding sessionKey, %s", err)
}
debugLog(fmt.Sprintf("Session key length is: %d", len(ses)))
if len(ses) != 64 {
log.Print("session key length is too short, returning with error. length is: ", len(ses))
return ck, fmt.Errorf("session key length is too short")
}
sesec := ses[:32]
sesmac := ses[32:64]

// the key which will be returned later, or empty in case of error
ck := CryptoKey{}

debugLog("comparing session mac with protected key")
mac := hmac.New(sha256.New, sesmac)
_, err = mac.Write(iv)
if err != nil {
Expand All @@ -160,10 +177,12 @@ func MakeDecryptKeyFromSession(protectedKey string, sessionKey string) (CryptoKe
ms := mac.Sum(nil)
if base64.StdEncoding.EncodeToString(ms) != base64.StdEncoding.EncodeToString(pkmac) {
log.Printf("MAC doesn't match %s %s", base64.StdEncoding.EncodeToString(pkmac), base64.StdEncoding.EncodeToString(ms))
return ck, fmt.Errorf("MACs don't match of protectedkey and session key")
}

// and this one now:
// makeing the sourcekey
// https://github.com/attie/bitwarden-decrypt#decrypt
debugLog("making the source key")
cs := CipherString{
encryptedString: "",
encryptionType: encryptionTypeInt,
Expand All @@ -178,33 +197,41 @@ func MakeDecryptKeyFromSession(protectedKey string, sessionKey string) (CryptoKe
MacKey: sesmac,
EncryptionType: 2,
}
sourceKey, err := cs.DecryptKey(ck, 2)
sourceKey, err := cs.DecryptKey(ck, ck.EncryptionType)
if err != nil {
log.Print("Error decrypting key, ", err)
return ck, fmt.Errorf("error decrypting key, %s", err)
}

// making now the intermediate keys:
// making the intermediate keys:
// https://github.com/attie/bitwarden-decrypt#derive-intermediate-keys-from-source-key
debugLog("making intermediate keys")
interKeys, err := MakeIntermediateKeys(sourceKey)
if err != nil {
log.Print("Error making intermediate keys, ", err)
return ck, fmt.Errorf("error making intermediate keys, %s", err)
}

// finally decrypting the real users encryption key:
// https://github.com/attie/bitwarden-decrypt#decrypt-the-users-final-keys
debugLog("decrypting final encryption keys")
ekCs, err := NewCipherString(bwData.EncKey)
if err != nil {
log.Print("Error making cipherstring from encKey, ", err)
return ck, fmt.Errorf("error making cipherstring from encKey, %s", err)
}
userDecryptKey, err := ekCs.DecryptKey(interKeys, ekCs.encryptionType)
if err != nil {
log.Print("Error decrypting key, ", err)
return ck, fmt.Errorf("error decrypting key, %s", err)
}

debugLog(fmt.Sprintf("bwData encKey length is: %d", len(bwData.EncKey)))
debugLog(fmt.Sprintf("bwData encKey encryption type is: %d", ekCs.encryptionType))
debugLog(fmt.Sprintf("User decrypt key length is: %d", len(userDecryptKey.EncKey)))
if len(userDecryptKey.EncKey) != 32 {
log.Print("User decrypt key length is too short, returning with error. length is: ", len(userDecryptKey.EncKey))
return ck, fmt.Errorf("user decrypt key length is too short")
}
tmpKeyEnc := userDecryptKey[:32]
tmpKeyMac := userDecryptKey[32:64]
userKey := CryptoKey{
EncKey: tmpKeyEnc,
MacKey: tmpKeyMac,
EncKey: userDecryptKey.EncKey,
MacKey: userDecryptKey.MacKey,
EncryptionType: 2,
}
return userKey, err
Expand Down
38 changes: 30 additions & 8 deletions crypt_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ func DecryptString(s string, mk CryptoKey) (string, error) {
return string(rv), err
}

func NewCryptoKey(key []byte, encryptionType int) (CryptoKey, error) {
c := CryptoKey{EncryptionType: encryptionType}

switch encryptionType {
case AesCbc256_B64:
c.EncKey = key
case AesCbc256_HmacSha256_B64:
c.EncKey = key[:32]
c.MacKey = key[32:]
default:
return c, fmt.Errorf("invalid encryption type: %d", encryptionType)
}

if len(key) != (len(c.EncKey) + len(c.MacKey)) {
return c, fmt.Errorf("invalid key size: %d", len(key))
}

return c, nil
}

func DecryptValue(s string, mk CryptoKey) ([]byte, error) {
if s == "" {
return []byte(""), nil
Expand All @@ -37,25 +57,26 @@ func DecryptValue(s string, mk CryptoKey) ([]byte, error) {
return rv, err
}

func (cs *CipherString) DecryptKey(key CryptoKey, encryptionType int) ([]byte, error) {
if encryptionType != 2 {
return nil, fmt.Errorf("encryption type not supported %d", encryptionType)
}
func (cs *CipherString) DecryptKey(key CryptoKey, encryptionType int) (CryptoKey, error) {
kb, err := cs.Decrypt(key)
return kb, err
if err != nil {
return CryptoKey{}, err
}
k, err := NewCryptoKey(kb, encryptionType)
return k, err
}

func MakeIntermediateKeys(sourceKey []byte) (CryptoKey, error) {
func MakeIntermediateKeys(sourceKey CryptoKey) (CryptoKey, error) {
tmpKeyEnc := make([]byte, 32)
tmpKeyMac := make([]byte, 32)
var r io.Reader
r = hkdf.Expand(sha256.New, sourceKey, []byte("enc"))
r = hkdf.Expand(sha256.New, sourceKey.EncKey, []byte("enc"))
_, err := r.Read(tmpKeyEnc)
if err != nil {
return CryptoKey{}, err
}

r = hkdf.Expand(sha256.New, sourceKey, []byte("mac"))
r = hkdf.Expand(sha256.New, sourceKey.EncKey, []byte("mac"))
_, err = r.Read(tmpKeyMac)
if err != nil {
return CryptoKey{}, err
Expand Down Expand Up @@ -84,6 +105,7 @@ func NewCipherString(encryptedString string) (*CipherString, error) {
return nil, errors.New("invalid key header")
}

debugLog(fmt.Sprintf("cs.encryptionType %d", cs.encryptionType))
switch cs.encryptionType {
case AesCbc256_B64:
if len(encPieces) != 2 {
Expand Down
2 changes: 1 addition & 1 deletion items.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ func addNewModifierItem(item *aw.Item, modifier modifierActionRelation) {

func addRefreshCacheItem() {
wf.NewItem("Refresh Bitwardens Secret Cache").
Subtitle("Fill the cache with cleaned Bitwarden secrets (the real secrets, are not kept in the cached)").
Subtitle("Fill the cache with cleaned Bitwarden secrets (the real secrets are not kept in the cached)").
Valid(true).
UID("cache").
Icon(ReloadIcon()).
Expand Down
12 changes: 11 additions & 1 deletion utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ func checkReturn(status cmd.Status, message string) ([]string, error) {
log.Println("[DEBUG] Stderr: => ", line)
}
}
return []string{}, fmt.Errorf("Unexpected error. Exit code %d.", exitCode)
errMessage := ""
for _, line := range status.Stderr {
errMessage += fmt.Sprintf(" %s", line)
}
return []string{}, fmt.Errorf("Unexpected error. Exit code %d. Has the session key changed?\n[ERROR] %s", exitCode, errMessage)
}
}

Expand Down Expand Up @@ -148,3 +152,9 @@ func clearCache() error {
}
return nil
}

func debugLog(message string) {
if conf.Debug {
log.Print("[DEBUG] ", message)
}
}
6 changes: 4 additions & 2 deletions workflow/info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,9 @@ Caching of the secret/item names (not the secret values itself) are cached so th
<key>BW_EXEC</key>
<string>bw</string>
<key>CACHE_AGE</key>
<string>1440</string>
<string>0</string>
<key>DEBUG</key>
<string>false</string>
<key>EMAIL</key>
<string></string>
<key>EMPTY_DETAIL_RESULTS</key>
Expand Down Expand Up @@ -1341,7 +1343,7 @@ Caching of the secret/item names (not the secret values itself) are cached so th
<key>SERVER_URL</key>
<string>https://bitwarden.com</string>
<key>SYNC_CACHE_AGE</key>
<string>1440</string>
<string>0</string>
<key>TITLE_WITH_USER</key>
<string>true</string>
<key>bw_keyword</key>
Expand Down

0 comments on commit cb0c15f

Please sign in to comment.