Cannot decipher if passphrase was generated with openssl
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
# 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…
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.
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.