diff --git a/manpages/manual.txt b/manpages/manual.txt index 11d3bee8..f2b7044a 100644 --- a/manpages/manual.txt +++ b/manpages/manual.txt @@ -35,14 +35,14 @@ wolfCLU_main ENCRYPTION SYNOPSIS - wolfssl -encrypt <-algorithm> <-in filename> [-out filename] [-pwd password] [-iv IV] + wolfssl -encrypt <-algorithm> <-in filename> [-out filename] [-pwd password] [-iv IV] [-key hex] [-inkey filename] DESCRIPTION This command allows data to be encrypted using ciphers and keys based on passwords if not explicitly provided .ALGORITHMS -aes-cbc-[128|192|256] uses AES algorithm with designated key size. -aes-ctr-[128|192|256] - uses AES Counter with designated key size. Only available if ./congigure settings support + uses AES Counter with designated key size. Only available if ./configure settings support -3des-cbc-[056|112|168] uses 3DES algorithm with designated key size. -camellia-cbc-[128|192|256] @@ -51,20 +51,21 @@ ENCRYPTION -in filename/stdin the input filename, standard input. If file does not exist, it will treat data as stdin -out filename the output filename, if filename does not exist, it will be created -pwd password password to derive the key from. prompts if password option is not provided. If used, iv isn't needed - -iv IV the actual iv to use. If not provided, one is randomly generated. Must be provided in hex - -key Key the actual key to use. Must be in hex + -iv IV the actual iv to use. If not provided, one is randomly generated. Must be provided in hex + -key hex the actual key to use, supplied as a hex string on the command line. Length must match the algorithm key size. Requires -iv: when an explicit key is supplied, no salt-based key/iv derivation runs and no Salted__ header is written. + -inkey filename read the key from a file. The file may hold either a hex-encoded key (whitespace within the file is ignored) or a raw binary key whose byte length matches the algorithm key size. The argument must name a real file; use -key to pass a hex key on the command line. DECRYPTION SYNOPSIS - wolfssl -decrypt <-algorithm> <-in filename> [-out filename] [-key password] [-iv IV] + wolfssl -decrypt <-algorithm> <-in filename> [-out filename] [-pwd password] [-iv IV] [-key hex] [-inkey filename] DESCRIPTION This command allows data to be decrypted using ciphers and keys based on passwords if not explicitly provided ALGORITHMS -aes-cbc-[128|192|256] uses AES algorithm with designated key size. -aes-ctr-[128|192|256] - uses AES Counter with designated key size. Only available if ./congigure settings support + uses AES Counter with designated key size. Only available if ./configure settings support -3des-cbc-[056|112|168] uses 3DES algorithm with designated key size. -camellia-cbc-[128|192|256] @@ -73,8 +74,9 @@ DECRYPTION -in filename/stdin the input filename, standard input. If file does not exist, it will treat data as stdin -out filename the output filename, if filename does not exist, it will be created -pwd password password to derive the key from. prompts if password option is not provided. If used, iv isn't needed - -iv IV the actual iv to use. If not provided, one is randomly generated. Must be provided in hex - -key Key the actual key to use. Must be in hex + -iv IV the actual iv to use. If not provided, one is randomly generated. Must be provided in hex + -key hex the actual key to use, supplied as a hex string on the command line. Length must match the algorithm key size. Requires -iv: when an explicit key is supplied, no salt-based key/iv derivation runs, so the IV must be provided directly. + -inkey filename read the key from a file. The file may hold either a hex-encoded key (whitespace within the file is ignored) or a raw binary key whose byte length matches the algorithm key size. The argument must name a real file; use -key to pass a hex key on the command line. HASH diff --git a/manpages/wolfCLU_decrypt.1 b/manpages/wolfCLU_decrypt.1 index a8b23bb6..46107de1 100644 --- a/manpages/wolfCLU_decrypt.1 +++ b/manpages/wolfCLU_decrypt.1 @@ -4,7 +4,7 @@ .SH NAME decrypt \- cipher routines .SH SYNOPSIS -wolfssl -decrypt <-algorithm> <-in filename> [-out filename] [-pwd password] [-iv IV] [-key key] +wolfssl -decrypt <-algorithm> <-in filename> [-out filename] [-pwd password] [-iv IV] [-key hex] [-inkey filename] .SH DESCRIPTION This command allows data to be decrypted using ciphers and keys based on passwords if not explicitly provided .SH ALGORITHMS @@ -44,7 +44,26 @@ This command allows data to be decrypted using ciphers and keys based on passwor Must be provided in hex .br .LP --key Key the actual key to use. Must be in hex +-key hex the actual key to use, supplied as a hex string on the +.br + command line. Length must match the algorithm key size. +.br + Requires -iv: when an explicit key is supplied, no +.br + salt-based key/iv derivation runs, so the IV must be +.br + provided directly. +.br +.LP +-inkey filename read the key from a file. The file may contain either a +.br + hex-encoded key (whitespace within the file is ignored) +.br + or a raw binary key whose byte length matches the +.br + algorithm key size. The argument must name a real +.br + file; use -key to pass a hex key on the command line. .SH BUGS No known bugs at this time. .SH AUTHOR diff --git a/manpages/wolfCLU_encrypt.1 b/manpages/wolfCLU_encrypt.1 index 4b6865ec..61ec23c4 100644 --- a/manpages/wolfCLU_encrypt.1 +++ b/manpages/wolfCLU_encrypt.1 @@ -4,7 +4,7 @@ .SH NAME encrypt \- cipher routines .SH SYNOPSIS -wolfssl -encrypt <-algorithm> <-in filename> [-out filename] [-pwd password] [-iv IV] +wolfssl -encrypt <-algorithm> <-in filename> [-out filename] [-pwd password] [-iv IV] [-key hex] [-inkey filename] .SH DESCRIPTION This command allows data to be encrypted using ciphers and keys based on passwords if not explicitly provided .SH ALGORITHMS @@ -40,7 +40,26 @@ This command allows data to be encrypted using ciphers and keys based on passwor Must be provided in hex .br .LP --key Key the actual key to use. Must be in hex +-key hex the actual key to use, supplied as a hex string on the +.br + command line. Length must match the algorithm key size. +.br + Requires -iv: when an explicit key is supplied, no +.br + salt-based key/iv derivation runs and no Salted__ +.br + header is written. +.br +.LP +-inkey filename read the key from a file. The file may contain either a +.br + hex-encoded key (whitespace within the file is ignored) +.br + or a raw binary key whose byte length matches the +.br + algorithm key size. The argument must name a real +.br + file; use -key to pass a hex key on the command line. .SH BUGS No known bugs at this time. .SH AUTHOR diff --git a/src/crypto/clu_crypto_setup.c b/src/crypto/clu_crypto_setup.c index 24a918b1..c278ecb9 100644 --- a/src/crypto/clu_crypto_setup.c +++ b/src/crypto/clu_crypto_setup.c @@ -25,6 +25,64 @@ #ifndef WOLFCLU_NO_FILESYSTEM +/* Decode a hex key string into the caller-provided keyOut buffer. + * + * Wraps wolfCLU_hexToBin so the caller's pre-allocated key buffer is not + * replaced by hexToBin's internal allocation (which would leak the original + * and, on hexToBin failure, leave the caller pointing at a freed buffer). + * + * Returns WOLFCLU_SUCCESS on success, WOLFCLU_FATAL_ERROR on length mismatch + * or hex decode failure, MEMORY_E on allocation failure. */ +static int wolfCLU_loadHexKeyInto(byte* keyOut, int keyBytes, + const char* hex, word32 hexLen) +{ + byte* tmp = NULL; + word32 tmpSz = 0; + char* hexCopy; + int ret; + + if (hexLen != (word32)keyBytes * 2) { + WOLFCLU_LOG(WOLFCLU_L0, "Length of key provided was: %u.", + (unsigned int)(hexLen * 4)); + WOLFCLU_LOG(WOLFCLU_L0, "Length of key expected was: %d.", + keyBytes * 8); + WOLFCLU_LOG(WOLFCLU_E0, + "Invalid Key. Must match algorithm key size."); + return WOLFCLU_FATAL_ERROR; + } + + hexCopy = (char*)XMALLOC(hexLen + 1, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (hexCopy == NULL) { + return MEMORY_E; + } + XMEMCPY(hexCopy, hex, hexLen); + hexCopy[hexLen] = '\0'; + + ret = wolfCLU_hexToBin(hexCopy, &tmp, &tmpSz, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL); + wolfCLU_ForceZero(hexCopy, hexLen); + XFREE(hexCopy, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + if (ret != WOLFCLU_SUCCESS) { + WOLFCLU_LOG(WOLFCLU_E0, + "failed during conversion of Key, ret = %d", ret); + /* On failure wolfCLU_hexToBin frees its own internal buffer; do not + * touch tmp here. Propagate MEMORY_E unchanged so callers (and the + * documented contract above) can distinguish allocation failure + * from a generic decode error. */ + return (ret == MEMORY_E) ? MEMORY_E : WOLFCLU_FATAL_ERROR; + } + + XMEMCPY(keyOut, tmp, keyBytes); + wolfCLU_ForceZero(tmp, tmpSz); + /* tmp was allocated by wolfCLU_hexToBin with a NULL heap hint + * (see src/tools/clu_hex_to_bin.c); free it with the same hint. */ + XFREE(tmp, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return WOLFCLU_SUCCESS; +} + /* Prompt for a filename on stdin with validation. * Returns WOLFCLU_SUCCESS on success, WOLFCLU_FATAL_ERROR on EOF/read error. * buf is filled with the stripped, non-empty filename on success. */ @@ -111,17 +169,15 @@ int wolfCLU_setup(int argc, char** argv, char action) int encCheck = 0; /* if user is encrypting data */ int decCheck = 0; /* if user is decrypting data */ int inputHex = 0; /* if user is encrypting hexidecimal data */ - int keyType = 0; /* tells Decrypt which key it will be using - * 1 = password based key, 2 = user set key - */ + int keyType = WOLFCLU_KEYTYPE_NONE; + /* tells Decrypt which key it will be using; + * one of the WOLFCLU_KEYTYPE_* values from + * clu_optargs.h (NONE / PASSWORD / USER) */ int verbose = 0; /* flag to print out key/iv/salt */ int pbkVersion = 1; const WOLFSSL_EVP_MD* hashType = wolfSSL_EVP_sha256(); const WOLFSSL_EVP_CIPHER* cphr = NULL; - word32 ivSize = 0; /* IV if provided should be 2*block since - * reading a hex string passed in */ - word32 numBits = 0; /* number of bits in argument from the user */ int option; int longIndex = 1; @@ -162,6 +218,10 @@ int wolfCLU_setup(int argc, char** argv, char action) } XMEMSET(iv, 0, block); + /* keySize is in bits, but the legacy non-EVP wolfCLU_encrypt path + * writes keySize *bytes* into this buffer (it conflates the two units + * internally), so over-allocate to keySize bytes to keep that path + * safe. The cleanup below also zeros the full allocation. */ key = (byte*)XMALLOC(keySize, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (key == NULL) { wolfCLU_freeBins(pwdKey, iv, NULL, NULL, NULL); @@ -179,7 +239,7 @@ int wolfCLU_setup(int argc, char** argv, char action) passwordSz = keySize; ret = wolfCLU_GetPassword((char*)pwdKey, &passwordSz, optarg); pwdKeyChk = 1; - keyType = 1; + keyType = WOLFCLU_KEYTYPE_PASSWORD; break; case WOLFCLU_PASSWORD: @@ -189,7 +249,7 @@ int wolfCLU_setup(int argc, char** argv, char action) else { XSTRLCPY((char*)pwdKey, optarg, keySize); pwdKeyChk = 1; - keyType = 1; + keyType = WOLFCLU_KEYTYPE_PASSWORD; } break; @@ -205,37 +265,67 @@ int wolfCLU_setup(int argc, char** argv, char action) noSalt = 1; break; - case WOLFCLU_KEY: /* Key if used must be in hex */ + case WOLFCLU_KEY: /* hex key string from the command line */ + if (optarg == NULL) { + wolfCLU_LogError("no key passed in.."); + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return WOLFCLU_FATAL_ERROR; + } + + ret = wolfCLU_loadHexKeyInto(key, (keySize + 7) / 8, + optarg, (word32)XSTRLEN(optarg)); + if (ret != WOLFCLU_SUCCESS) { + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return ret; + } + keyCheck = 1; + keyType = WOLFCLU_KEYTYPE_USER; break; case WOLFCLU_IV: /* IV if used must be in hex */ { - char* ivString; + char* ivString; + byte* ivTmp = NULL; + word32 ivTmpSz = 0; if (optarg == NULL) { return WOLFCLU_FATAL_ERROR; } - else { - ivString = (char*)XMALLOC(XSTRLEN(optarg) + 1, HEAP_HINT, + ivString = (char*)XMALLOC(XSTRLEN(optarg) + 1, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (ivString == NULL) { - wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); - return MEMORY_E; - } - - XSTRLCPY(ivString, optarg, XSTRLEN(optarg) + 1); - ret = wolfCLU_hexToBin(ivString, &iv, &ivSize, - NULL, NULL, NULL, - NULL, NULL, NULL, - NULL, NULL, NULL); - XFREE(ivString, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (ret != WOLFCLU_SUCCESS) { - WOLFCLU_LOG(WOLFCLU_E0, - "failed during conversion of IV, ret = %d", ret); - wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); - return WOLFCLU_FATAL_ERROR; - } - ivCheck = 1; + if (ivString == NULL) { + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return MEMORY_E; } + XSTRLCPY(ivString, optarg, XSTRLEN(optarg) + 1); + + /* Decode into a temporary so the pre-allocated `iv` buffer + * (block bytes) isn't replaced by hexToBin's internal + * allocation, which would leak the original. */ + ret = wolfCLU_hexToBin(ivString, &ivTmp, &ivTmpSz, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL); + XFREE(ivString, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (ret != WOLFCLU_SUCCESS) { + WOLFCLU_LOG(WOLFCLU_E0, + "failed during conversion of IV, ret = %d", ret); + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return WOLFCLU_FATAL_ERROR; + } + if ((int)ivTmpSz != block) { + WOLFCLU_LOG(WOLFCLU_E0, + "IV length mismatch: expected %d bytes, got %u", + block, (unsigned int)ivTmpSz); + wolfCLU_ForceZero(ivTmp, ivTmpSz); + XFREE(ivTmp, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return WOLFCLU_FATAL_ERROR; + } + XMEMCPY(iv, ivTmp, ivTmpSz); + wolfCLU_ForceZero(ivTmp, ivTmpSz); + /* hexToBin allocates with NULL heap hint; free with same. */ + XFREE(ivTmp, NULL, DYNAMIC_TYPE_TMP_BUFFER); + ivCheck = 1; } break; @@ -274,50 +364,131 @@ int wolfCLU_setup(int argc, char** argv, char action) break; case WOLFCLU_INKEY: - if (optarg == NULL) { - wolfCLU_LogError("no key passed in.."); - wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); - return WOLFCLU_FATAL_ERROR; - } + { + WOLFSSL_BIO* keyBio = NULL; + byte* fileBuf = NULL; + int fileLen = 0; + int keyBytes = (keySize + 7) / 8; + int isHex = 1; + int i; - /* 2 characters = 1 byte. 1 byte = 8 bits - */ - numBits = (word32)(XSTRLEN(optarg) * 4); - /* Key for encryption */ - if ((int)numBits != keySize) { - WOLFCLU_LOG(WOLFCLU_L0, - "Length of key provided was: %d.", numBits); - WOLFCLU_LOG(WOLFCLU_L0, - "Length of key expected was: %d.", keySize); - WOLFCLU_LOG(WOLFCLU_E0, - "Invalid Key. Must match algorithm key size."); - wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); - return WOLFCLU_FATAL_ERROR; - } - else { - char* keyString; + if (optarg == NULL) { + wolfCLU_LogError("no key file passed in.."); + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return WOLFCLU_FATAL_ERROR; + } - keyString = (char*)XMALLOC(XSTRLEN(optarg) + 1, HEAP_HINT, + /* -inkey is "input file for key" (matches the help text and + * openssl convention). The argument must name a real file; + * use -key for a hex key on the command line. */ + keyBio = wolfSSL_BIO_new_file(optarg, "rb"); + if (keyBio == NULL) { + wolfCLU_LogError("could not open key file '%s'", optarg); + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return WOLFCLU_FATAL_ERROR; + } + + fileLen = wolfSSL_BIO_get_len(keyBio); + if (fileLen <= 0) { + wolfCLU_LogError("key file '%s' is empty or unreadable", + optarg); + wolfSSL_BIO_free(keyBio); + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return WOLFCLU_FATAL_ERROR; + } + + fileBuf = (byte*)XMALLOC(fileLen, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (keyString == NULL) { + if (fileBuf == NULL) { + wolfSSL_BIO_free(keyBio); wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); return MEMORY_E; } - XSTRLCPY(keyString, optarg, XSTRLEN(optarg) + 1); - ret = wolfCLU_hexToBin(keyString, &key, &numBits, - NULL, NULL, NULL, - NULL, NULL, NULL, - NULL, NULL, NULL); - XFREE(keyString, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (ret != WOLFCLU_SUCCESS) { - WOLFCLU_LOG(WOLFCLU_E0, - "failed during conversion of Key, ret = %d", ret); + if (wolfSSL_BIO_read(keyBio, fileBuf, fileLen) != fileLen) { + wolfCLU_LogError("failed to read key file '%s'", optarg); + wolfCLU_ForceZero(fileBuf, fileLen); + XFREE(fileBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + wolfSSL_BIO_free(keyBio); wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); return WOLFCLU_FATAL_ERROR; } + wolfSSL_BIO_free(keyBio); + + /* Decide hex vs raw by inspecting every byte. Whitespace + * (\r \n space tab) is allowed inside hex files as a + * separator; any non-hex non-whitespace byte means the + * file is raw binary. fileLen is left unmodified so a + * raw-binary key whose last byte is 0x09/0x0A/0x0D/0x20 + * still round-trips correctly. */ + for (i = 0; i < fileLen; i++) { + byte c = fileBuf[i]; + if (c == '\r' || c == '\n' || c == ' ' || c == '\t') { + continue; + } + if (!wolfCLU_isHexDigit(c)) { + isHex = 0; + break; + } + } + + if (isHex) { + char* keyString; + int j = 0; + + keyString = (char*)XMALLOC(fileLen + 1, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (keyString == NULL) { + wolfCLU_ForceZero(fileBuf, fileLen); + XFREE(fileBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return MEMORY_E; + } + /* Copy out hex characters, skipping any embedded + * whitespace so block-formatted hex files work. */ + for (i = 0; i < fileLen; i++) { + byte c = fileBuf[i]; + if (c == '\r' || c == '\n' || c == ' ' || c == '\t') { + continue; + } + keyString[j++] = (char)c; + } + keyString[j] = '\0'; + + ret = wolfCLU_loadHexKeyInto(key, keyBytes, + keyString, (word32)j); + wolfCLU_ForceZero(keyString, j); + XFREE(keyString, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + wolfCLU_ForceZero(fileBuf, fileLen); + XFREE(fileBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (ret != WOLFCLU_SUCCESS) { + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return ret; + } + } + else { + /* Raw binary key. Length must match the algorithm. */ + if (fileLen != keyBytes) { + WOLFCLU_LOG(WOLFCLU_L0, + "Length of key provided was: %d bits.", + fileLen * 8); + WOLFCLU_LOG(WOLFCLU_L0, + "Length of key expected was: %d bits.", + keySize); + WOLFCLU_LOG(WOLFCLU_E0, + "Invalid Key. Must match algorithm key size."); + wolfCLU_ForceZero(fileBuf, fileLen); + XFREE(fileBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return WOLFCLU_FATAL_ERROR; + } + XMEMCPY(key, fileBuf, fileLen); + wolfCLU_ForceZero(fileBuf, fileLen); + XFREE(fileBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } + keyCheck = 1; - keyType = 2; + keyType = WOLFCLU_KEYTYPE_USER; } break; @@ -400,14 +571,26 @@ int wolfCLU_setup(int argc, char** argv, char action) if (ivCheck == 1) { if (keyCheck == 0) { WOLFCLU_LOG(WOLFCLU_E0, - "-iv was explicitly set, but no -key was set. User" - " needs to provide a non-password based key when setting" - " the -iv flag."); + "-iv was explicitly set, but no -key or -inkey was" + " provided. A non-password based key must be supplied" + " when setting the -iv flag."); wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); return WOLFCLU_FATAL_ERROR; } } + /* When the user supplies an explicit -key/-inkey, no salt-based + * key/iv derivation runs. The cipher therefore needs an explicit -iv: + * silently using the all-zero buffer would produce ciphertext that no + * one (including this tool on a later run) can decrypt safely. */ + if (keyCheck == 1 && ivCheck == 0) { + WOLFCLU_LOG(WOLFCLU_E0, + "-key/-inkey requires -iv to be set: an IV must be" + " supplied alongside an explicit key."); + wolfCLU_freeBins(pwdKey, iv, key, NULL, NULL); + return WOLFCLU_FATAL_ERROR; + } + if (pwdKeyChk == 1 && keyCheck == 1) { XMEMSET(pwdKey, 0, keySize + block); } @@ -419,7 +602,7 @@ int wolfCLU_setup(int argc, char** argv, char action) if (cphr != NULL) { ret = wolfCLU_evp_crypto(cphr, mode, pwdKey, key, (keySize+7)/8, in, out, NULL, iv, 0, 1, pbkVersion, hashType, verbose, isBase64, - noSalt); + noSalt, keyType); } else { if (outCheck == 0) { @@ -443,7 +626,7 @@ int wolfCLU_setup(int argc, char** argv, char action) if (cphr != NULL) { ret = wolfCLU_evp_crypto(cphr, mode, pwdKey, key, (keySize+7)/8, in, out, NULL, iv, 0, 0, pbkVersion, hashType, verbose, - isBase64, noSalt); + isBase64, noSalt, keyType); } else { if (outCheck == 0) { @@ -464,7 +647,9 @@ int wolfCLU_setup(int argc, char** argv, char action) else { wolfCLU_help(); } - /* clear and free data */ + /* clear and free data — zero the full allocation, not just the + * keyBytes actually used, so any future code path that writes past + * the cipher key length doesn't leak material across XFREE. */ XMEMSET(key, 0, keySize); XMEMSET(pwdKey, 0, keySize + block); XMEMSET(iv, 0, block); diff --git a/src/crypto/clu_decrypt.c b/src/crypto/clu_decrypt.c index 774ada0a..f99fa3ee 100644 --- a/src/crypto/clu_decrypt.c +++ b/src/crypto/clu_decrypt.c @@ -124,7 +124,7 @@ int wolfCLU_decrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, ret = FREAD_ERROR; } /* replicates old pwdKey if pwdKeys match */ - if (ret == 0 && keyType == 1) { + if (ret == 0 && keyType == WOLFCLU_KEYTYPE_PASSWORD) { if (wc_PBKDF2(key, pwdKey, (int) XSTRLEN((const char*)pwdKey), salt, SALT_SIZE, CLU_4K_TYPE, size, CLU_SHA256) != 0) { @@ -132,7 +132,7 @@ int wolfCLU_decrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, ret = ENCRYPT_ERROR; } } - else if (ret == 0 && keyType == 2) { + else if (ret == 0 && keyType == WOLFCLU_KEYTYPE_USER) { for (i = 0; i < size; i++) { /* ensure key is set */ diff --git a/src/crypto/clu_evp_crypto.c b/src/crypto/clu_evp_crypto.c index 87477e47..77e1afa4 100644 --- a/src/crypto/clu_evp_crypto.c +++ b/src/crypto/clu_evp_crypto.c @@ -35,8 +35,18 @@ int wolfCLU_evp_crypto(const WOLFSSL_EVP_CIPHER* cphr, char* mode, byte* pwdKey, byte* key, int keySz, char* fileIn, char* fileOut, char* hexIn, byte* iv, int hexOut, int enc, int pbkVersion, - const WOLFSSL_EVP_MD* hashType, int printOut, int isBase64, int noSalt) + const WOLFSSL_EVP_MD* hashType, int printOut, int isBase64, int noSalt, + int keyType) { + /* WOLFCLU_KEYTYPE_USER means the caller supplied -key/-inkey (and -iv): + * the key+iv buffers already hold the user's material, and the cipher + * must use them directly with no salt-based PBKDF2/BytesToKey derivation + * and no Salted__ header in the output. Treat this case like an + * implicit -nosalt for everything except the algorithm choice. */ + int userKey = (keyType == WOLFCLU_KEYTYPE_USER); + if (userKey) { + noSalt = 1; + } WOLFSSL_BIO *out = NULL; WOLFSSL_BIO *in = NULL; WOLFSSL_BIO *tmp = NULL; @@ -157,8 +167,10 @@ int wolfCLU_evp_crypto(const WOLFSSL_EVP_CIPHER* cphr, char* mode, byte* pwdKey, } } - /* stretches pwdKey */ - if (ret == WOLFCLU_SUCCESS) { + /* stretches pwdKey (skipped when the caller supplied an explicit key + * via -key/-inkey: there is no password to derive from, and the key/iv + * buffers already hold the user's material). */ + if (ret == WOLFCLU_SUCCESS && !userKey) { if (pbkVersion == WOLFCLU_PBKDF2) { #ifdef HAVE_FIPS if (XSTRLEN((const char*)pwdKey) < HMAC_FIPS_MIN_KEY) { diff --git a/src/tools/clu_hex_to_bin.c b/src/tools/clu_hex_to_bin.c index 58b63b52..de57a4fe 100644 --- a/src/tools/clu_hex_to_bin.c +++ b/src/tools/clu_hex_to_bin.c @@ -29,6 +29,17 @@ #include +/* Return 1 if c is an ASCII hex digit, 0 otherwise. Provided as a shared + * predicate for the per-character hex-class checks in wolfCLU; paths that + * decode whole strings should still use wolfCLU_hexToBin / Base16_Decode + * rather than open-coding their own scan. */ +int wolfCLU_isHexDigit(byte c) +{ + return (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); +} + /* free up to 5 binary buffers using wolfssl abstraction layer */ void wolfCLU_freeBins(byte* b1, byte* b2, byte* b3, byte* b4, byte* b5) { diff --git a/src/tools/clu_rand.c b/src/tools/clu_rand.c index cce46f1d..100d8f19 100644 --- a/src/tools/clu_rand.c +++ b/src/tools/clu_rand.c @@ -26,6 +26,7 @@ static const struct option rand_options[] = { {"-out", required_argument, 0, WOLFCLU_OUTFILE}, {"-base64", no_argument, 0, WOLFCLU_BASE64 }, + {"-hex", no_argument, 0, WOLFCLU_HEX }, {0, 0, 0, 0} /* terminal element */ }; @@ -35,6 +36,7 @@ static void wolfCLU_RandHelp(void) WOLFCLU_LOG(WOLFCLU_L0, "wolfssl rand "); WOLFCLU_LOG(WOLFCLU_L0, "\t-out the file to output data to (default to stdout)"); WOLFCLU_LOG(WOLFCLU_L0, "\t-base64 output the results in base64 encoding"); + WOLFCLU_LOG(WOLFCLU_L0, "\t-hex output the results in hex encoding"); } @@ -43,14 +45,19 @@ int wolfCLU_Rand(int argc, char** argv) #ifndef WC_NO_RNG int ret = WOLFCLU_SUCCESS; int useBase64 = 0; + int useHex = 0; + int outIsStdout = 0; int size = 0; int option; int longIndex = 1; WOLFSSL_BIO *bioOut = NULL; byte *buf = NULL; - /* last parameter is the rand bytes output size */ - if (XSTRNCMP("-h", argv[argc-1], 2) == 0) { + /* last parameter is the rand bytes output size. Match -h/-help exactly + * so other -h-prefixed flags (e.g. -hex) aren't swallowed as a help + * request. */ + if (XSTRCMP("-h", argv[argc-1]) == 0 || + XSTRCMP("-help", argv[argc-1]) == 0) { wolfCLU_RandHelp(); return WOLFCLU_SUCCESS; } @@ -72,6 +79,10 @@ int wolfCLU_Rand(int argc, char** argv) useBase64 = 1; break; + case WOLFCLU_HEX: + useHex = 1; + break; + case WOLFCLU_OUTFILE: #ifdef WOLFCLU_NO_FILESYSTEM WOLFCLU_LOG(WOLFCLU_E0, "No filesystem support. Unable to open input file"); @@ -101,6 +112,20 @@ int wolfCLU_Rand(int argc, char** argv) } + if (ret == WOLFCLU_SUCCESS && useBase64 && useHex) { + wolfCLU_LogError("-base64 and -hex are mutually exclusive"); + ret = WOLFCLU_FATAL_ERROR; + } + + /* Fail fast on a size that would overflow the hex buffer length + * (size * 2). Validate before the RNG allocation so an over-large + * request does not first burn an O(GB) malloc and the matching + * RNG fill. */ + if (ret == WOLFCLU_SUCCESS && useHex && size > INT_MAX / 2) { + wolfCLU_LogError("requested size too large for -hex output"); + ret = WOLFCLU_FATAL_ERROR; + } + if (ret == WOLFCLU_SUCCESS) { buf = (byte*)XMALLOC(size, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (buf == NULL) { @@ -125,6 +150,7 @@ int wolfCLU_Rand(int argc, char** argv) /* setup output bio to stdout if not set */ if (ret == WOLFCLU_SUCCESS && bioOut == NULL) { + outIsStdout = 1; bioOut = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); if (bioOut == NULL) { ret = WOLFCLU_FATAL_ERROR; @@ -137,6 +163,29 @@ int wolfCLU_Rand(int argc, char** argv) } } + /* check and convert to hex (size * 2 was already bounded above) */ + if (ret == WOLFCLU_SUCCESS && useHex) { + static const char hexChars[] = "0123456789abcdef"; + word32 hexSz = (word32)size * 2; + byte* hex = (byte*)XMALLOC(hexSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + int i; + + if (hex == NULL) { + wolfCLU_LogError("Error malloc'ing for hex"); + ret = WOLFCLU_FATAL_ERROR; + } + else { + for (i = 0; i < size; i++) { + hex[2 * i] = hexChars[(buf[i] >> 4) & 0x0F]; + hex[2 * i + 1] = hexChars[buf[i] & 0x0F]; + } + wolfCLU_ForceZero(buf, size); + XFREE(buf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + buf = hex; + size = (int)hexSz; + } + } + /* check and convert to base64 */ if (ret == WOLFCLU_SUCCESS && useBase64) { byte *base64 = NULL; @@ -179,6 +228,11 @@ int wolfCLU_Rand(int argc, char** argv) wolfCLU_LogError("Error writing out RNG data"); ret = WOLFCLU_FATAL_ERROR; } + else if (useHex && outIsStdout) { + /* Match `openssl rand -hex` and avoid the next shell prompt + * landing on the same line as the hex output. */ + (void)wolfSSL_BIO_write(bioOut, "\n", 1); + } } if (buf != NULL) { diff --git a/tests/encrypt/enc-test.py b/tests/encrypt/enc-test.py index 02c9ac50..61cce271 100644 --- a/tests/encrypt/enc-test.py +++ b/tests/encrypt/enc-test.py @@ -249,6 +249,62 @@ def test_pbkdf2_wolfssl_enc_openssl_dec(self): self.assertEqual(ossl.returncode, 0, ossl.stderr) self.assertTrue(filecmp.cmp(orig, dec, shallow=False)) + def test_explicit_key_iv_wolfssl_to_openssl(self): + """Proves the user-supplied -key actually reaches the cipher. + + Without keyType plumbed into the EVP path, wolfCLU silently runs + BytesToKey/PBKDF2 over an empty password and overwrites the + user's key, so a same-tool round-trip succeeds while interop with + openssl on the same hex key fails.""" + key_hex = "00112233445566778899aabbccddeeff"\ + "00112233445566778899aabbccddeeff" + iv_hex = "0123456789abcdef0123456789abcdef" + enc = "wolfssl_key_iv.enc" + dec = "wolfssl_key_iv.dec" + orig = os.path.join(CERTS_DIR, "crl.der") + self._cleanup(enc, dec) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", orig, "-out", enc, + "-key", key_hex, "-iv", iv_hex) + self.assertEqual(r.returncode, 0, r.stderr) + + ossl = subprocess.run( + ["openssl", "enc", "-d", "-aes-256-cbc", "-nosalt", + "-K", key_hex, "-iv", iv_hex, + "-in", enc, "-out", dec], + capture_output=True, timeout=60) + self.assertEqual(ossl.returncode, 0, + "openssl could not decrypt wolfCLU output with " + "the same -K/-iv: " + ossl.stderr.decode( + errors="replace")) + self.assertTrue(filecmp.cmp(orig, dec, shallow=False), + "wolfCLU -> openssl interop failed") + + def test_explicit_key_iv_openssl_to_wolfssl(self): + """Reverse interop: openssl encrypts with -K/-iv, wolfCLU decrypts.""" + key_hex = "00112233445566778899aabbccddeeff"\ + "00112233445566778899aabbccddeeff" + iv_hex = "0123456789abcdef0123456789abcdef" + enc = "openssl_key_iv.enc" + dec = "openssl_key_iv.dec" + orig = os.path.join(CERTS_DIR, "crl.der") + self._cleanup(enc, dec) + + ossl = subprocess.run( + ["openssl", "enc", "-aes-256-cbc", "-nosalt", + "-K", key_hex, "-iv", iv_hex, + "-in", orig, "-out", enc], + capture_output=True, timeout=60) + self.assertEqual(ossl.returncode, 0, ossl.stderr) + + r = run_wolfssl("-decrypt", "-aes-cbc-256", + "-in", enc, "-out", dec, + "-key", key_hex, "-iv", iv_hex) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertTrue(filecmp.cmp(orig, dec, shallow=False), + "openssl -> wolfCLU interop failed") + def test_pbkdf2_wolfssl_pass_flag(self): enc = "test-enc.der" dec = "test-dec.der" @@ -489,5 +545,278 @@ def test_camellia_outname_too_long_reprompt(self): "Camellia roundtrip mismatch after too-long reprompt") +class EncKeyInputTest(unittest.TestCase): + """Tests for the -key (hex on CLI) and -inkey (key from file) flags.""" + + # AES-256 key/iv used across the tests. + KEY_HEX = "00112233445566778899aabbccddeeff"\ + "00112233445566778899aabbccddeeff" + IV_HEX = "0123456789abcdef0123456789abcdef" + + @classmethod + def setUpClass(cls): + if not os.path.isdir(CERTS_DIR): + raise unittest.SkipTest("certs directory not found") + + config_log = os.path.join(".", "config.log") + if os.path.isfile(config_log): + with open(config_log, "r") as f: + if "disable-filesystem" in f.read(): + raise unittest.SkipTest("filesystem support disabled") + + def _cleanup(self, *files): + for f in files: + self.addCleanup(lambda p=f: os.remove(p) + if os.path.exists(p) else None) + + def _orig(self): + return os.path.join(CERTS_DIR, "server-key.der") + + def test_key_hex_cli_roundtrip(self): + """`-key ` accepts a hex key on the command line.""" + enc = "key_cli.enc" + dec = "key_cli.dec" + self._cleanup(enc, dec) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-key", self.KEY_HEX, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "encrypt with -key hex failed: " + r.stderr) + + r = run_wolfssl("-decrypt", "-aes-cbc-256", + "-in", enc, "-out", dec, + "-key", self.KEY_HEX, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "decrypt with -key hex failed: " + r.stderr) + self.assertTrue(filecmp.cmp(self._orig(), dec, shallow=False), + "round-trip with -key hex did not recover plaintext") + + def test_inkey_hex_file_roundtrip(self): + """`-inkey ` reads a hex-encoded key from a file.""" + keyfile = "key_inkey_hex.txt" + enc = "inkey_hex.enc" + dec = "inkey_hex.dec" + self._cleanup(keyfile, enc, dec) + + # Write hex with a trailing newline (typical of `echo > file`). + with open(keyfile, "w") as f: + f.write(self.KEY_HEX + "\n") + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "encrypt with -inkey hex file failed: " + r.stderr) + + r = run_wolfssl("-decrypt", "-aes-cbc-256", + "-in", enc, "-out", dec, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "decrypt with -inkey hex file failed: " + r.stderr) + self.assertTrue(filecmp.cmp(self._orig(), dec, shallow=False), + "round-trip with -inkey hex file mismatched") + + def test_inkey_raw_binary_file_roundtrip(self): + """`-inkey ` reads a raw binary key from a file.""" + keyfile = "key_inkey_bin.key" + enc = "inkey_bin.enc" + dec = "inkey_bin.dec" + self._cleanup(keyfile, enc, dec) + + with open(keyfile, "wb") as f: + f.write(bytes.fromhex(self.KEY_HEX)) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "encrypt with -inkey raw binary file failed: " + + r.stderr) + + r = run_wolfssl("-decrypt", "-aes-cbc-256", + "-in", enc, "-out", dec, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "decrypt with -inkey raw binary file failed: " + + r.stderr) + self.assertTrue(filecmp.cmp(self._orig(), dec, shallow=False), + "round-trip with -inkey raw binary file mismatched") + + def test_inkey_hex_file_with_embedded_whitespace(self): + """Embedded whitespace inside a hex key file is ignored.""" + keyfile = "key_inkey_ws.txt" + enc = "inkey_ws.enc" + dec = "inkey_ws.dec" + self._cleanup(keyfile, enc, dec) + + # Split the hex key across two lines with a leading space. + chunked = " " + self.KEY_HEX[:32] + "\n" + self.KEY_HEX[32:] + "\n" + with open(keyfile, "w") as f: + f.write(chunked) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "encrypt with whitespace-formatted hex file failed: " + + r.stderr) + + r = run_wolfssl("-decrypt", "-aes-cbc-256", + "-in", enc, "-out", dec, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "decrypt with whitespace-formatted hex file failed: " + + r.stderr) + self.assertTrue(filecmp.cmp(self._orig(), dec, shallow=False), + "round-trip with whitespace-formatted hex file failed") + + def test_inkey_missing_file_errors(self): + """-inkey requires an actual file. A missing path must error; + the previous "fall back to hex parse" behavior was removed because + it ambiguously interpreted typo'd filenames as keys.""" + enc = "inkey_bad.enc" + self._cleanup(enc) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-inkey", "no-such-file.key", "-iv", self.IV_HEX) + self.assertNotEqual(r.returncode, 0, + "missing key file must error out") + self.assertFalse(os.path.exists(enc), + "no output should be produced on missing key file") + # And specifically: even a 64-char hex string that happens to be a + # missing path must NOT be silently parsed as a hex key — use -key + # for that. + if os.path.exists(self.KEY_HEX): + os.remove(self.KEY_HEX) + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-inkey", self.KEY_HEX, "-iv", self.IV_HEX) + self.assertNotEqual(r.returncode, 0, + "-inkey hex string must error (use -key instead)") + + def test_inkey_raw_binary_trailing_whitespace_byte(self): + """Regression: a 32-byte raw binary key whose last byte is 0x0A must + not be silently truncated by trailing-whitespace handling.""" + keyfile = "key_inkey_lf.bin" + enc = "inkey_lf.enc" + dec = "inkey_lf.dec" + self._cleanup(keyfile, enc, dec) + + # 31 random-looking bytes (chosen so hex detection routes to raw) + # followed by 0x0A as the terminal byte. + key_bytes = bytes(range(0x80, 0x80 + 31)) + b"\x0a" + self.assertEqual(len(key_bytes), 32) + with open(keyfile, "wb") as f: + f.write(key_bytes) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "raw key ending in 0x0A must not be truncated: " + + r.stderr) + + r = run_wolfssl("-decrypt", "-aes-cbc-256", + "-in", enc, "-out", dec, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "decrypt with 0x0A-terminated raw key failed: " + + r.stderr) + self.assertTrue(filecmp.cmp(self._orig(), dec, shallow=False), + "round-trip with 0x0A-terminated raw key mismatched") + + def test_inkey_wrong_length_raw_binary_errors(self): + """A raw binary file whose byte length doesn't match must error.""" + keyfile = "key_inkey_short.bin" + enc = "inkey_short.enc" + self._cleanup(keyfile, enc) + + # 16 bytes, but algorithm requires 32 (AES-256). + with open(keyfile, "wb") as f: + f.write(bytes(range(16))) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertNotEqual(r.returncode, 0, + "wrong-size raw binary key must error out") + self.assertFalse(os.path.exists(enc), + "no output should be produced on key size mismatch") + + def test_key_without_iv_errors(self): + """`-key`/`-inkey` requires `-iv` (no salt-based derivation runs).""" + enc = "key_no_iv.enc" + self._cleanup(enc) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-key", self.KEY_HEX) + self.assertNotEqual(r.returncode, 0, + "-key with no -iv must fail") + combined = (r.stdout or "") + (r.stderr or "") + self.assertIn("-iv", combined, + "validation error should mention -iv") + + def test_iv_without_key_error_mentions_inkey(self): + """The -iv-without-key error now references both -key and -inkey.""" + enc = "iv_only.enc" + self._cleanup(enc) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-iv", self.IV_HEX) + self.assertNotEqual(r.returncode, 0, + "-iv without a key must fail") + combined = (r.stdout or "") + (r.stderr or "") + self.assertIn("-inkey", combined, + "validation error should mention -inkey alongside -key") + + def test_key_invalid_hex_does_not_crash(self): + """Regression for double-free: a -key arg with the right length but + invalid hex characters must error cleanly, not abort/segfault.""" + enc = "key_bad_hex.enc" + self._cleanup(enc) + + bad_hex = "gg" + ("11" * 31) # 64 chars, contains non-hex + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-key", bad_hex, "-iv", self.IV_HEX) + self.assertNotEqual(r.returncode, 0, + "invalid hex must produce an error") + # Process aborts (SIGABRT/-6 on POSIX, large unsigned on Windows) + # would surface here; assert we got a clean wolfCLU error code. + self.assertGreater(r.returncode, 0, + "expected a normal error exit, got signal: " + + str(r.returncode)) + + def test_rand_hex_to_inkey_workflow(self): + """End-to-end: generate hex key with `rand -hex` and use via -inkey.""" + keyfile = "rand_hex_key.hex" + enc = "rand_hex.enc" + dec = "rand_hex.dec" + self._cleanup(keyfile, enc, dec) + + r = run_wolfssl("rand", "-hex", "-out", keyfile, "32") + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl("-encrypt", "-aes-cbc-256", + "-in", self._orig(), "-out", enc, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "encrypt with rand-generated hex key failed: " + + r.stderr) + + r = run_wolfssl("-decrypt", "-aes-cbc-256", + "-in", enc, "-out", dec, + "-inkey", keyfile, "-iv", self.IV_HEX) + self.assertEqual(r.returncode, 0, + "decrypt with rand-generated hex key failed: " + + r.stderr) + self.assertTrue(filecmp.cmp(self._orig(), dec, shallow=False), + "rand-hex -> -inkey workflow did not round-trip") + + if __name__ == "__main__": test_main() diff --git a/tests/rand/rand-test.py b/tests/rand/rand-test.py index 84843fba..68be8f52 100644 --- a/tests/rand/rand-test.py +++ b/tests/rand/rand-test.py @@ -42,6 +42,63 @@ def test_output_file(self): self.assertEqual(r.returncode, 0, r.stderr) self.assertTrue(os.path.isfile(out), "entropy.txt not created") + def test_hex_stdout_length(self): + """`-hex` to stdout produces 2 hex chars per byte plus trailing \\n.""" + r = run_wolfssl("rand", "-hex", "16") + self.assertEqual(r.returncode, 0, r.stderr) + # 32 hex chars + newline so the next shell prompt isn't on the same + # line (matches `openssl rand -hex` behavior). + self.assertEqual(len(r.stdout), 33, + "expected 32 hex chars + newline from -hex 16") + self.assertEqual(r.stdout[-1], "\n", "missing trailing newline") + self.assertTrue(all(c in "0123456789abcdef" for c in r.stdout[:-1]), + "output is not lowercase hex") + + def test_hex_to_file(self): + """`-hex -out file` writes a hex-only file (no trailing newline) + suitable for direct use as -inkey input.""" + out = "hex_rand.hex" + self.addCleanup(lambda: os.remove(out) + if os.path.exists(out) else None) + + r = run_wolfssl("rand", "-hex", "-out", out, "32") + self.assertEqual(r.returncode, 0, r.stderr) + with open(out, "rb") as f: + data = f.read() + self.assertEqual(len(data), 64, + "expected 64 bytes for 32 random bytes in hex") + self.assertTrue(all(chr(b) in "0123456789abcdef" for b in data), + "hex file should contain only lowercase hex") + + def test_hex_and_base64_mutually_exclusive(self): + """`-hex` and `-base64` cannot be combined.""" + r = run_wolfssl("rand", "-hex", "-base64", "8") + self.assertNotEqual(r.returncode, 0, + "-hex with -base64 must error out") + + def test_hex_size_overflow_rejected(self): + """`-hex` must reject sizes that would overflow size*2 rather than + silently allocating an undersized buffer and writing past it.""" + # 2^30 fits in int but 2^30 * 2 == 2^31 wraps signed int. The + # binary should refuse this size, not crash or silently truncate. + r = run_wolfssl("rand", "-hex", str(2**30)) + self.assertNotEqual(r.returncode, 0, + "rand -hex with overflow-prone size must error") + + def test_hex_flag_not_swallowed_by_help_check(self): + """Regression: `-hex` must not match the `-h`/`-help` prefix detector. + + Putting -hex *last* on the command line previously triggered the + rand help screen because the first two characters ("-h") matched. + Help text must only fire for an exact -h/-help argument.""" + r = run_wolfssl("rand", "16", "-hex") + # Either the user gets a "missing size" error (because "-hex" is + # not the documented size-as-last-arg) or actual hex output — + # but never a silent help screen with exit 0. + out = (r.stdout or "") + self.assertNotIn("wolfssl rand ", out, + "rand 16 -hex must not be treated as help") + if __name__ == "__main__": test_main() diff --git a/wolfclu/clu_header_main.h b/wolfclu/clu_header_main.h index f80ac610..3f10d1ce 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -317,6 +317,9 @@ int wolfCLU_hexToBin(const char* h1, byte** b1, word32* b1Sz, const char* h3, byte** b3, word32* b3Sz, const char* h4, byte** b4, word32* b4Sz); +/* Return 1 if c is an ASCII hex digit ([0-9a-fA-F]), 0 otherwise. */ +int wolfCLU_isHexDigit(byte c); + /* A function to free MALLOCED buffers * * @param b1 a buffer to be freed, can be set to NULL @@ -371,8 +374,9 @@ int wolfCLU_encrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, * @param out the filename to output following en/de cryption * @param iv if entered must be in hex otherwise generated at run time * @param block size of block as determined by the algorithm being used - * @param keyType let's decrypt know if it's using a password based key or a - * hexidecimal, user specified key. + * @param keyType selects between password-derived and explicit user key; + * one of WOLFCLU_KEYTYPE_PASSWORD or WOLFCLU_KEYTYPE_USER (see + * clu_optargs.h). */ int wolfCLU_decrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, char* in, char* out, byte* iv, int block, int keyType); @@ -396,12 +400,20 @@ int wolfCLU_decrypt(int alg, char* mode, byte* pwdKey, byte* key, int size, * @param pbkVersion WOLFCLU_PBKDF2 or WOLFCLU_PBKDF1 * @param hashType the hash type to use with key/iv generation * @param printOut set to 1 for debug print outs + * @param isBase64 set to 1 if the input/output should be base64 encoded + * @param noSalt set to 1 to skip the Salted__ header and salt-based + * key/iv derivation (no salt written on encrypt, none read + * on decrypt) + * @param keyType WOLFCLU_KEYTYPE_PASSWORD for a password-derived key, or + * WOLFCLU_KEYTYPE_USER for an explicit key/iv supplied by + * the caller (skips PBKDF2/BytesToKey derivation and forces + * noSalt semantics). See clu_optargs.h. */ int wolfCLU_evp_crypto(const WOLFSSL_EVP_CIPHER* cphr, char* mode, byte* pwdKey, byte* key, int keySz, char* fileIn, char* fileOut, char* hexIn, byte* iv, int hexOut, int enc, int pbkVersion, const WOLFSSL_EVP_MD* hashType, int printOut, int isBase64, - int noSalt); + int noSalt, int keyType); /* benchmarking function * diff --git a/wolfclu/clu_optargs.h b/wolfclu/clu_optargs.h index 13d9b150..ce12e18e 100644 --- a/wolfclu/clu_optargs.h +++ b/wolfclu/clu_optargs.h @@ -108,6 +108,7 @@ enum { WOLFCLU_OUTPUT, WOLFCLU_PBKDF2, WOLFCLU_BASE64, + WOLFCLU_HEX, WOLFCLU_NOSALT, WOLFCLU_HELP, WOLFCLU_DEBUG, @@ -157,5 +158,11 @@ enum { #define WOLFCLU_PBKDF2 2 #define WOLFCLU_PBKDF1 1 +/* keyType values used by wolfCLU_setup / wolfCLU_evp_crypto / wolfCLU_decrypt + * to distinguish password-derived keys from explicit user-supplied keys. */ +#define WOLFCLU_KEYTYPE_NONE 0 /* no key set yet */ +#define WOLFCLU_KEYTYPE_PASSWORD 1 /* derive key from -pwd / -pass */ +#define WOLFCLU_KEYTYPE_USER 2 /* explicit -key / -inkey supplied */ + #endif /* WOLFCLU_OPTARGS_H */