Skip to content

Commit

Permalink
Merge pull request #340 from DopplerHQ/pbkdf2
Browse files Browse the repository at this point in the history
Increase default number of pbkdf2 rounds to 500,000
  • Loading branch information
Piccirello authored Nov 3, 2022
2 parents 788eb37 + 4e030ba commit a3c672c
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 57 deletions.
8 changes: 1 addition & 7 deletions pkg/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,13 +515,7 @@ func readFallbackFile(path string, legacyPath string, passphrase string, silent
}

utils.LogDebug("Decrypting fallback file")
// default to hex for backwards compatibility b/c we didn't always include a prefix
// TODO remove support for optional prefix when releasing CLI v4 (DPLR-435)
encoding := "hex"
if strings.HasPrefix(string(response), crypto.Base64EncodingPrefix) {
encoding = "base64"
}
decryptedSecrets, err := crypto.Decrypt(passphrase, response, encoding)
decryptedSecrets, err := crypto.Decrypt(passphrase, response)
if err != nil {
var msg []string
msg = append(msg, "")
Expand Down
9 changes: 1 addition & 8 deletions pkg/controllers/fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/DopplerHQ/cli/pkg/crypto"
"github.com/DopplerHQ/cli/pkg/models"
Expand Down Expand Up @@ -114,13 +113,7 @@ func SecretsCacheFile(path string, passphrase string) (map[string]string, Error)
}

utils.LogDebug("Decrypting cache file")
// default to hex for backwards compatibility b/c we didn't always include a prefix
// TODO remove support for optional prefix when releasing CLI v4 (DPLR-435)
encoding := "hex"
if strings.HasPrefix(string(response), crypto.Base64EncodingPrefix) {
encoding = "base64"
}
decryptedSecrets, err := crypto.Decrypt(passphrase, response, encoding)
decryptedSecrets, err := crypto.Decrypt(passphrase, response)
if err != nil {
return nil, Error{Err: err, Message: "Unable to decrypt cache file"}
}
Expand Down
97 changes: 65 additions & 32 deletions pkg/crypto/aes.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,21 @@ import (
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"time"

"github.com/DopplerHQ/cli/pkg/utils"
"golang.org/x/crypto/pbkdf2"
)

const Base64EncodingPrefix = "base64:"
const HexEncodingPrefix = "hex:"
const base64EncodingPrefix = "base64"
const hexEncodingPrefix = "hex"

func deriveKey(passphrase string, salt []byte) ([]byte, []byte, error) {
const pbkdf2Rounds = 500000
const legacyPbkdf2Rounds = 50000

func deriveKey(passphrase string, salt []byte, numRounds int) ([]byte, []byte, error) {
if salt == nil {
salt = make([]byte, 8)
// http://www.ietf.org/rfc/rfc2898.txt
Expand All @@ -48,13 +52,17 @@ func deriveKey(passphrase string, salt []byte) ([]byte, []byte, error) {
}
}

return pbkdf2.Key([]byte(passphrase), salt, 50000, 32, sha256.New), salt, nil
if numRounds < 0 {
return nil, nil, errors.New("Invalid number of key derivation rounds")
}

return pbkdf2.Key([]byte(passphrase), salt, numRounds, 32, sha256.New), salt, nil
}

// Encrypt plaintext with a passphrase; uses pbkdf2 for key deriv and aes-256-gcm for encryption
func Encrypt(passphrase string, plaintext []byte, encoding string) (string, error) {
now := time.Now()
key, salt, err := deriveKey(passphrase, nil)
key, salt, err := deriveKey(passphrase, nil, pbkdf2Rounds)
if err != nil {
return "", err
}
Expand All @@ -81,35 +89,27 @@ func Encrypt(passphrase string, plaintext []byte, encoding string) (string, erro

data := aesgcm.Seal(nil, iv, plaintext, nil)

var prefix string
var encodedSalt string
var encodedIV string
var encodedData string
if encoding == "base64" {
prefix = Base64EncodingPrefix
encodedSalt = base64.StdEncoding.EncodeToString(salt)
encodedIV = base64.StdEncoding.EncodeToString(iv)
encodedData = base64.StdEncoding.EncodeToString(data)
} else if encoding == "hex" {
prefix = HexEncodingPrefix
encodedSalt = hex.EncodeToString(salt)
encodedIV = hex.EncodeToString(iv)
encodedData = hex.EncodeToString(data)
} else {
return "", errors.New("Invalid encoding, must be one of [base64, hex]")
}

s := fmt.Sprintf("%s%s-%s-%s", prefix, encodedSalt, encodedIV, encodedData)
s := fmt.Sprintf("%s:%d:%s-%s-%s", encoding, pbkdf2Rounds, encodedSalt, encodedIV, encodedData)
return s, nil
}

func decodeBase64(passphrase string, ciphertext string) ([]byte, []byte, []byte, error) {
// prefix is required
if !strings.HasPrefix(ciphertext, Base64EncodingPrefix) {
return []byte{}, []byte{}, []byte{}, errors.New("Invalid ciphertext")
}

arr := strings.SplitN(ciphertext[len(Base64EncodingPrefix):], "-", 3)
arr := strings.SplitN(ciphertext, "-", 3)
if len(arr) != 3 {
return []byte{}, []byte{}, []byte{}, errors.New("Invalid ciphertext")
}
Expand Down Expand Up @@ -137,12 +137,6 @@ func decodeBase64(passphrase string, ciphertext string) ([]byte, []byte, []byte,
}

func decodeHex(passphrase string, ciphertext string) ([]byte, []byte, []byte, error) {
// prefix is optional
// TODO make this required when releasing CLI v4 (DPLR-435)
if strings.HasPrefix(ciphertext, HexEncodingPrefix) {
ciphertext = ciphertext[len(HexEncodingPrefix):]
}

arr := strings.SplitN(string(ciphertext), "-", 3)
if len(arr) != 3 {
return []byte{}, []byte{}, []byte{}, errors.New("Invalid ciphertext")
Expand All @@ -158,12 +152,10 @@ func decodeHex(passphrase string, ciphertext string) ([]byte, []byte, []byte, er
if err != nil {
return []byte{}, []byte{}, []byte{}, err
}

iv, err = hex.DecodeString(arr[1])
if err != nil {
return []byte{}, []byte{}, []byte{}, err
}

data, err = hex.DecodeString(arr[2])
if err != nil {
return []byte{}, []byte{}, []byte{}, err
Expand All @@ -172,28 +164,69 @@ func decodeHex(passphrase string, ciphertext string) ([]byte, []byte, []byte, er
return salt, iv, data, nil
}

// Decrypt ciphertext with a passphrase
func Decrypt(passphrase string, ciphertext []byte, encoding string) (string, error) {
// Decrypt ciphertext with a passphrase.
// Formats:
// 1) `encoding:numRounds:text`
// 2) `encoding:text`
// 3) `text`
func Decrypt(passphrase string, ciphertext []byte) (string, error) {
var salt []byte
var iv []byte
var data []byte
if encoding == "base64" {

cParts := strings.SplitN(string(ciphertext), ":", 3)
rawEncoding := ""
rawNumRounds := ""
ciphertextData := ""
if len(cParts) == 3 {
rawEncoding = cParts[0]
rawNumRounds = cParts[1]
ciphertextData = cParts[2]
} else if len(cParts) == 2 {
rawEncoding = cParts[0]
ciphertextData = cParts[1]
} else if len(cParts) == 1 {
ciphertextData = cParts[0]
} else {
return "", errors.New("Invalid ciphertext")
}

var encoding string
if rawEncoding == base64EncodingPrefix {
encoding = base64EncodingPrefix
} else if rawEncoding == hexEncodingPrefix || rawEncoding == "" {
// default to hex for backwards compatibility b/c we didn't always include an encoding prefix
// TODO remove support for optional prefix when releasing CLI v4 (DPLR-435)
encoding = hexEncodingPrefix
} else {
return "", errors.New("Invalid encoding, must be one of [base64, hex]")
}

numPbkdf2Rounds := legacyPbkdf2Rounds
if rawNumRounds != "" {
n, err := strconv.ParseInt(rawNumRounds, 10, 32)
if err != nil {
return "", errors.New("Unable to parse number of rounds")
}

numPbkdf2Rounds = int(n)
}

if encoding == base64EncodingPrefix {
var err error
salt, iv, data, err = decodeBase64(passphrase, string(ciphertext))
salt, iv, data, err = decodeBase64(passphrase, ciphertextData)
if err != nil {
return "", err
}
} else if encoding == "hex" {
} else {
var err error
salt, iv, data, err = decodeHex(passphrase, string(ciphertext))
salt, iv, data, err = decodeHex(passphrase, ciphertextData)
if err != nil {
return "", err
}
} else {
return "", errors.New("Invalid encoding, must be one of [base64, hex]")
}

key, _, err := deriveKey(passphrase, salt)
key, _, err := deriveKey(passphrase, salt, numPbkdf2Rounds)
if err != nil {
return "", err
}
Expand Down
36 changes: 26 additions & 10 deletions pkg/crypto/aes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,46 @@ const originalPassphrase = "secret"
const originalPlaintext = "{\"TEST_SECRET\":\"value\"}"

func TestDecrypt(t *testing.T) {
var ciphertext string

// decode hex w/o prefix
ciphertext := "9bc0a6db97dadea4-0d16d53716f505651f894aba-11b04a80eafd8ea700c7755de860aeb0347cff4ae93b626e858681e7e123034b4c11691a412843"
plaintext, err := Decrypt(originalPassphrase, []byte(ciphertext), "hex")
ciphertextData := "9bc0a6db97dadea4-0d16d53716f505651f894aba-11b04a80eafd8ea700c7755de860aeb0347cff4ae93b626e858681e7e123034b4c11691a412843"
plaintext, err := Decrypt(originalPassphrase, []byte(ciphertextData))
if err != nil || plaintext != originalPlaintext {
t.Error("Invalid plaintext when decrypting non-prefixed hex value")
}

// decode hex w/ prefix
ciphertext = fmt.Sprintf("hex:%s", ciphertext)
plaintext, err = Decrypt(originalPassphrase, []byte(ciphertext), "hex")
ciphertext = fmt.Sprintf("hex:%s", ciphertextData)
plaintext, err = Decrypt(originalPassphrase, []byte(ciphertext))
if err != nil || plaintext != originalPlaintext {
t.Error("Invalid plaintext when decrypting hex value")
}

// decode hex w/ prefix and num rounds
ciphertext = fmt.Sprintf("hex:50000:%s", ciphertextData)
plaintext, err = Decrypt(originalPassphrase, []byte(ciphertext))
if err != nil || plaintext != originalPlaintext {
t.Error("Invalid plaintext when decrypting hex value")
}

// decode base64 w/o prefix (should error)
ciphertext = "qwbkFMWB7FE=-Ew968YdkAXRb6l46-eA4o9Pf9mSIaOofa8YIEP+FqJ6DwScHsYIObAw3dvKvHbe5SDTzB"
plaintext, err = Decrypt(originalPassphrase, []byte(ciphertext), "base64")
ciphertextData = "qwbkFMWB7FE=-Ew968YdkAXRb6l46-eA4o9Pf9mSIaOofa8YIEP+FqJ6DwScHsYIObAw3dvKvHbe5SDTzB"
_, err = Decrypt(originalPassphrase, []byte(ciphertextData))
if err == nil {
t.Error("Expected error when decrypting non-prefixed base64 value")
}

// decode base64 w/ prefix
ciphertext = fmt.Sprintf("base64:%s", ciphertext)
plaintext, err = Decrypt(originalPassphrase, []byte(ciphertext), "base64")
ciphertext = fmt.Sprintf("base64:%s", ciphertextData)
plaintext, err = Decrypt(originalPassphrase, []byte(ciphertext))
if err != nil || plaintext != originalPlaintext {
t.Error("Invalid plaintext when decrypting base64 value")
}

// decode base64 w/ prefix and num rounds
ciphertext = fmt.Sprintf("base64:50000:%s", ciphertextData)
plaintext, err = Decrypt(originalPassphrase, []byte(ciphertext))
if err != nil || plaintext != originalPlaintext {
t.Error("Invalid plaintext when decrypting base64 value")
}
Expand All @@ -59,7 +75,7 @@ func TestEncrypt(t *testing.T) {
if err != nil {
t.Error("Invalid ciphertext when encrypting value w/ hex encoding")
}
plaintext, err := Decrypt(originalPassphrase, []byte(ciphertext), "hex")
plaintext, err := Decrypt(originalPassphrase, []byte(ciphertext))
if err != nil || plaintext != originalPlaintext {
t.Error("Invalid plaintext when decrypting hex value")
}
Expand All @@ -69,7 +85,7 @@ func TestEncrypt(t *testing.T) {
if err != nil {
t.Error("Invalid ciphertext when encrypting value w/ base64 encoding")
}
plaintext, err = Decrypt(originalPassphrase, []byte(ciphertext), "base64")
plaintext, err = Decrypt(originalPassphrase, []byte(ciphertext))
if err != nil || plaintext != originalPlaintext {
t.Error("Invalid plaintext when decrypting base64 value")
}
Expand Down

0 comments on commit a3c672c

Please sign in to comment.