go-openssl icon indicating copy to clipboard operation
go-openssl copied to clipboard

Cannot decipher if passphrase was generated with openssl

Open Canadadry opened this issue 3 years ago • 3 comments

I cannot decipher if passphrase was generated with openssl, I get an invalid padding error

Here a minimal code

package main

import (
	"bytes"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"flag"
	"fmt"
	openssl "github.com/Luzifer/go-openssl/v4"
	"io"
	"os"
	"os/exec"
)

func main() {
	if err := run(); err != nil {
		fmt.Println("failed", err)
	}
}

func run() error {
	basicKey := false
	mode := ""
	keepKey := false
	flag.BoolVar(&basicKey, "basic", basicKey, "enable passphrase to be generated by go")
	flag.StringVar(&mode, "mode", mode, "opensl rand mode empty|base64|hex")
	flag.BoolVar(&keepKey, "keep", keepKey, "keep generated key")
	flag.Parse()

	data := "data.txt"
	dataEnc := "data.txt.enc"
	plain := "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
	key := "key.bin"

	err := os.WriteFile(data, []byte(plain), 0644)
	if err != nil {
		return fmt.Errorf("WriteFile errored: %w", err)
	}
	defer os.Remove(data)

	if basicKey {
		err = generateKey(key)
	} else {
		err = generateKeyWithOpenSSL(key, mode)
	}
	if err != nil {
		return fmt.Errorf("generateKey errored: %w : mode basic %v", err, basicKey)
	}
	if !keepKey {
		defer os.Remove(key)
	}

	cmd := exec.Command("openssl",
		"enc", "-aes-256-cbc", "-salt", "-pbkdf2", "-iter", "100000", "-md", "sha256",
		"-in", data,
		"-out", dataEnc,
		"-pass", fmt.Sprintf("file:./%s", key),
	)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("OpenSSL enc errored: %w : %s", err, string(out))
	}
	defer os.Remove(dataEnc)

	keyContent, err := os.ReadFile(key)
	if err != nil {
		return fmt.Errorf("cannot read key %w", err)
	}

	dataEncContent, err := os.ReadFile(dataEnc)
	if err != nil {
		return fmt.Errorf("cannot read key %w", err)
	}

	decodedKey := decodeKey(keyContent)

	_, err = openssl.New().DecryptBinaryBytes(string(keyContent), dataEncContent, credsGenerator())
	if err != nil {
		fmt.Printf("failed with raw key DecryptBinaryBytes errored %v\n", err)
	}

	_, err = openssl.New().DecryptBinaryBytes(string(decodedKey), dataEncContent, credsGenerator())
	if err != nil {
		fmt.Printf("failed with decoded key DecryptBinaryBytes errored %v\n", err)
	}
	return nil
}

func credsGenerator() openssl.CredsGenerator {
	iter := 100000
	return openssl.NewPBKDF2Generator(sha256.New, iter)
}

func decodeKey(key []byte) []byte {
	plain, err := base64.RawURLEncoding.DecodeString(string(key))
	if err == nil && len(plain) == 32 {
		fmt.Println("key was base64 RawURLEncoding")
		return plain
	} else {
		fmt.Printf("key was not base64 RawURLEncoding because len %d and err %v\n", len(plain), err)
	}
	plain, err = base64.RawStdEncoding.DecodeString(string(key))
	if err == nil && len(plain) == 32 {
		fmt.Println("key was base64 RawStdEncoding")
		return plain
	} else {
		fmt.Printf("key was not base64 RawStdEncoding because len %d and err %v\n", len(plain), err)
	}
	plain, err = base64.URLEncoding.DecodeString(string(key))
	if err == nil && len(plain) == 32 {
		fmt.Println("key was base64 URLEncoding %s", plain)
		return plain
	} else {
		fmt.Printf("key was not base64 URLEncoding because len %d and err %v\n", len(plain), err)
	}
	plain, err = base64.StdEncoding.DecodeString(string(key))
	if err == nil && len(plain) == 32 {
		fmt.Println("key was base64 StdEncoding")
		return plain
	} else {
		fmt.Printf("key was not base64 StdEncoding because len %d and err %v\n", len(plain), err)
	}
	plain, err = hex.DecodeString(string(key))
	if err == nil && len(plain) == 32 {
		fmt.Println("key was hex")
		return plain
	} else {
		fmt.Printf("key was not hex because len %d and err %v\n", len(plain), err)
	}
	fmt.Println("nothing found")
	return key
}

func generateKeyWithOpenSSL(keyPath, mode string) error {
	action := []string{"rand"}
	option := []string{"-out", keyPath, "32"}
	if mode != "" {
		action = append(action, "-"+mode)
	}
	action = append(action, option...)

	cmd := exec.Command("openssl", action...)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("OpenSSL rand errored: %w : %s", err, string(out))
	}
	return nil
}

func generateKey(keyPath string) error {
	key, err := GenerateRandom(32)
	if err != nil {
		return fmt.Errorf("cannot generate random : %w", err)
	}
	return os.WriteFile(keyPath, key, 0644)
}

func GenerateRandom(keyLength int64) ([]byte, error) {
	buf := bytes.Buffer{}
	enc := base64.NewEncoder(base64.StdEncoding, &buf)
	defer enc.Close()
	_, err := io.Copy(enc, io.LimitReader(rand.Reader, keyLength))
	if err != nil {
		return nil, fmt.Errorf("cannot generate key : %w", err)
	}
	return buf.Bytes(), nil
}

If you call it with -basic it work greats

My openssl version :

> openssl version
OpenSSL 1.1.1k  25 Mar 2021

Any idea on what can change in the passphrase that can have this kind of behavior ?

Edit : I can make a pr with a new test with this logic if that helps. Edit 2: update code to add more flag

Canadadry avatar May 03 '22 16:05 Canadadry

# go run main.go; echo "$?"
failed DecryptBinaryBytes errored invalid padding
0

# git diff
diff --git a/main.go b/main.go
index 02408e1..70f6193 100644
--- a/main.go
+++ b/main.go
@@ -81,7 +81,7 @@ func credsGenerator() openssl.CredsGenerator {
 }

 func generateKeyWithOpenSSL(keyPath string) error {
-       cmd := exec.Command("openssl", "rand", "-hex", "-out", keyPath, "32")
+       cmd := exec.Command("openssl", "rand", "-out", keyPath, "32")
        out, err := cmd.CombinedOutput()
        if err != nil {
                return fmt.Errorf("OpenSSL rand errored: %w : %s", err, string(out))

# go run main.go; echo "$?"
0

# openssl version
OpenSSL 1.1.1n  15 Mar 2022

I think that's the output you've expected (no error being logged)?

I'd suspect OpenSSL to have some detection for hex-encoded keys while this library tries to use the key literally?

Need to have a look further into this, just a first short look…

Luzifer avatar May 03 '22 17:05 Luzifer

Yeah that what I though too. I didn't have the courage yet to read openssl source.

But I does not seems to be that simple. You have the same bug with the -base64 option and when I generate the random key on my side there is no error. I tried to use base64 to mimic the openssl rand command.

I add option to the program to ease testing (and edit the first post) and I found interesting stuff.

  • OpenSSL use std base64 and not url base64
  • I cannot read OpenSSL hex encoded, after reading 32bytes it always gives me the same error : encoding/hex: invalid byte: U+000A
  • when decoding my random key I never get 32bytes of data only 30bytes, with no error.

Edit:

And with base64 mode I can decode the key but it still not working :

> go run main.go -mode base64
key was not base64 RawURLEncoding because len 0 and err illegal base64 data at input byte 3
key was not base64 RawStdEncoding because len 30 and err illegal base64 data at input byte 43
key was not base64 URLEncoding because len 0 and err illegal base64 data at input byte 3
key was base64 StdEncoding
failed with raw key DecryptBinaryBytes errored invalid padding
failed with decoded key DecryptBinaryBytes errored invalid padding

Edit 2: OpenSSL source code for rand does not show anything special in command

        chunk = (num > buflen) ? buflen : num;
        r = RAND_bytes(buf, chunk);
        if (r <= 0)
            goto end;
        if (format != FORMAT_TEXT) {
            if (BIO_write(out, buf, chunk) != chunk)
                goto end;
        } else {
            for (i = 0; i < chunk; i++)
                if (BIO_printf(out, "%02x", buf[i]) != 2)
                    goto end;
        }

nor in the base64 code, hex encoding is even simplier.

Canadadry avatar May 04 '22 07:05 Canadadry

Okay I dig a little in openssl source code and did not fnd anything special.

First it start here in the enc command around line 350 passarg came from the cli -pass

    if (str == NULL && passarg != NULL) {
        if (!app_passwd(passarg, NULL, &pass, NULL)) {
            BIO_printf(bio_err, "Error getting password\n");
            goto end;
        }
        str = pass;
    }

I follow the app_passwd func which which wrap a call to app_get_pass to load the pass here around line 250 the only external stuff they do is using OPENSSL_strdup which is only a string duplication function call openssl malloc instead of the std one.

static char *app_get_pass(const char *arg, int keepbio)
{
    static BIO *pwdbio = NULL;
    char *tmp, tpass[APP_PASS_LEN];
    int i;

    /* PASS_SOURCE_SIZE_MAX = max number of chars before ':' in below strings */
    if (CHECK_AND_SKIP_PREFIX(arg, "pass:"))
        return OPENSSL_strdup(arg);
    if (CHECK_AND_SKIP_PREFIX(arg, "env:")) {
        tmp = getenv(arg);
        if (tmp == NULL) {
            BIO_printf(bio_err, "No environment variable %s\n", arg);
            return NULL;
        }
        return OPENSSL_strdup(tmp);
    }

If we get back the the command to see how the real pass is used we can follow var str to line 499 for an example in the PKCS5_PBKDF2_HMAC

            if (pbkdf2 == 1) {
                /*
                * derive key and default iv
                * concatenated into a temporary buffer
                */
                unsigned char tmpkeyiv[EVP_MAX_KEY_LENGTH + EVP_MAX_IV_LENGTH];
                int iklen = EVP_CIPHER_get_key_length(cipher);
                int ivlen = EVP_CIPHER_get_iv_length(cipher);
                /* not needed if HASH_UPDATE() is fixed : */
                int islen = (sptr != NULL ? sizeof(salt) : 0);
                if (!PKCS5_PBKDF2_HMAC(str, str_len, sptr, islen,
                                       iter, dgst, iklen+ivlen, tmpkeyiv)) {
                    BIO_printf(bio_err, "PKCS5_PBKDF2_HMAC failed\n");
                    goto end;
                }
                /* split and move data back to global buffer */
                memcpy(key, tmpkeyiv, iklen);
                memcpy(iv, tmpkeyiv+iklen, ivlen);

I don't think PKCS5_PBKDF2_HMAC would transform the key from hex or base64 to anything.

Canadadry avatar May 04 '22 08:05 Canadadry