Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
This module provides an interface to the following function. This simplified
presentation elides the fact that the variable-length padding between the
args
parameter and the initial counter depends on the tag itself.
phkdfStream :: BitString -> [BitString] -> Word32 -> BitString -> Stream ByteString phkdfStream key args counter tag = [output0, output1 ..] where output0 = hmac key (encode args ++ encode counter ++ tag) output1 = hmac key (output0 ++ encode (counter + 1) ++ tag) output2 = hmac key (output1 ++ encode (counter + 2) ++ tag) ...
This hash function exhibits a misleading resemblance to HKDF, with the key
corresponding to HKDF's salt
, the msgs
parameter corresponding to HKDF's
ikm
(initial keying material), and the counter
and tag
parameters
corresponding to HKDF's info parameter.
hkdf :: BitString -> BitString -> ByteString -> [ByteString] hkdf salt ikm info = [output1, output2 .. output255] where key = hmac salt ikm output1 = hmac key (info ++ encodeWord8 1) output2 = hmac key (output1 ++ info ++ encodeWord8 2) output3 = hmac key (output2 ++ info ++ encodeWord8 3) ...
However this is a false cognate. The first thing to notice about phkdfStream
is that it doesn't matter how secure the args
parameter is, if you use a
publicly known key, counter, and tag, then revealing a full output block reveals
the remainder of the output stream.
This is in contrast to hkdf
, which allows secret initial keying material and
publicly-known salt and info parameters to be expanded into a large number of
output blocks. These blocks can be divvied up into non-overlapping pieces that
may be revealed independently of each other.
Thus phkdfStream
is actually a much lower-level hash function than hkdf
. As
such has it's own modes of operation, which provide various different answers
for this issue of output stream predictability. Building a proper replacement
for hkdf
requires combining two or more calls to phkdfStream
in different
modes of operation.
The first and simplest mode of operation for phkdfStream
is to simply discard
all but the first output block. In this case, phkdfStream
simplifies to a call
to HMAC with the addition of TupleHash style encoding, and custom end-of-message
padding determined by the counter and tag. Thus we can use this mode to
implement the key extraction portion of an HKDF-like hash function.
In this mode of operation, we can safely use phkdfStream
with secret initial
keying materials and optionally non-secret salt, counter, and tag, and possibly
even reveal the output. After all it doesn't matter if anybody can predict the
remainder of the stream if it's never been granted any meaning.
The second mode of operation is to use phkdfStream
with a secret key,
non-secret arguments, and optionally secret counter and tag. In this mode, we
can reveal arbitrary non-overlapping portions of the output stream to third
parties, without worry that one portion can be derived from another.
Thus we can implement a variant of the HKDF construction using these two modes of operation in conjunction with each other:
hkdfSimple :: BitString -> [BitString] -> BitString -> Stream ByteString hkdfSimple salt ikms tag = out where key = head $ phkdfStream salt ikms inCtr tag out = phkdfStream key echoArgs outCtr tag echoArgs = ["hkdf-simple"] inCtr = word32 "INx00x00" outCtr = word32 "OUTx00"
If the recommendations of NIST SP 800-108 are to be followed strictly, one
shouldn't examine more than 2^32 output blocks which is about 137.4 GB of
output from hkdfSimple
. I don't think this will be a problem in practice,
as this particular CSPRNG is not overly well suited to generating large amounts
of pseudorandom data.
However, we must be aware of the echo args gotcha: for reasons intimately
related to the predictability of phkdfStream
with a non-secret key, counter,
and tag, the echoArgs
parameter must not include any important new secrets.
This time we are deriving a secret key using initial keying material. However,
if that material is potentially guessable, then introducing a high-entropy
secret in the echoArgs
parameter will secure the first output block, but
revealing two output blocks would re-reveal the ability to guess the original
keying material.
Thus all secrets should be included in the derivation of the key, or possibly included in the tag parameter. A secret counter can also help, but cannot provide a sufficient level of entropy tmo secure the output all by itself.
One of HKDF's design principles was to obtain a clean seperation between the extraction and expansion phases. This seperation allows HKDF's design to avoid the echo args gotcha by specifying that the echo args is the empty string.
In a literal, low-level sense, phkdfStream
intentionally violates this
seperation. In a metaphorical, higher-level sense, phkdf
affirms this design
principle, rather phkdf
s
goal is to allow a single primitive to serve both
roles. This unification makes it easy to create cryptographic hash protocols
where every call to HMAC is covered by a directly self-documenting plaintext tag.
Moreover, the alternative to PBKDF2 is phkdf's slow extraction function, which makes crucial use of the echo args gotcha. This brings us to the third mode of operation, which keeps the output stream secret, except possibly for the very last output block examined.
Each mode of operation provides an answer to the predictability of phkdfStream
.
Our first answer is to make it irrelevant that the output stream is predictable.
Our second answer achieves unpredictability by using a key, counter, and/or tag
that is secret. The third answer achieves unpredictability by keeping the output
stream secret, allowing a publicly-known key, counter, and tag to be used as
self-documenting domain seperation constants.
Thus phkdf's slow extraction function calls phkdfStream
to generate a stream
that is allowed to be predictable, but at an unpredictable starting point. This
predictable stream remains secret, and is immediately consumed by a second call
to phkdfStream
. After rounds + 1
blocks have been produced and consumed, the
second call to phkdfStream
has an opportunity to add some additional
post-key-stretching tweaks before the output stream is finalized.
Conceptually, the slow extraction function looks like this:
phkdfSlowExtract :: BitString -> [BitString] -> Word32 -> BitString -> ByteString -> Word32 -> [BitString] -> Stream ByteString phkdfSlowExtract key args counter tag fnName rounds tweaks = out where blocks = take (rounds + 1) $ phkdfStream key args counter tag header = [makePadding fnName rounds, makeLongString tag blocks] out = phkdfStream key (header ++ tweaks) (counter + rounds + 1) tag
Compared to PBKDF2, phkdfSlowExtract
uses essentially the same stream
generator, but enhanced with counters and contextual parameters. PBKDF2 proper
then condenses that stream by xor-ing all the output blocks together.
phkdfSlowExtract
condenses it's internal stream by feeding it to another call
to HMAC. So phkdfSlowExtract
is very likely at least as strong as PBKDF2.
Again, assuming key, counter, rounds, and tag are all publicly known, which is
the primary intended use case of this function, then the output stream is
predictable. Thus the output of phkdfSlowExtract
must itself be subjected to
the first or third mode of operation.
If more than 32 bytes ever need to be revealed, then another call to
phkdfStream
with a secret key in the second mode of operation is required
for final output expansion. We do just this in our next example.
phkdfVerySimple
uses our flavor of not-quite-PBKDF2 to produce a pseudorandom
key to use with our flavor of not-quite-HKDF for final output expansion. Thus
the algorithm behind this construction is a portmanteau of the algorithms behind
PBKDF2 and HKDF. Thus the name.
phkdfVerySimple :: BitString -> BitString -> BitString -> BitString -> Word32 -> Stream ByteString phkdfVerySimple seguid tag username password rounds = out where inArgs = [myLabel, username, password, encode rounds] key = head $ phkdfSlowExtract seguid inArgs inCtr tag myLabel rounds [] out = phkdfStream key [myLabel] outCtr tag myLabel = "phkdf-very-simple" inCtr = word32 "INx00x00" outCtr = word32 "OUTx00"
phkdfVerySimple
is a distillation of the core features of the phkdfSimple
function exported from the Crypto.PHKDF
module, containing the most salient
features of that more fully worked construction.
Not only does phkdfVerySimple
provide key stretching very similar in flavor
to PBKDF2, but it also infuses the entire key-stretching process with
cryptoacoustic repetitions of the plaintext of the tag. This amplifies the
minimum obfuscation overhead associated with any tag obscuration attack that is
truly secure against the best reverse engineers. This in turns reduces the
minimum obfuscation overhead associated with a single application of SHA256
in order for the overall construction to be cryptoacoustically viable.
phkdfVerySimple
encodes the number of rounds to be performed in the
key-stretching phase in order to ensure that changing the number of rounds
requires a full key-stretching recomputation. This is necessary because it is
possible to share portions of phkdfSlowExtract
's key-stretching computation
when the rounds
parameter is varied while holding the input arguments
constant. Including an encoding of the rounds
parameter in the input arguments
forces both to be varied, thus forcing a full recomputation.
Synopsis
- data HmacKey
- hmacKey_init :: ByteString -> HmacKey
- data PhkdfCtx
- phkdfCtx_init :: ByteString -> PhkdfCtx
- phkdfCtx_initFromHmacKey :: HmacKey -> PhkdfCtx
- phkdfCtx_hmacKey :: PhkdfCtx -> HmacKey
- phkdfCtx_resetCtx :: PhkdfCtx -> HmacCtx
- phkdfCtx_reset :: PhkdfCtx -> PhkdfCtx
- phkdfCtx_addArg :: ByteString -> PhkdfCtx -> PhkdfCtx
- phkdfCtx_addArgs :: Foldable f => f ByteString -> PhkdfCtx -> PhkdfCtx
- phkdfCtx_addArgsBy :: Foldable f => (a -> ByteString) -> f a -> PhkdfCtx -> PhkdfCtx
- phkdfCtx_finalize :: (Int -> ByteString) -> Word32 -> ByteString -> PhkdfCtx -> ByteString
- phkdfCtx_finalizeHmac :: PhkdfCtx -> ByteString
- phkdfCtx_finalizeHmacCtx :: PhkdfCtx -> HmacCtx
- phkdfCtx_finalizeStream :: (Int -> ByteString) -> Word32 -> ByteString -> PhkdfCtx -> Stream ByteString
- phkdfCtx_finalizeGen :: (Int -> ByteString) -> Word32 -> ByteString -> PhkdfCtx -> PhkdfGen
- data PhkdfSlowCtx
- phkdfSlowCtx_extract :: (Int -> ByteString) -> Word32 -> ByteString -> ByteString -> Word32 -> PhkdfCtx -> PhkdfSlowCtx
- phkdfSlowCtx_addArg :: ByteString -> PhkdfSlowCtx -> PhkdfSlowCtx
- phkdfSlowCtx_addArgs :: Foldable f => f ByteString -> PhkdfSlowCtx -> PhkdfSlowCtx
- phkdfSlowCtx_finalize :: (Int -> ByteString) -> PhkdfSlowCtx -> ByteString
- phkdfSlowCtx_finalizeStream :: (Int -> ByteString) -> PhkdfSlowCtx -> Stream ByteString
- data PhkdfGen
- phkdfGen_initFromHmacKey :: ByteString -> Word32 -> ByteString -> HmacKey -> PhkdfGen
- phkdfGen_read :: PhkdfGen -> (ByteString, PhkdfGen)
- phkdfGen_peek :: PhkdfGen -> Maybe ByteString
- phkdfGen_finalizeStream :: PhkdfGen -> Stream ByteString
Documentation
A precomputed HMAC key. Computing an HMAC key costs two SHA256 blocks.
No additional blocks are incurred for keys that are 64 bytes or less in length. Keys that are longer than 64 bytes long must be first hashed with SHA256 before the key can be derived, incurring extra blocks.
It is not uncommon that implementations of PBKDF2, HKDF, etc unnecessarily redo this computation even though a single HMAC key is used repeatedly.
TODO: FIXME: this data structure is way larger than it should be. We can pack this into a single 64-byte bytestring, but right now it's 208 bytes of data plus extra overhead.
On the other hand, this approach may actually be more efficient for the core PHKDF algorithm as currently implemented. Reducing the size of this data structure while maintaining tight code involves some additional work on cryptohash-sha256
hmacKey_init :: ByteString -> HmacKey Source #
Precompute an HMAC key for some literal HMAC key.
phkdfCtx_init :: ByteString -> PhkdfCtx Source #
initialize an empty phkdfStream
context from a plaintext HMAC key.
phkdfCtx_initFromHmacKey :: HmacKey -> PhkdfCtx Source #
initialize an empty phkdfStream
context from a precomputed HMAC key.
phkdfCtx_hmacKey :: PhkdfCtx -> HmacKey Source #
phkdfCtx_resetCtx :: PhkdfCtx -> HmacCtx Source #
initialize a new empty HMAC context from the key originally supplied to the PHKDF context, discarding all arguments already added.
phkdfCtx_reset :: PhkdfCtx -> PhkdfCtx Source #
initialize a new empty phkdfStream
context from the HMAC key
originally supplied to the context, discarding all arguments already added.
phkdfCtx_addArg :: ByteString -> PhkdfCtx -> PhkdfCtx Source #
append a single string onto the end of phkdfStream
's list of
arguments.
phkdfCtx_addArgs :: Foldable f => f ByteString -> PhkdfCtx -> PhkdfCtx Source #
append zero or more strings onto the end of phkdfStream
's list of
arguments.
phkdfCtx_addArgsBy :: Foldable f => (a -> ByteString) -> f a -> PhkdfCtx -> PhkdfCtx Source #
phkdfCtx_finalize :: (Int -> ByteString) -> Word32 -> ByteString -> PhkdfCtx -> ByteString Source #
close out a phkdfStream
context using the first mode of operation,
examining only the first output block and discarding the rest of the
stream.
phkdfCtx_finalizeHmac :: PhkdfCtx -> ByteString Source #
"improperly" close out a PhkdfCtx
as if it were a call to hmac
instead
of phkdfStream
, though with a TupleHash message encoding.
phkdfCtx_finalizeHmacCtx :: PhkdfCtx -> HmacCtx Source #
Turn a PhkdfCtx
into a incomplete call to hmac
, with the option of
adding additional data to the end of the message that need not be
TupleHash encoded.
phkdfCtx_finalizeStream :: (Int -> ByteString) -> Word32 -> ByteString -> PhkdfCtx -> Stream ByteString Source #
close out a phkdfStream
context with a given counter and tag
phkdfCtx_finalizeGen :: (Int -> ByteString) -> Word32 -> ByteString -> PhkdfCtx -> PhkdfGen Source #
data PhkdfSlowCtx Source #
phkdfSlowCtx_extract :: (Int -> ByteString) -> Word32 -> ByteString -> ByteString -> Word32 -> PhkdfCtx -> PhkdfSlowCtx Source #
close out a phkdfStream
context with a call to phkdfSlowExtract
,
providing the counter, tag, fnName
, and number of rounds to compute.
Note that fnName
is truncated to a length of 25-29 bytes long,
depending upon the number of rounds specified. Thus the fnName
is
primarily intended to be a protocol constant.
phkdfSlowCtx_addArg :: ByteString -> PhkdfSlowCtx -> PhkdfSlowCtx Source #
Add a tweak to a call to phkdfSlowExtract
.
phkdfSlowCtx_addArgs :: Foldable f => f ByteString -> PhkdfSlowCtx -> PhkdfSlowCtx Source #
Add zero or more tweaks to a call to phkdfSlowExtract
.
phkdfSlowCtx_finalize :: (Int -> ByteString) -> PhkdfSlowCtx -> ByteString Source #
finalize a call to phkdfSlowExtract
, discarding all but the first block
of the output stream
phkdfSlowCtx_finalizeStream :: (Int -> ByteString) -> PhkdfSlowCtx -> Stream ByteString Source #
finalize a call to phkdfSlowExtract
phkdfGen_initFromHmacKey :: ByteString -> Word32 -> ByteString -> HmacKey -> PhkdfGen Source #
phkdfGen_read :: PhkdfGen -> (ByteString, PhkdfGen) Source #
phkdfGen_peek :: PhkdfGen -> Maybe ByteString Source #