diff --git a/src/include.am b/src/include.am index 776b7c65..8d1b44a2 100644 --- a/src/include.am +++ b/src/include.am @@ -9,6 +9,7 @@ wolfssl_SOURCES = src/clu_main.c \ src/tools/clu_funcs.c \ src/tools/clu_hex_to_bin.c \ src/tools/clu_rand.c \ + src/tools/clu_io.c \ src/crypto/clu_crypto_setup.c \ src/crypto/clu_encrypt.c \ src/crypto/clu_decrypt.c \ diff --git a/src/tools/clu_base64.c b/src/tools/clu_base64.c index 20a9d8b3..13430e9e 100644 --- a/src/tools/clu_base64.c +++ b/src/tools/clu_base64.c @@ -23,6 +23,7 @@ #include #include #include +#include static const struct option base64_options[] = { {"-in", required_argument, 0, WOLFCLU_INFILE }, @@ -49,14 +50,15 @@ static void wolfCLU_Base64Help(void) int wolfCLU_Base64Setup(int argc, char** argv) { #if !defined(WOLFCLU_NO_FILESYSTEM) && !defined(NO_CODING) - WOLFSSL_BIO *bioIn = NULL; - WOLFSSL_BIO *bioOut = NULL; + char *inFile = NULL; + char *outFile = NULL; byte* input = NULL; byte* output = NULL; int ret = WOLFCLU_SUCCESS; int decode = 0; int isPEM = 0; - sword32 inputSz = 8000; + /* set by wolfCLU_ReadIo */ + word32 inputSz = 0; word32 outputSz = 0; int option; int longIndex = 1; @@ -75,20 +77,23 @@ int wolfCLU_Base64Setup(int argc, char** argv) break; case WOLFCLU_INFILE: - bioIn = wolfSSL_BIO_new_file(optarg, "rb"); - if (bioIn == NULL) { - wolfCLU_LogError("unable to open file %s", optarg); + if (optarg == NULL) { + wolfCLU_LogError("-in expected a value"); ret = WOLFCLU_FATAL_ERROR; } + else { + inFile = optarg; + } break; case WOLFCLU_OUTFILE: - bioOut = wolfSSL_BIO_new_file(optarg, "wb"); - if (bioOut == NULL) { - wolfCLU_LogError("unable to open output file %s", - optarg); + if (optarg == NULL) { + wolfCLU_LogError("-out expected a value"); ret = WOLFCLU_FATAL_ERROR; } + else { + outFile = optarg; + } break; case 'd': @@ -97,12 +102,6 @@ int wolfCLU_Base64Setup(int argc, char** argv) case WOLFCLU_HELP: wolfCLU_Base64Help(); - if (bioIn != NULL) { - wolfSSL_BIO_free(bioIn); - } - if (bioOut != NULL) { - wolfSSL_BIO_free(bioOut); - } return WOLFCLU_SUCCESS; case ':': @@ -118,56 +117,40 @@ int wolfCLU_Base64Setup(int argc, char** argv) } } - if (ret == WOLFCLU_SUCCESS && bioIn == NULL) { - bioIn = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); - if (bioIn != NULL) - wolfSSL_BIO_set_fp(bioIn, stdin, BIO_NOCLOSE); - } - else if (ret == WOLFCLU_SUCCESS) { - /* get data size using raw FILE pointer and seek */ - XFILE f; - if (wolfSSL_BIO_get_fp(bioIn, &f) != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Unable to get raw file pointer"); - ret = WOLFCLU_FATAL_ERROR; + if (ret == WOLFCLU_SUCCESS) { + WOLFCLU_IO ioIn = {0}; + if (inFile == NULL) { + ioIn = wolfCLU_OpenIo_fp(stdin, WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_NOCLOSE); + } + else { + ioIn = wolfCLU_OpenIo_file(inFile, WOLFCLU_IO_READABLE_FILE); } - if (ret == WOLFCLU_SUCCESS && XFSEEK(f, 0, XSEEK_END) != 0) { - wolfCLU_LogError("Unable to seek end of file"); + if (ioIn.type <= 0 || + wolfCLU_ReadIo(&ioIn, &input, &inputSz, 0) != + WOLFCLU_SUCCESS) { ret = WOLFCLU_FATAL_ERROR; } - - if (ret == WOLFCLU_SUCCESS) { - inputSz = (sword32)XFTELL(f); - wolfSSL_BIO_reset(bioIn); + if (ioIn.type > 0 && wolfCLU_CloseIo(&ioIn) != WOLFCLU_SUCCESS) { + wolfCLU_LogError("Could not close io"); + ret = WOLFCLU_FATAL_ERROR; } } - if (ret == WOLFCLU_SUCCESS) { - input = (byte*)XMALLOC(inputSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (input == NULL) { - wolfCLU_LogError("Memory allocation error for input buffer"); - ret = MEMORY_E; - } - else { - inputSz = wolfSSL_BIO_read(bioIn, input, inputSz); - - if (inputSz < 0) { - wolfCLU_LogError("Could not read input."); - ret = WOLFCLU_FATAL_ERROR; - } - /* For decoding, check if input is in PEM format */ - else if (decode && inputSz > 11) { - /* Check if the input starts with a PEM header */ - if (XMEMCMP(input, "-----BEGIN", 10) == 0) { - isPEM = 1; - } - } - + /* when decoding, check for a PEM header on the input */ + if (ret == WOLFCLU_SUCCESS && decode && inputSz >= 10) { + if (XMEMCMP(input, "-----BEGIN", 10) == 0) { + isPEM = 1; } } /* Perform encoding/decoding */ - if (ret == WOLFCLU_SUCCESS && decode) { + if (ret == WOLFCLU_SUCCESS && inputSz == 0) { + /* empty input produces empty output, matching 'openssl base64' */ + outputSz = 0; + } + else if (ret == WOLFCLU_SUCCESS && decode) { if (isPEM) { #ifdef WOLFSSL_PEM_TO_DER /* Try different PEM types */ @@ -259,52 +242,41 @@ int wolfCLU_Base64Setup(int argc, char** argv) } if (ret == WOLFCLU_SUCCESS) { - if (Base64_Encode(input, inputSz, output, &outputSz) < 0) { - wolfCLU_LogError("Base64 encode failed: %d", ret); + int encRet = Base64_Encode(input, inputSz, output, &outputSz); + if (encRet < 0) { + wolfCLU_LogError("Base64 encode failed: %d", encRet); ret = WOLFCLU_FATAL_ERROR; } - else { - ret = WOLFCLU_SUCCESS; - } } } - if (ret == WOLFCLU_SUCCESS && bioOut != NULL) { - /* Write output */ - ret = wolfSSL_BIO_write(bioOut, output, outputSz); - if (ret <= 0) { - wolfCLU_LogError("Failed to write output data: %d", ret); - ret = WOLFCLU_FATAL_ERROR; + if (ret == WOLFCLU_SUCCESS) { + WOLFCLU_IO ioOut = {0}; + if (outFile == NULL) { + ioOut = wolfCLU_OpenIo_fp(stdout, WOLFCLU_IO_WRITABLE_STREAM | + WOLFCLU_IO_NOCLOSE); } else { - ret = WOLFCLU_SUCCESS; + ioOut = wolfCLU_OpenIo_file(outFile, WOLFCLU_IO_WRITABLE_FILE); } - } - else if (ret == WOLFCLU_SUCCESS) { - /* Write to stdout */ - bioOut = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); - if (bioOut != NULL) { - wolfSSL_BIO_set_fp(bioOut, stdout, BIO_NOCLOSE); - ret = wolfSSL_BIO_write(bioOut, output, outputSz); - if (ret <= 0) { - wolfCLU_LogError("Failed to write to stdout: %d", ret); - ret = WOLFCLU_FATAL_ERROR; - } - else { - ret = WOLFCLU_SUCCESS; - } + + if (ioOut.type <= 0 || + wolfCLU_WriteIo(&ioOut, output, outputSz) != WOLFCLU_SUCCESS) { + ret = WOLFCLU_FATAL_ERROR; } - else { - wolfCLU_LogError("Failed to create stdout BIO"); - ret = MEMORY_E; + if (ioOut.type > 0 && wolfCLU_CloseIo(&ioOut) != WOLFCLU_SUCCESS) { + wolfCLU_LogError("Could not close io"); + ret = WOLFCLU_FATAL_ERROR; } } /* Clean up */ if (input != NULL) { + wolfCLU_ForceZero(input, inputSz); XFREE(input, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } if (output != NULL) { + wolfCLU_ForceZero(output, outputSz); XFREE(output, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); } #ifdef WOLFSSL_PEM_TO_DER @@ -312,12 +284,6 @@ int wolfCLU_Base64Setup(int argc, char** argv) wc_FreeDer(&der); } #endif - if (bioIn != NULL) { - wolfSSL_BIO_free(bioIn); - } - if (bioOut != NULL) { - wolfSSL_BIO_free(bioOut); - } return ret; #else diff --git a/src/tools/clu_io.c b/src/tools/clu_io.c new file mode 100644 index 00000000..421fa7fa --- /dev/null +++ b/src/tools/clu_io.c @@ -0,0 +1,441 @@ +/* clu_io.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +/* Windows opens stdout and stdin in text mode, translating 0x0A <-> 0x0D 0x0A. + * We don't want that */ +#if defined(_WIN32) + #include + #include +#endif + + +enum WOLFCLU_IO_TYPE_SETS { + WOLFCLU_IO_READABLE = + WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_READABLE_FILE, + + WOLFCLU_IO_WRITABLE = + WOLFCLU_IO_WRITABLE_STREAM | + WOLFCLU_IO_WRITABLE_FILE, + + WOLFCLU_IO_RW = + WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_READABLE_FILE | + WOLFCLU_IO_WRITABLE_STREAM | + WOLFCLU_IO_WRITABLE_FILE, +}; + +typedef struct WOLFCLU_IO_BUFFER { + byte* outBuf; + word32 len; + word32 cap; +} WOLFCLU_IO_BUFFER; + +#ifndef WOLFCLU_NO_FILESYSTEM +/* move the data to a new allocation of newCap bytes, wiping the old one so no + * stale copies are left on the heap */ +static int ResizeBuffer(WOLFCLU_IO_BUFFER* buffer, word32 newCap) +{ + byte* tmp = (byte*)XMALLOC(newCap, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (tmp == NULL) { + wolfCLU_LogError("Could not allocate space for io read."); + return WOLFCLU_FATAL_ERROR; + } + if (buffer->outBuf != NULL) { + XMEMCPY(tmp, buffer->outBuf, buffer->len); + wolfCLU_ForceZero(buffer->outBuf, buffer->len); + XFREE(buffer->outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } + buffer->outBuf = tmp; + buffer->cap = newCap; + return WOLFCLU_SUCCESS; +} + +/* read a stream of unknown length, growing the buffer as we go. A stream with + * no end, like /dev/urandom, is read until WOLFCLU_IO_MAX_READ_SZ is passed */ +static int StreamRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer, word32 limit) +{ + word32 bytesRead = 0; + int newCap; + int ret = WOLFCLU_SUCCESS; + while (1) { + if (buffer->cap == buffer->len) { + if (buffer->cap == limit) { + /* try to read one more byte to make sure that if the + * one more byte is EOF we break out with a full buffer */ + char lookOneMore; + if (XFREAD(&lookOneMore, 1, 1, fp) != 0) { + wolfCLU_LogError("input too big needs to be %u " + "bytes or less", limit); + ret = WOLFCLU_FATAL_ERROR; + break; + } + else if (XFERROR(fp) != 0) { + wolfCLU_LogError("Error occured while reading the file"); + ret = WOLFCLU_FATAL_ERROR; + break; + } + else { + /* we have read exactly to our limit! */ + break; + } + } + + if (buffer->cap == 0) { + newCap = limit > 1024 ? 1024 : limit; + } + else if (buffer->cap > (limit / 3)) { + newCap = limit; + } + else { + newCap = buffer->cap * 2; + } + ret = ResizeBuffer(buffer, newCap); + if (ret != WOLFCLU_SUCCESS) { + break; + } + } + bytesRead = (word32)XFREAD(buffer->outBuf + buffer->len, + sizeof(*buffer->outBuf), buffer->cap - buffer->len, fp); + /* check for errors first, ports without ferror define XFERROR as 0 */ + if (XFERROR(fp)) { + wolfCLU_LogError("Error while reading input stream."); + ret = WOLFCLU_FATAL_ERROR; + break; + } + if (bytesRead == 0) { + break; /* EOF */ + } + + buffer->len += (word32)bytesRead; + } + + return ret; +} + +/* seek to get the length, then read it in one allocation. Inputs that can't + * be sized this way, like pipes and devices, are read as a stream */ +static int FileRead(XFILE fp, WOLFCLU_IO_BUFFER* buffer, word32 limit) +{ + word32 bytesRead = 0; + long fileSize = 0; + if (XFSEEK(fp, 0, XSEEK_END) != 0) { + /* not seekable, e.g. a pipe */ + return StreamRead(fp, buffer, limit); + } + if ((fileSize = XFTELL(fp)) < 0) { + wolfCLU_LogError("Could not get length of file"); + return WOLFCLU_FATAL_ERROR; + } + else if (fileSize > limit) { + wolfCLU_LogError("File is too large max is %d bytes", + limit); + return WOLFCLU_FATAL_ERROR; + } + if (XFSEEK(fp, 0, XSEEK_SET) != 0) { + wolfCLU_LogError("Could not seek input file"); + return WOLFCLU_FATAL_ERROR; + } + if (fileSize == 0) { + /* devices report a size of 0, an empty file just reads nothing. An + * endless device, like /dev/zero, reads until the max read size */ + return StreamRead(fp, buffer, limit); + } + buffer->len = (word32)fileSize; + buffer->outBuf = (byte*)XMALLOC(buffer->len, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (buffer->outBuf == NULL) { + wolfCLU_LogError("Could not allocate space for io read."); + return WOLFCLU_FATAL_ERROR; + } + buffer->cap = buffer->len; + bytesRead = (word32)XFREAD(buffer->outBuf, sizeof(*buffer->outBuf), + buffer->len, fp); + /* check for errors first, ports without ferror define XFERROR as 0 */ + if (XFERROR(fp)) { + wolfCLU_LogError("Could not read all of the file data"); + return WOLFCLU_FATAL_ERROR; + } + /* a short read is EOF, e.g. sysfs files report a size larger than their + * data */ + buffer->len = (int)bytesRead; + return WOLFCLU_SUCCESS; +} +#endif + +int wolfCLU_ReadIo(WOLFCLU_IO* io, byte** buf, word32* len, word32 limit) +{ + int ret = WOLFCLU_SUCCESS; +#ifndef WOLFCLU_NO_FILESYSTEM + WOLFCLU_IO_BUFFER buffer = {0}; + + if (io == NULL || io->type <= 0 || buf == NULL || len == NULL) { + wolfCLU_LogError("Bad arg passed to wolfCLU_ReadIo"); + return BAD_FUNC_ARG; + } + + if (limit == 0) { + limit = UINT_MAX; + } + + *buf = NULL; + *len = 0; + if (!(io->type & WOLFCLU_IO_READABLE)) { + wolfCLU_LogError("Invalid ioType passed to wolfCLU_ReadIo"); + return BAD_FUNC_ARG; + } + + if (io->type & WOLFCLU_IO_READABLE_FILE) { + ret = FileRead(io->fp, &buffer, limit); + } + else { + ret = StreamRead(io->fp, &buffer, limit); + } + + if (ret != WOLFCLU_SUCCESS) { + if (buffer.outBuf != NULL) { + wolfCLU_ForceZero(buffer.outBuf, buffer.cap); + XFREE(buffer.outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + buffer.outBuf = NULL; + } + } + else { + if (buffer.len == 0 && buffer.outBuf != NULL) { + wolfCLU_ForceZero(buffer.outBuf, buffer.cap); + XFREE(buffer.outBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + buffer.outBuf = NULL; + } + *len = buffer.len; + *buf = buffer.outBuf; + } + return ret; +#else + (void)io; + (void)buf; + (void)len; + ret = NOT_COMPILED_IN; + return ret; +#endif +} + +int wolfCLU_WriteIo(WOLFCLU_IO* io, const byte* buf, word32 len) +{ + int ret = WOLFCLU_SUCCESS; +#ifndef WOLFCLU_NO_FILESYSTEM + if (io == NULL || io->type <= 0 || (buf == NULL && len > 0)) { + wolfCLU_LogError("Bad arg passed to wolfCLU_WriteIo"); + return BAD_FUNC_ARG; + } + + if (!(io->type & WOLFCLU_IO_WRITABLE)) { + wolfCLU_LogError("Invalid ioType passed to wolfCLU_WriteIo"); + return BAD_FUNC_ARG; + } + + if (len > 0 && XFWRITE(buf, sizeof(*buf), len, io->fp) != len) { + wolfCLU_LogError("Could not write buffer out to target"); + ret = WOLFCLU_FATAL_ERROR; + } +#ifdef XFFLUSH + /* the write may only be buffered, flush so errors like a full disk are + * caught here */ + if (ret == WOLFCLU_SUCCESS && XFFLUSH(io->fp) != 0) { + wolfCLU_LogError("Could not flush buffer out to target"); + ret = WOLFCLU_FATAL_ERROR; + } +#endif + return ret; + +#else + (void)io; + (void)buf; + (void)len; + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); + ret = NOT_COMPILED_IN; + return ret; +#endif +} + +WOLFCLU_IO wolfCLU_OpenIo_fd(int fd, enum WOLFCLU_IO_TYPE type) +{ + WOLFCLU_IO io = {WOLFCLU_IO_NOT_OPEN, XBADFILE}; +#if !defined(WOLFCLU_NO_FILESYSTEM) && defined(XFDOPEN) + XFILE fp = XBADFILE; + if (fd < 0 || type <= 0) { + wolfCLU_LogError("Bad arg passed to wolfCLU_OpenIo_fd"); + io.type = WOLFCLU_IO_ERROR; + return io; + } + + if ((type & WOLFCLU_IO_READABLE) && (type & WOLFCLU_IO_WRITABLE)) { + fp = XFDOPEN(fd, "rb+"); + } + else if (type & WOLFCLU_IO_READABLE) { + fp = XFDOPEN(fd, "rb"); + } + else if (type & WOLFCLU_IO_WRITABLE) { + fp = XFDOPEN(fd, "wb"); + } + else { + wolfCLU_LogError("Bad type passed to wolfCLU_OpenIo_fd"); + io.type = WOLFCLU_IO_ERROR; + } + if (fp == XBADFILE) { + wolfCLU_LogError("Could not open file pointer from file descriptor"); + io.type = WOLFCLU_IO_ERROR; + } + + if (io.type == WOLFCLU_IO_NOT_OPEN) { + io.fp = fp; + io.type = type; +#ifdef _WIN32 + /* binary mode */ + (void)_setmode(_fileno(io.fp), _O_BINARY); +#endif + } + return io; +#else + (void)type; + (void)fd; +#if defined(WOLFCLU_NO_FILESYSTEM) + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); +#endif +#if !defined(XFDOPEN) + wolfCLU_LogError("Cannot open file pointer from file descriptor"); +#endif + io.type = WOLFCLU_IO_NOT_COMPILED_IN; + return io; +#endif +} + +WOLFCLU_IO wolfCLU_OpenIo_fp(XFILE fp, enum WOLFCLU_IO_TYPE type) +{ + WOLFCLU_IO io = {WOLFCLU_IO_NOT_OPEN, XBADFILE}; +#ifndef WOLFCLU_NO_FILESYSTEM + if (fp == XBADFILE) { + wolfCLU_LogError("Bad file pointer passed to wolfCLU_OpenIo_fp"); + io.type = WOLFCLU_IO_ERROR; + } + + /* check that at least one valid type bit is set */ + if (!(type & WOLFCLU_IO_RW) || type <= 0) { + wolfCLU_LogError("Bad type passed to wolfCLU_OpenIo_fp"); + io.type = WOLFCLU_IO_ERROR; + } + + if (io.type == WOLFCLU_IO_NOT_OPEN) { + io.fp = fp; + io.type = type; +#ifdef _WIN32 + /* binary mode */ + (void)_setmode(_fileno(io.fp), _O_BINARY); +#endif + } + return io; +#else + (void)fp; + (void)type; + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); + io.type = WOLFCLU_IO_NOT_COMPILED_IN; + return io; +#endif +} + +WOLFCLU_IO wolfCLU_OpenIo_file(const char* fileName, enum WOLFCLU_IO_TYPE type) +{ + WOLFCLU_IO io = {WOLFCLU_IO_NOT_OPEN, XBADFILE}; +#ifndef WOLFCLU_NO_FILESYSTEM + XFILE fp = XBADFILE; + if (fileName == NULL || type <= 0) { + wolfCLU_LogError("Bad arg passed to wolfCLU_OpenIo_file"); + io.type = WOLFCLU_IO_ERROR; + return io; + } + + if ((type & WOLFCLU_IO_READABLE) && (type & WOLFCLU_IO_WRITABLE)) { + fp = XFOPEN(fileName, "rb+"); + } + else if (type & WOLFCLU_IO_READABLE) { + fp = XFOPEN(fileName, "rb"); + } + else if (type & WOLFCLU_IO_WRITABLE) { + fp = XFOPEN(fileName, "wb"); + } + else { + wolfCLU_LogError("Bad type passed to wolfCLU_OpenIo_file"); + io.type = WOLFCLU_IO_ERROR; + } + if (fp == XBADFILE) { + wolfCLU_LogError("Could not open file pointer from file name: %s", + fileName); + io.type = WOLFCLU_IO_ERROR; + } + + if (io.type == WOLFCLU_IO_NOT_OPEN) { + io.fp = fp; + io.type = type; +#ifdef _WIN32 + /* binary mode */ + (void)_setmode(_fileno(io.fp), _O_BINARY); +#endif + } + return io; +#else + (void)type; + (void)fileName; + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); + io.type = WOLFCLU_IO_NOT_COMPILED_IN; + return io; +#endif +} + +int wolfCLU_CloseIo(WOLFCLU_IO* io) +{ +#ifndef WOLFCLU_NO_FILESYSTEM + int ret = WOLFCLU_SUCCESS; + if (io == NULL || io->type <= 0) + return BAD_FUNC_ARG; + + /* the stream is gone even when fclose fails */ + if (!(io->type & WOLFCLU_IO_NOCLOSE) && XFCLOSE(io->fp) != 0) { + ret = WOLFCLU_FATAL_ERROR; + } + + io->fp = XBADFILE; + io->type = WOLFCLU_IO_NOT_OPEN; + + return ret; +#else + (void)io; + wolfCLU_LogError("There is no filesystem in this config: " + "cannot do file operations"); + return NOT_COMPILED_IN; +#endif +} diff --git a/tests/base64/base64-test.py b/tests/base64/base64-test.py index c955ac71..33e5ae18 100644 --- a/tests/base64/base64-test.py +++ b/tests/base64/base64-test.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 """Base64 encode/decode tests for wolfCLU.""" +import base64 import filecmp import os +import random import subprocess import sys import unittest @@ -12,6 +14,13 @@ from wolfclu_test import WOLFSSL_BIN, CERTS_DIR, run_wolfssl, test_main +def pem_lines(data): + """Base64 encode data as 64 character lines, the way wolfssl writes it.""" + encoded = base64.b64encode(data) + return b"".join(encoded[i:i + 64] + b"\n" + for i in range(0, len(encoded), 64)) + + class Base64Test(unittest.TestCase): @classmethod @@ -103,6 +112,360 @@ def test_stdin_input(self): self.assertEqual(result.returncode, 0, "Couldn't parse input from stdin") + def test_stdin_input_long(self): + """Encode 100,000 bytes from stdin, spanning several buffer grows.""" + data = random.Random(0).randbytes(100000) + + result = subprocess.run( + [WOLFSSL_BIN, "base64"], + input=data, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "stdin encode does not match python base64") + + def test_stdin_decode_long(self): + """Decode more than the old 8000 byte stdin limit.""" + data = random.Random(1).randbytes(100000) + + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-d"], + input=pem_lines(data), + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, data, + "stdin decode does not match the original data") + + @unittest.skipUnless(os.path.exists("/dev/stdin"), "needs /dev/stdin") + def test_pipe_input_file(self): + """-in on a pipe, which can't be sized with seek, reads like stdin.""" + data = random.Random(2).randbytes(5000) + + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-in", "/dev/stdin"], + input=data, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "pipe input encode does not match python base64") + + def test_empty_stdin(self): + """Empty stdin produces empty output, matching 'openssl base64'.""" + result = subprocess.run( + [WOLFSSL_BIN, "base64"], + input=b"", + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, b"", "empty input should give no output") + + def test_empty_stdin_decode(self): + """Empty stdin with -d produces empty output, like 'openssl base64'.""" + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-d"], + input=b"", + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, b"", "empty input should give no output") + + def test_empty_file(self): + """An empty input file produces an empty output file.""" + empty_file = "test-b64-empty.txt" + out_file = "test-b64-empty.b64" + self.addCleanup(lambda: os.remove(empty_file) + if os.path.exists(empty_file) else None) + self.addCleanup(lambda: os.remove(out_file) + if os.path.exists(out_file) else None) + + with open(empty_file, "wb"): + pass + + result = run_wolfssl("base64", "-in", empty_file, "-out", out_file) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(os.path.getsize(out_file), 0, + "empty input should give an empty output file") + + def test_missing_input_file(self): + """A missing -in file gives a non-zero exit.""" + result = run_wolfssl("base64", "-in", "test-b64-does-not-exist.bin") + self.assertNotEqual(result.returncode, 0, + "missing input file should fail") + self.assertNotIn("Could not close io", result.stderr, + "a failed open should not be closed") + + def test_output_dir_missing(self): + """An -out path in a missing directory gives a non-zero exit.""" + result = run_wolfssl("base64", "-in", + os.path.join(CERTS_DIR, "server-key.der"), + "-out", os.path.join("test-b64-no-such-dir", + "out.b64")) + self.assertNotEqual(result.returncode, 0, + "output in a missing directory should fail") + self.assertNotIn("Could not close io", result.stderr, + "a failed open should not be closed") + + def test_failed_decode_keeps_output(self): + """A failed -d does not create or truncate the -out file.""" + out_file = "test-b64-keep.txt" + self.addCleanup(lambda: os.remove(out_file) + if os.path.exists(out_file) else None) + + with open(out_file, "wb") as f: + f.write(b"keep") + + result = run_wolfssl("base64", "-d", "-out", out_file, + stdin_data="@@@@") + self.assertNotEqual(result.returncode, 0, "bad base64 should fail") + with open(out_file, "rb") as f: + self.assertEqual(f.read(), b"keep", + "failed decode should leave -out unchanged") + + def test_in_place(self): + """-in and -out naming the same file encodes it in place.""" + work_file = "test-b64-inplace.bin" + self.addCleanup(lambda: os.remove(work_file) + if os.path.exists(work_file) else None) + + with open(os.path.join(CERTS_DIR, "server-key.der"), "rb") as f: + original = f.read() + with open(work_file, "wb") as f: + f.write(original) + + result = run_wolfssl("base64", "-in", work_file, "-out", work_file) + self.assertEqual(result.returncode, 0, result.stderr) + with open(work_file, "rb") as f: + self.assertEqual(f.read().replace(b"\n", b""), + base64.b64encode(original), + "in place encode does not match python base64") + + @unittest.skipUnless(os.path.exists("/dev/full"), "needs /dev/full") + def test_stdout_write_error(self): + """A failed write to stdout gives a non-zero exit.""" + with open("/dev/full", "wb") as full: + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-in", + os.path.join(CERTS_DIR, "server-key.der")], + stdout=full, + stderr=subprocess.PIPE, + timeout=60, + ) + self.assertNotEqual(result.returncode, 0, + "write to a full device should fail") + + @unittest.skipUnless(os.path.exists("/dev/full"), "needs /dev/full") + def test_output_file_write_error(self): + """A failed write to the -out file gives a non-zero exit.""" + result = run_wolfssl("base64", "-in", + os.path.join(CERTS_DIR, "server-key.der"), + "-out", "/dev/full") + self.assertNotEqual(result.returncode, 0, + "write to a full device should fail") + + def test_stdin_buffer_boundaries(self): + """Stream reads at and around each buffer grow size (1024, 3072, 7168).""" + rng = random.Random(3) + for size in (1, 1023, 1024, 1025, 3071, 3072, 3073, 7168, 7169): + with self.subTest(size=size): + data = rng.randbytes(size) + result = subprocess.run( + [WOLFSSL_BIN, "base64"], + input=data, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "stdin encode does not match python base64") + + @unittest.skipUnless(os.path.exists("/dev/stdin"), "needs /dev/stdin") + def test_pipe_input_file_boundaries(self): + """-in on a pipe falls back to stream reads, including at grow sizes.""" + rng = random.Random(4) + for size in (1024, 3072, 7168): + with self.subTest(size=size): + data = rng.randbytes(size) + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-in", "/dev/stdin"], + input=data, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "pipe input encode does not match python base64") + + def test_stdin_from_regular_file(self): + """stdin redirected from a seekable file is read in full as a stream.""" + in_file = "test-b64-stdin-file.bin" + self.addCleanup(lambda: os.remove(in_file) + if os.path.exists(in_file) else None) + + data = random.Random(5).randbytes(20000) + with open(in_file, "wb") as f: + f.write(data) + + with open(in_file, "rb") as f: + result = subprocess.run( + [WOLFSSL_BIN, "base64"], + stdin=f, + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "redirected stdin encode does not match python base64") + + def test_binary_file_roundtrip(self): + """Every byte value, including NUL, CR and LF, survives -in/-out files.""" + in_file = "test-b64-binary.bin" + encoded_file = "test-b64-binary.b64" + decoded_file = "test-b64-binary.out" + for name in (in_file, encoded_file, decoded_file): + self.addCleanup(lambda n=name: os.remove(n) + if os.path.exists(n) else None) + + data = bytes(range(256)) * 40 + b"\r\n\r\n\x00\x1a" + with open(in_file, "wb") as f: + f.write(data) + + result = run_wolfssl("base64", "-in", in_file, "-out", encoded_file) + self.assertEqual(result.returncode, 0, result.stderr) + with open(encoded_file, "rb") as f: + self.assertEqual(f.read().replace(b"\n", b""), + base64.b64encode(data), + "file encode does not match python base64") + + result = run_wolfssl("base64", "-d", "-in", encoded_file, + "-out", decoded_file) + self.assertEqual(result.returncode, 0, result.stderr) + with open(decoded_file, "rb") as f: + self.assertEqual(f.read(), data, + "binary file round trip does not match") + + @unittest.skipUnless(os.path.exists("/dev/null"), "needs /dev/null") + def test_dev_null_input(self): + """-in on a device that reports a size of 0 gives empty output.""" + for args in (("base64",), ("base64", "-d")): + with self.subTest(args=args): + result = run_wolfssl(*args, "-in", "/dev/null") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "", + "empty device should give no output") + + @unittest.skipUnless(os.path.exists("/sys/class/net/lo/address"), + "needs /sys/class/net/lo/address") + def test_short_read_sysfs_file(self): + """-in on a sysfs file that reports a size larger than its data.""" + sysfs_file = "/sys/class/net/lo/address" + with open(sysfs_file, "rb") as f: + data = f.read() + + result = subprocess.run( + [WOLFSSL_BIN, "base64", "-in", sysfs_file], + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.replace(b"\n", b""), + base64.b64encode(data), + "sysfs file encode does not match python base64") + + def test_output_file_truncated(self): + """An existing -out file longer than the output is fully replaced.""" + in_file = "test-b64-trunc.bin" + out_file = "test-b64-trunc.b64" + for name in (in_file, out_file): + self.addCleanup(lambda n=name: os.remove(n) + if os.path.exists(n) else None) + + data = b"short" + with open(in_file, "wb") as f: + f.write(data) + with open(out_file, "wb") as f: + f.write(b"x" * 10000) + + result = run_wolfssl("base64", "-in", in_file, "-out", out_file) + self.assertEqual(result.returncode, 0, result.stderr) + with open(out_file, "rb") as f: + self.assertEqual(f.read().replace(b"\n", b""), + base64.b64encode(data), + "old -out contents were not truncated") + + def test_missing_input_file_no_output(self): + """A missing -in file does not create the -out file.""" + out_file = "test-b64-missing-in.b64" + self.addCleanup(lambda: os.remove(out_file) + if os.path.exists(out_file) else None) + + result = run_wolfssl("base64", "-in", "test-b64-does-not-exist.bin", + "-out", out_file) + self.assertNotEqual(result.returncode, 0, + "missing input file should fail") + self.assertNotIn("Could not close io", result.stderr, + "a failed open should not be closed") + self.assertFalse(os.path.exists(out_file), + "failed read should not create -out") + + def test_input_is_directory(self): + """-in naming a directory gives a non-zero exit.""" + in_dir = "test-b64-in-dir" + os.makedirs(in_dir, exist_ok=True) + self.addCleanup(lambda: os.rmdir(in_dir) + if os.path.isdir(in_dir) else None) + + result = run_wolfssl("base64", "-in", in_dir) + self.assertNotEqual(result.returncode, 0, + "directory as input should fail") + self.assertNotIn("Could not close io", result.stderr, + "closing a dir after a failed read should succeed") + + def test_output_is_directory(self): + """-out naming a directory gives a non-zero exit.""" + out_dir = "test-b64-out-dir" + os.makedirs(out_dir, exist_ok=True) + self.addCleanup(lambda: os.rmdir(out_dir) + if os.path.isdir(out_dir) else None) + + result = run_wolfssl("base64", "-in", + os.path.join(CERTS_DIR, "server-key.der"), + "-out", out_dir) + self.assertNotEqual(result.returncode, 0, + "directory as output should fail") + + @unittest.skipIf(not hasattr(os, "geteuid") or os.geteuid() == 0, + "needs a non-root POSIX user") + def test_unreadable_input_file(self): + """An -in file without read permission gives a non-zero exit.""" + in_file = "test-b64-unreadable.bin" + self.addCleanup(lambda: os.remove(in_file) + if os.path.exists(in_file) else None) + + with open(in_file, "wb") as f: + f.write(b"secret") + os.chmod(in_file, 0o200) + + result = run_wolfssl("base64", "-in", in_file) + self.assertNotEqual(result.returncode, 0, + "unreadable input file should fail") + self.assertNotIn("Could not close io", result.stderr, + "a failed open should not be closed") + self.assertEqual(result.stdout, "", + "unreadable input should give no output") + def test_help(self): """ Test help flag """ result = subprocess.run( diff --git a/wolfCLU.vcxproj b/wolfCLU.vcxproj index 8faba25e..306a81c3 100644 --- a/wolfCLU.vcxproj +++ b/wolfCLU.vcxproj @@ -177,6 +177,7 @@ + @@ -195,6 +196,7 @@ + diff --git a/wolfCLU.vcxproj.filters b/wolfCLU.vcxproj.filters index 9c43d2c1..a6030785 100644 --- a/wolfCLU.vcxproj.filters +++ b/wolfCLU.vcxproj.filters @@ -102,6 +102,9 @@ Source Files + + Source Files + Source Files @@ -164,6 +167,9 @@ Header Files + + Header Files + Header Files diff --git a/wolfclu/clu_io.h b/wolfclu/clu_io.h new file mode 100644 index 00000000..ca6635eb --- /dev/null +++ b/wolfclu/clu_io.h @@ -0,0 +1,131 @@ +/* clu_io.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifndef _WOLFSSL_CLU_IO_HEADER_ +#define _WOLFSSL_CLU_IO_HEADER_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +/** + * @brief IO types passed to the IO functions below + */ +enum WOLFCLU_IO_TYPE { + /*error codes*/ + WOLFCLU_IO_NOT_COMPILED_IN = NOT_COMPILED_IN, + WOLFCLU_IO_ERROR = WOLFCLU_FATAL_ERROR, + + /* base types */ + WOLFCLU_IO_NOT_OPEN = 0, + WOLFCLU_IO_READABLE_STREAM = 1 << 0, + WOLFCLU_IO_WRITABLE_STREAM = 1 << 1, + WOLFCLU_IO_READABLE_FILE = 1 << 2, + WOLFCLU_IO_WRITABLE_FILE = 1 << 3, + + /*behaviors*/ + WOLFCLU_IO_NOCLOSE = 1 << 10, + + /* type groups */ + WOLFCLU_IO_RW_STREAM = + WOLFCLU_IO_READABLE_STREAM | + WOLFCLU_IO_WRITABLE_STREAM, + + WOLFCLU_IO_RW_FILE = + WOLFCLU_IO_READABLE_FILE | + WOLFCLU_IO_WRITABLE_FILE, +}; + +#ifndef XFILE +#define XFILE void* +#endif + +#ifndef XBADFILE +#define XBADFILE NULL +#endif + + +typedef struct WOLFCLU_IO { + enum WOLFCLU_IO_TYPE type; + XFILE fp; +}WOLFCLU_IO; + +/** + * @brief Open a WOLFCLU_IO from a file descriptor, file pointer or file name. + * type sets the read/write mode and can include WOLFCLU_IO_NOCLOSE to + * leave the underlying file open on wolfCLU_CloseIo, e.g. for stdin + * @param fd file descriptor to open a file pointer on. After a successful open + * the fd is owned by the IO and closed by wolfCLU_CloseIo, on failure + * the caller still owns it + * @param fp file pointer to wrap, closed by wolfCLU_CloseIo unless + * WOLFCLU_IO_NOCLOSE is set + * @param fileName path of the file to open, writable files are truncated, + * read/write files are not truncated. + * @param type WOLFCLU_IO_TYPE flags for the IO + * @return an IO with type > 0 on success. On failure type is <= 0 + * (WOLFCLU_IO_ERROR or WOLFCLU_IO_NOT_COMPILED_IN) and the IO must not + * be passed to wolfCLU_CloseIo + */ +WOLFCLU_IO wolfCLU_OpenIo_fd(int fd, enum WOLFCLU_IO_TYPE type); +WOLFCLU_IO wolfCLU_OpenIo_fp(XFILE fp, enum WOLFCLU_IO_TYPE type); +WOLFCLU_IO wolfCLU_OpenIo_file(const char* fileName, enum WOLFCLU_IO_TYPE type); + +/** + * @brief close an open IO. The file is closed unless WOLFCLU_IO_NOCLOSE is + * set. The IO is reset to WOLFCLU_IO_NOT_OPEN even if the close fails + * @param io IO from a successful wolfCLU_OpenIo_* call + * @return WOLFCLU_SUCCESS on success, BAD_FUNC_ARG if io is not open, + * WOLFCLU_FATAL_ERROR if closing the file failed + */ +int wolfCLU_CloseIo(WOLFCLU_IO* io); + +/** + * @brief read file/stream data in to an allocated buffer. If limit is + * 0 it will default to UINT_MAX otherwise it will be the max number of + * bytes read from the file/stream. + * @param io open IO with a readable type + * @param buf pointer to store the buffer, NULL if empty. The caller frees it, + * wiping it first with wolfCLU_ForceZero if it holds secrets + * @param len pointer to store the number of bytes read + * @param limit maximum number of bytes that we want to read from a + * file or stream; 0 defaults to UINT_MAX bound + * @return WOLFCLU_SUCCESS on success, negative on error + */ +int wolfCLU_ReadIo(WOLFCLU_IO* io, byte** buf, word32* len, word32 limit); + +/** + * @brief write len bytes of buf to io and flush it + * @param io open IO with a writable type + * @param buf buffer to write, may be NULL when len is 0 + * @param len number of bytes to write + * @return WOLFCLU_SUCCESS on success, negative on error + */ +int wolfCLU_WriteIo(WOLFCLU_IO* io, const byte* buf, word32 len); + +#ifdef __cplusplus +} +#endif + +#endif /*_WOLFSSL_CLU_IO_HEADER_*/ diff --git a/wolfclu/include.am b/wolfclu/include.am index ffd25213..bc9c4559 100644 --- a/wolfclu/include.am +++ b/wolfclu/include.am @@ -10,6 +10,7 @@ nobase_include_HEADERS+=wolfclu/version.h \ wolfclu/clu_optargs.h \ wolfclu/clu_log.h \ wolfclu/clu_error_codes.h \ + wolfclu/clu_io.h \ wolfclu/x509/clu_cert.h \ wolfclu/x509/clu_parse.h \ wolfclu/x509/clu_request.h \