Source and Sink in Crypto++
This post builds upon A Brief Introduction to
Crypto++. I suggest to have a quick look at it before
diving into Source
and Sink
classes.
Sources and sinks are quite similar, so we will review them together. Just a refresher: a source provides an input for a Crypto++ pipeline, while a sink terminates it by saving the result to an output. The input and output of a pipeline are usually data containers (e.g., arrays, vectors, strings) or files.
Sources
Sources implement the Source
base class. The following implementations exist:
FileSource
StringSource
-
ArraySource
(which is a typedef ofStringSource
) VectorSource
RandomNumberSource
StringSource
and VectorSource
write the result to the corresponding STL containers. Their
constructor parameters are self-explanatory as long as you follow Crypto++'s convention: a pointer
means 'pass ownership,' while a reference means 'retain ownership.'
StringSource
has a constructor that accepts a pointer to a byte array:
StringSource (const byte *string, size_t length, bool pumpAll, BufferedTransformation *attachment=NULL)
In Crypto++, byte
is a typedef for unsigned char
, so StringSource
(and ArraySource
) can work
with either a std::string
or an unsigned char
array.
FileSource
gets input data from a file (an instance of std::ifstream
). RandomNumberSource
uses
an RNG (random number generator) to generate input data, but I'll leave this for Chapter 6, which is
dedicated to RNGs.
Sinks
Sinks implement the Sink
base class. Their implementations are similar to the Source
ones:
-
StringSink
- writes data to astd::string
or an array. -
VectorSink
- writes data to astd::vector
. -
RandomNumberSink
- adds additional entropy to an RNG (more on RNGs in Chapter 6). -
ArraySink
- unlike itsArraySource
counterpart, it has a separate implementation. It uses a fixed-size buffer to write to. Once the buffer is full,ArraySink
silently discards any additional data. -
BitBucket
- swallows and discards everything passed to it. -
FileSink
- writes to a file (an instance ofstd::ostream
).
Each Sink
also implements BufferedTransformation
, allowing it to be used to terminate pipelines,
as we will see in a moment.
You should keep in mind that sinks writing to STL containers will resize them as needed. This behavior might lead to unexpected memory allocations. To avoid the performance penalty associated with such allocations, it is a good practice to preallocate sufficient memory for the output buffer.
Code Sample - StringSource
and VectorSink
Let's see some examples. The first one reads a std::string
with StringSource
and saves it to a
std::vector
with a VectorSink
:
#include <iostream> #include <string> #include <vector> #include <cryptopp/cryptlib.h> #include <cryptopp/filters.h> int main() { const std::string input{"INPUT"}; std::vector<uint8_t> output; (void)CryptoPP::StringSource(input, true, new CryptoPP::VectorSink(output)); if (output != std::vector<uint8_t>({73, 78, 80, 85, 84})) { std::cout << "Output should match input" << std::endl; } return 0; }
The signature of the StringSource
constructor is:
StringSource( const std::string &string, bool pumpAll, BufferedTransformation *attachment = NULLPTR )
The first parameter is a reference to the input string. There are overrides accepting const byte*
(aka const unsigned char*
) and const char*
pointers. It's important to know that Source
implementations doesn't take ownership over any of these const
pointers. Remember that in A Brief
Introduction to Crypto++ we said that Crypto++ classes
don't take ownership over pointers to primitive types such as char
and int
. So in this case if
the input was allocated dynamically it's our responsibility to handle the memory deallocation.
The pumpAll
parameter is a boolean indicating whether all the input should be pumped to the
output. Usually, you want this parameter set to true
unless you have a very large input or want
manual flow control. The final parameter (attachment
) is a pointer to a BufferedTransformation
instance, which in our case is VectorSink
.
Usually, BufferedTransformation
is implemented for filters, but in order to terminate a Crypto++
pipeline with a sink, the Sink
base class also implements BufferedTransformation
. In other
words, anywhere you can put a filter, you can also put a sink to terminate the pipeline.
Now let's see how the final element of the pipeline, the VectorSink
, is initialized. VectorSink
is actually a typedef:
DOCUMENTED_TYPEDEF(StringSinkTemplate<std::vector<byte> >, VectorSink);
It has a simple constructor:
StringSinkTemplate(T &output)
Note that T
in our case is std::vector<byte>
. So VectorSink
accepts a reference to a
std::vector<unsigned char>
. Sinks usually don't take ownership of their inputs because you want to
use them after the pipeline finishes. Consuming them would leave you empty-handed.
Also note that VectorSink
will
resize
the output buffer if it is not big enough so again - it's a good practice to preallocate enough
memory for the outputs to avoid performance penalties from unnecessary memory allocations.
Code Sample - FileSource
and FileSink
Let's have a look at another example involving files:
#include <iostream> #include <string> #include <vector> #include <cryptopp/cryptlib.h> #include <cryptopp/files.h> #include <cryptopp/filters.h> int main() { const std::string input{"INPUT"}; std::vector<uint8_t> result; { auto output = std::ofstream("/tmp/test-file", std::ios::binary); (void)CryptoPP::StringSource(input, true, new CryptoPP::FileSink(output)); } { auto input = std::ifstream("/tmp/test-file", std::ios::binary); (void)CryptoPP::FileSource(input, true, new CryptoPP::VectorSink(result)); } if (result != std::vector<uint8_t>({73, 78, 80, 85, 84})) { std::cout << "Output should match input" << std::endl; } return 0; }
This sample contains two pipelines. The first one reads a std::string
with a StringSource
and
saves it to a file with a FileSink
. The second one reads the same file with a FileSource
and
saves it to a VectorSink
. We already saw how StringSource
and VectorSink
work, so in this
sample, we'll focus on the file source/sink.
The FileSink
constructor looks like this:
FileSink(std::ostream &out)
Note that FileSink
doesn't take ownership of the std::ostream
object, so it is our
responsibility to deallocate it (if needed) and more importantly to close the output file. That's
why each pipeline is enclosed in a separate scope. std::ostream
closes the file on deallocation,
and we want the file closed before the second pipeline starts.
FileSource
's constructor is quite similar to StringSource
's:
FileSource(std::istream &in, bool pumpAll, BufferedTransformation *attachment = NULLPTR)
We still have attachment
and pumpAll
parameters, but the input parameter here is a reference to
std::istream
. Again, closing the file is our responsibility, which is why this pipeline is in a
separate scope as well.
Conclusion
Source
and Sink
are fundamental elements of a Crypto++ pipeline since they start and terminate
it. Crypto++ provides implementations that work with std::string
, std::vector
, byte arrays, and
files. If you need something more complex (e.g., a source reading from a network socket), you can
implement your own Source
or Sink
, but this is beyond the scope of this book.
Each Sink
is also a filter since they both implement BufferedTransformation
. This makes sense
because wherever you can plug in a filter, it should also be possible to terminate the pipeline with
a Sink
.
The book
This post is an excerpt from my book "Brief Introduction to Crypto++". The book should be published by the end of May 2025. You can learn more about the book in this post.
Use the form below to join my newsteller if you want to receive occasional updates about the book and other stuff I find interesting:
Comments
Comments powered by Disqus