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

Cannot decipher if passphrase was generated with openssl #22

Open
Canadadry opened this issue May 3, 2022 · 3 comments
Open

Cannot decipher if passphrase was generated with openssl #22

Canadadry opened this issue May 3, 2022 · 3 comments
Labels

Comments

@Canadadry
Copy link

Canadadry commented May 3, 2022

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

@Luzifer
Copy link
Owner

Luzifer commented May 3, 2022

# 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…

@Canadadry
Copy link
Author

Canadadry commented May 4, 2022

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
Copy link
Author

Canadadry commented May 4, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants