The source code for
this project can be accessed here.
This blog post is a bit of a doozy. If massive walls of text
aren’t your thing, check out the source code and refer to the text below if
there’s anything you need clarification on.
Context
Recently I was involved in a bit of a
debacle that
made rounds on hacker news circles about the computer security class I was
enrolled in. The course was over-enrolled and under-staffed, and the
professor’s response was to enforce a series of hilariously draconian
rules until they could boot enough people out. Late to class once?
Dropped. Cell phone goes off? Dropped. Miss a discussion section? Dropped.
Threats were made about the difficulty of the course, how it would be extremely
math-heavy and require 40+ hours of work a week. Within 3 days over 50 students
had dropped, with a huge amount of anger coming from the computer science
student base.
As it turns out, he was totally lying. As much as I would
love to say I called his bluff, I stuck around mainly because I had no choice
and as of now, it’s turning out to be one of the easiest classes I’ve taken.
It’s funny how now I’m actually giving myself more work for the class I thought
I would be consuming my life.
The class is pretty interesting, but so far completely
conceptual. To try and get a better understanding of the ciphers and protocols that
I’m learning, I decided to try and implement them in code as we went along. I was
pretty eager to try out the
Vernam Cipher, but I realized that there was
no intuitive way to use
cryptographic keys in the context of Java. Keys
are of arbitrary length, and are best streamed bit-by-bit. Java primitive data
types wouldn’t really work.
Java does provide a SecretKey interface, but it’s pretty
useless as well considering it does nothing
other than provide type safety for whatever you choose to use as your
cryptographic key. I decided that if I was going to be implementing these
ciphers and dealing with keys, I would need a fluid method for doing so, and
that I would have to make it myself.
One course of action would have been to make a specific CrytographicKey class, but the issues that make Java primitives inappropriate
actually apply to a lot of things out there. We’re used to bit-strings
representing numbers, but very often they represent something completely
different. For example, suppose you have some set of a finite number of
distinct elements, and a subset of that set. One way to describe that subset uniquely
is by assigning to it a bit-string, where each bit corresponds to an element in
the superset. If the bit is a 0, that element is not in the subset; if the bit
is 1, it is. In this case, the length of the bit-string can be anything, and
each bit has meaning.
So, instead of a specific CryptographicKey class, the goal is
to make a class that represents an arbitrary-length bit-string, which can be
streamed bit by bit. Once that’s finished, it would be pretty easy to use the
class as a cryptographic key.
Getting Started
The class needs to be able to represent a bit-string of any length. You should be able to
specify every bit of the bit-string, and there should be fluid conversion
between the Java numeric primitives. The class should be able to represent both
positive and negative numbers, using a 2s complement signing scheme (the
standard for Java). The class is meant to simply represent a number, so the
complexity of the inner-workings shouldn’t be apparent to an outside user. Plus,
this all needs to be done with as little memory usage as possible.
To begin, we have to design the way in which the bits are
stored. We know the bits are ordered, and that each bit is critical, so an array
of some sort sounds good. It’s tempting to immediately start looking into an
ArrayList,
LinkedList, or other data structure, but in this case the length and
contents of the array will be set at instantiation, so the default Java array works
just fine.
What should we put in the array? An array of bits would
definitely give us the control we need, but even if that were possible to
implement it would be enormously tedious for the user. Bytes are a good option,
as they let you to specify batches of 8 bits at a time; plus, it would minimize
the amount of overhead that occurs as a result of extra bits that we’ll have if
the length of the bit-string is not a multiple of our building block.
Let's expand that last point a bit more. Since our building block is not atomic, what happens when
the length of our bit-string is not a multiple of 8 (the size of a byte)? What
would 10111 look like? Well, there’s no way to get around using the full 8 bits
of a byte to represent this (that's what our array is storing), so we’re forced to pad it to 00010111. But what
happens if we now want to represent 010111? You can’t pad it to 00010111, that
was the padding of the last number and each representation needs to be
distinct. Every bit counts, after all.
The solution is pretty much what you expect; we create a
variable that stores the length of the bit-string, and use that to determine
where to stop streaming bits.
There’s one more small technical detail to cover before
jumping into the specifics of the code. The array indexing is
big-endian. This means that the
first byte in the array actually corresponds to the most significant bytes of
the integer. While this may be unintuitive, it makes streaming the bits
slightly easier. It does add complications elsewhere, so in retrospect little-endian ordering would work equally well, if not slightly better.
Since we'll want the user to specify an arbitrary number of fixed bytes at instantiation, we'll call this class FixedByteInteger.
Constructing the
Constructors
So far we’ve established two member variables: one that
stores the length (mLength), and one that stores the bytes (mBytes). For good
measure, we’ll also include the number of bytes needed by a particular
FixedByteInteger (mNumBytes), which is usually given by:
Now, we have to work on designing constructors to set these
values. The most obvious one is a constructor that takes an argument of an
array of bytes. Then, we can simply set our bytes to be equal to the bytes of
the argument. This is where the user is given full control over the contents of the bit-string. A first attempt at the constructor might look something like this:
Unfortunately, it isn’t quite that simple. What if the user wants
a bit-string length that isn’t a multiple of 8? A user needs the power of
specifying when to cut off the bit-stream. Fortunately, the fix is pretty easy.
For now, we trust that whatever is going to stream the bits will do so
correctly as long as it knows the length of the bit-string. Thus, we can write
the following:
We use abs(length) to avoid dealing with the negative length
case. What’s up with the last two lines in the first constructor though? Later, when
we calculate the numeric value of the FixedByteInteger based on its bit-string,
it will make our lives much easier if we sign-extend the top byte.
Like I said previously, one goal we have is to make the
process of converting between Java primitive values and FixedByteIntegers fluid
and painless for the user. As such, one of our constructors will take a numeric
argument, and construct the bytes of the FixedByteInteger based on that value. We
will only have one such constructor that takes a long. Ideally, we would have
multiple constructors to avoid casting, but any cast to a long (except for
floats and doubles, which are not integers anyway and therefore should not be
accepted as input values) will never result in a loss of information, so we
won’t worry about it for now.
We want to be efficient with our memory. Just because a long
takes up 64 bits, doesn’t mean we have to. Say we have the input 32. How many
bytes are needed to represent that? If we preserved the same data structure as
a long, it would take 8 bytes, when in fact it only takes 1 byte to represent
it: 00100000. Therefore, we need to develop a method to find the absolute
minimum number of bytes to represent the input value.
The method for doing this actually depends on whether or not
the value is negative. Let’s consider the positive case first. In a 2s complement
scheme, it is guaranteed that there will be leading zeroes. By chopping them
off, we can usually reduce the number of bytes in our array that encode the
value of in the input long. Therefore, we scan (remember: big-endian!) the
input until we find the first 1, and set mLength to where that 1 lies.
Be cautious. What about when input = 0x00000080? This value
represents 8 in decimal. However, using the scheme described above, that value would
be reduced to a single byte: 0x80. This value does not represent 8; it represents -128. We have to add a special case
where the leading one is byte-aligned when the value is positive, in order to
avoid this bug. You can see this case in the code further below, however we essentially
just append a 0 to the left of the leading 1 if that case occurs.
What about when the input long is negative? A different case
is needed because if the method above is applied to a negative input, the
number of bytes used will always be same as the number of bytes in a long,
since a negative long always has a leading 1. For example, how many bytes are
needed to represent -1? As a long, -1 would be 111…1, 8 bytes of 1s. In fact, we only
need a single byte to represent -1: 11111111. The same applies for any negative number. It
is likely that a negative value encoded as a long has unnecessary leading 1s.
In order to deal with this, we treat it the same way we
would treat leading 0s. Simply scan through the long until the first 0 is
encountered, and shear all bits to the left of the 1 that occurs before it.
In the negative case, special consideration of byte-aligned
inputs is not needed. I won’t get into the specifics of the code, but our
constructor ends up looking like this:
Converting it Back
Since FixedByteInteger can represent a number, it’s logical
to have it extend the Number class, which every
numeric primitive wrapper also extends. Therefore, it is required that we implement the abstract methods
floatValue(), doubleValue(), intValue(), and longValue(), which do exactly what
they sound like they do. First off, it isn’t guaranteed that these values will
be accurate. For example, if the bit length of a particular FixedByteInteger is
greater than 64 bits, it cannot possibly be represented by a long. Therefore,
we cannot guarantee that the value will be accurate. This is reflected in the
documentation. Secondly, we’ll only worry about doubleValue() and longValue().
floatValue() and intValue() can be implemented by simply calling their
respective promoted value methods and then casting the result down (check the
source code to see this in action).
Let’s tackle longValue() first. This method is much more
straightforward than doubleValue(), since we can literally build the result bit
by bit. Again, I don’t want to get into the details of the code, but this is
what we arrive at:
The first step is particularly important to understand. Keep
in mind that we sign-extended the first byte earlier, so checking the top bit will always tell us the sign of the bit-string. Also, we got to use our
ZeroExtender from the last post! Why did we need it?
Now we have to tackle the doubleValue() method. This is much
less straightforward, since doubles are
not represented as a literal base-2
bit-string, and representing negative doubles is an entirely different
ball game. A fortunate turn of events is that the range of negative values is
exactly the same as the range of positive values for a double (this is not the
case for byte, short, int or long). This will also come into play.
Like before, we’ll consider the case where our FixedByteInteger
is positive first. This case is also pretty straightforward: for every index i
that corresponds to a 1 in our bit-string, we add 2^i to the result, and return
the result after all 1s in the bit-string are found.
The negative case is far less straightforward. As mentioned
before, the range of negatives for a double is the same as the range of
positives. Therefore, we can convert our negative FixedByteInteger to a
positive, calculate the result the same way as the positive case, and then
negate it before returning.
The conversion is the hard part. Since the bytes are
represented as a 2s complement scheme, the conversion occurs by flipping all
bits, and adding 1. However, our bit-string is split into bytes. Therefore, we
have to flip the bytes individually, and carry the additional 1 in between consecutive
bytes in the case that the addition results in an overflow. After a lot of tribulation, we end up with
this. If you aren’t completely comfortable with bit-twiddling, I encourage you
to try and fully understand the following code:
All that’s left is to create the class responsible for
streaming the bits. This is pretty straightforward, so I won’t discuss it here.
Review the source code linked at the beginning of this post to see how it
works (it's defined as a nested class at the end of FixedByteInteger).
Closing Remarks
When should this class be used? It’s tempting think this
class is appropriate for large-value bit-streams. However, this should only be
the case when the integer value of the bit stream is not relevant. The float and double primitives offer a fantastic
interface for encoding large-value numbers, and this class offers almost no
advantage over them unless very high precision is needed. Instead, this class
is best used when the number of bits needed is within the general range of
those offered by Java primitives, but the length of the bit-string has to be
exact. As per our motivating example, this class would be extremely well suited
for a cryptographic key, which can take on any length but generally has fewer
than 256 bits.
It should also be noted that any arithmetic operation on a
FixedByteInteger has to be done bit by bit through the provided bit-stream
class. This is an unfortunate result of Java arithmetic operators (+, -, *, /,
etc.) being strictly defined. As a result, you cannot define what
FixedByteInteger + FixedByteInteger actually means. This heavily limits the usefulness
of this class, however the utility would be vastly improved in languages where
such operations can be defined on arbitrary data types. None of the concepts
used were specific to Java, so translating the source code to another language
shouldn’t be too hard.
That’s all I’m going to post about bit-twiddling for now. It’s
a powerful concept that is extremely important to understand, and hopefully
these last two posts have provided you with some familiarity with how it can be applied. Stay tuned for string reversals and sorting,
and thanks for stopping by.