packages feed

phkdf-0.0.0.0: lib/Crypto/PHKDF.hs

{-# LANGUAGE OverloadedStrings #-}

-- | The Password Hash Key Derivation Function (PHKDF) is a unification,
--   synthesis, and distillation of PBKDF2, HKDF, and TupleHash. It was
--   designed as a building block for implementing a variety of
--   self-documenting cryptographic constructions.
--
--   This module is intended more as a demonstration of and cookbook for
--   what can be done with the PHKDF primitives.  For actual deployments,
--   consider if the Global Password Prehash Protocol (G3P) is more
--   appropriate for your needs.  The G3P is a variant of 'phkdfPass' that
--   additionally integrates bcrypt as the primary key-stretching component.
--
--   These examples also serve as design studies that help informally justify
--   the G3P. Within my design framework, I've tried to maximize the benefits
--   while managing implementation costs.
--
--   1. Every bit of every parameter matters. Every boundary between
--      parameters matter. There aren't supposed to be any trivial collisions,
--      the only exception being null-extension collisions on the seguid.
--
--   2. Except for the tweaks, any change to any parameter requires restarting
--      the PHKDF key-stretching computation from somewhere in the very first
--      call to HMAC.
--
--   3. All input arguments are hardened against length-related timing side
--      channels in various different ways.
--
--      At one extreme, the username, password, and long tag have the most
--      aggressive length hardening in the conventional sense, exhibiting no
--      timing side channels except on multi-kilobyte inputs, after which
--      the timing impacts are minimzed.
--
--      At another extreme, the domain tag exhibits severe yet predictable
--      timing side channels transitioning from 19 to 20 bytes and every 64
--      bytes thereafter.  However, the domain tag is otherwise free of
--      timing-based side channels, so it too is hardened in its own way.
--
--   The design I converged upon employs fairly complicated data encoding
--   procedures. Unfortunately, this provides a fair bit of surface area
--   for subtly wrong implementations that work most of the time, but will
--   return garbage on certain lengths of inputs. I hope that this will
--   eventually be remediated with a more comprehensive suite of test vectors.

module Crypto.PHKDF where

import           Data.ByteString (ByteString)
import qualified Data.ByteString as B
import           Data.Function((&))
import           Data.Word
import           Data.Stream (Stream)
import           Data.Vector (Vector)
import qualified Data.Vector as V
import           Network.ByteOrder (word32)

import           Crypto.Encoding.PHKDF
import           Crypto.Encoding.SHA3.TupleHash
import           Crypto.PHKDF.Primitives
import           Crypto.PHKDF.Primitives.Assert

-- | These input parameters are grouped together because the envisioned use
--   for them is that they are constants (or near-constants) specified by
--   a deployment. User-supplied inputs would typically not go here.
--
--   The seguid parameter acts as a deployment-wide salt. Cryptographically
--   speaking, the most important thing a deployment can do is specify a
--   constant seguid.  It is highly recommended that the seguid input be a
--   genuine Self-Documenting Globally Unique Identifier attesting to the
--   parameters, purposes, and public playbook of the protocol for y'all
--   to follow.
--
--   In more concrete cryptographic terms, the seguid parameter is the constant
--   HMAC key used by the protocol right up until the final output exansion.
--   This design is closely modelled on the HKDF construction. As such, adding
--   null bytes onto the ends of seguids that are less than 64 bytes long
--   should be the only source of trivial collisions in the entire protocol.
--
--   The remaining parameter strings are all directly-documenting plaintext
--   tags. A deployment can use these tags to encode a message into the password
--   hash function so that it must be known to whomever is hashing a password
--   of their choice.
--
--   Finally, the rounds parameter determines the latency of the function.
--   At least 250,000 rounds are recommended if PHKDF is used as the sole key
--   stretching component of a password hash database.
--
--   Unfortunately PHKDF is inexpensively parallelized, so large investments
--   here aren't a good expenditure of a user's latency budget. This is why
--   the G3P integrates bcrypt, and cuts the suggested rounds down to 20,000
--
--   For comparison, @n@ rounds of PHKDF is approximately equivalent to
--   @(1.5 + dtl)*n + c@ rounds  of PBKDF2, where @dtl@ is related to the domain
--   tag length, and c is a bit larger than 130 or so.
--
--   Here, @dtl@ is 0 when the domain tag is between 0 and 19 bytes long, 0.5
--   when the domain tag is between 20 and 83 bytes long, and an additional 0.5
--   for every 64 bytes thereafter.  Thus these functions exhibit extreme
--   timing side channels on the length of the domain tag.
--
--   By contrast, the long tag is hardened against timing side channels up to
--   a bit less than 5 kilobytes in length.  However, an extremely long tag
--   does reduce the headroom provided to masking the length of the username
--   and password fields,  however the minimum headroom allocated to the
--   username and password fields is a bit less than 3 kilobytes.
--
--   As an alternate tagging location, consider the 'phkdfInputArgs_credentials'
--   vector, which can be used as an inexpensive, pay-as-you-go plaintext
--   tagging location.
--
--   If the total encoded byte length of 'phkdfInputBlock_tags' is between 0-63
--   bytes, then these hash protocols operate in a constant number of SHA256
--   blocks.  Every additional 64 bytes incurs the computation of two or three
--   additional SHA256 blocks, because these tags are hashed into the result
--   two times in the case of 'phkdfPass', and three times in the case of
--   'phkdfSimple' (and @g3pHash@).

data PhkdfInputBlock = PhkdfInputBlock
  { phkdfInputBlock_seguid     :: !ByteString
    -- ^ HMAC-SHA256 key, usable as a high-repetition indirect tag via
    --   self-documenting globally unique identifiers (seguids).
  , phkdfInputBlock_domainTag  :: !ByteString
    -- ^ plaintext tag with one repetition per round.  0-19 bytes are free,
    --   20-83 bytes cost a additional sha256 block per round, with every
    --   64 bytes thereafter incurring a similar cost.
  , phkdfInputBlock_longTag    :: !ByteString
    -- ^ plaintext tag with 1x repetition, then cycled for roughly
    --   8 kilobytes.  Constant time on inputs up to nearly 5 kilobytes.
  , phkdfInputBlock_tags       :: !(Vector ByteString)
    -- ^ plaintext tag with 2x repetition ('phkdfPass') or 3x repetition
    --   ('phkdfSimple'). Constant-time on 0-63 encoded bytes, which includes
    --   the length encoding of each string. Thus 60 of those bytes are usable
    --   if the tags vector is a single string, or less if it contains two or
    --   more strings.
  , phkdfInputBlock_rounds     :: !Word32
    -- ^ how expensive will this hash function be? An optimal implementation
    --   computes exactly three SHA256 blocks per round if the domain tag is
    --   19 bytes or less.  It is not recommended that phkdf be used as the
    --   primary key-stretching component of a deployment, but if it is used
    --   this way, we recommend at least 250,000 rounds.  This can be adjusted
    --   downward in the case of domain tags longer than 19 bytes.
  } deriving (Eq, Ord, Show)

-- | The username and password are grouped together because they are normally
--   expected to be supplied by users or other observers of a deployment.
--
--   Furthermore, the credentials vector is here because it is an ideal
--   location to include other user input. For example, one could implement
--   a Two-Secret Key Derivation (2SKD) scheme analogous to 1Password's.
--
--   A deployment can also specify additional constant tags as part of the
--   credentials vector.  As the plaintext of these tags is only ever hashed
--   into the output a single time, this is the least expensive
--   pay-as-you-go option for plaintext tagging.
--
--   The credentials vector is constant time on 0-63 encoded bytes, incurring
--   one additional SHA256 block every 64 bytes thereafter. This includes
--   a variable-length field that encodes the bit length of each string; this
--   field itself requires 2 or more bytes.
--
--   The username and password are constant time as long as their encoded
--   lengths add up to less than roughly 3 kilobytes, or the username,
--   password, and domain tag add up to less than roughly 8 kilobytes.
--   The actual numbers are somewhat less in both cases, but this is a
--   good approximation.

data PhkdfInputArgs = PhkdfInputArgs
  { phkdfInputArgs_username    :: !ByteString
  -- ^ The name of this parameter is suggestive, but this parameter is
  --   functionally identical to a second password. The only difference
  --   is the fact that a password can be cracked without knowledge of the
  --   plaintext username. By contrast, the password acts as a plaintext tag
  --   if one provides the username: guessing the username implies plaintext
  --   knowledge of the password.
  , phkdfInputArgs_password    :: !ByteString
  , phkdfInputArgs_credentials :: !(Vector ByteString)
  } deriving (Eq, Ord, Show)

-- | These parameters are used to tweak the final output, without redoing any
--   expensive key stretching.  A possible use case is including a high entropy
--   secret in the role itself that isn't available until after a successful
--   stage of authentication.
--
--   Since these parameters are processed in a context that could conceivably be
--   performance sensitive, we don't apply any length padding or side-channel
--   hardening.  Instead we opt for maximizing free tagging space.  Thus we
--   want to avoid incurring additional SHA256 block computations, one of the
--   favorite techniques employed by the key-stretching phase of 'phkdfPass'
--   to harden against timing side-channels.
--
--   A deployment could conceivably harden this expansion phase against timing
--   side channels themselves, if the were sufficiently inclined. There are
--   several techniques. For starters, a deployment could specify an additional
--   variable-length string in the role vector, used to control its relative
--   ending position inside the SHA256 buffer.

data PhkdfInputTweak = PhkdfInputTweak
  { phkdfInputTweak_role :: !(Vector ByteString)
  , phkdfInputTweak_echoTag  :: !ByteString
  } deriving (Eq, Ord, Show)

-- | A plain-old-data explicit representation of the intermediate 'phkdfPass'
--   computation after the 'PhkdfInputBlock' and 'PhkdfInputArgs' have been
--   processed and key stretching has been completed, but before the tweaks
--   have been applied and the final output generated.
--
--   If you ever need to serialize or persist a seed, you probably want this.
--
--   Intended to be generated by 'phkdfPass_seedInit' and then consumed
--   without modification by 'phkdfPass_seedFinalize'.

data PhkdfSeed = PhkdfSeed
  { phkdfSeed_seguid :: !ByteString
  , phkdfSeed_seguidKey :: !HmacKey
  , phkdfSeed_domainTag :: !ByteString
  , phkdfSeed_secret :: !ByteString
  } deriving (Eq)

-- | A non-tweakable, complete password prehash protocol

phkdfSimple :: PhkdfInputBlock -> PhkdfInputArgs -> Stream ByteString
phkdfSimple block args = echo
  where
    -- Explicitly unpack everything for the unused variable warnings.
    -- i.e. It's relatively easy to check that we've unpacked every
    -- field, then we can rely on unused variable warnings to ensure
    -- we have in fact made use of everything.
    domainTag = phkdfInputBlock_domainTag block
    seguid = phkdfInputBlock_seguid block
    longTag = phkdfInputBlock_longTag block
    tags = phkdfInputBlock_tags block
    rounds = phkdfInputBlock_rounds block

    username = phkdfInputArgs_username args
    password = phkdfInputArgs_password args
    credentials = phkdfInputArgs_credentials args

    headerExtract = [ "phkdf-simple0 username", username ]

    headerUsername = headerExtract ++ [ usernamePadding headerExtract domainTag domainTag ]
    -- password field goes here

    headerLongTag =
      [ longTag
      , B.concat
        [ "password-hash-key-derivation-function phkdf-simple0\x00"
        , leftEncodeFromBytes (B.length domainTag)
        , bareEncode rounds
        ]
      ]

    secretKey =
        phkdfCtx_init seguid &
        phkdfCtx_addArgs headerUsername &
        phkdfCtx_assertBufferPosition 32 &
        phkdfCtx_addArg  password &
        phkdfCtx_addArgs headerLongTag &
        -- FIXME: fusing addArg and passwordPadding can save ~ 8 KiB RAM
        phkdfCtx_addArg (passwordPadding headerUsername headerLongTag longTag domainTag password) &
        phkdfCtx_assertBufferPosition 32 &
        phkdfCtx_addArgs credentials &
        phkdfCtx_addArg (credentialsPadding credentials longTag domainTag) &
        phkdfCtx_assertBufferPosition 29 &
        phkdfCtx_addArgs tags &
        phkdfCtx_addArg (bareEncode (V.length tags)) &
        phkdfSlowCtx_extract
            (cycleByteStringWithNull domainTag)
            (word32 "go\x00\x00" + 2023) domainTag
            "phkdf-simple0 compact" rounds &
        phkdfSlowCtx_assertBufferPosition 32 &
        phkdfSlowCtx_addArgs tags &
        phkdfSlowCtx_finalize (cycleByteStringWithNull domainTag)

    -- Harden the tags vector against length-based timing side-channels
    echoHeader = cycleByteStringWithNull "phkdf-simple0 expand echo" 30

    echo = phkdfCtx_init secretKey &
           phkdfCtx_addArg echoHeader &
           phkdfCtx_assertBufferPosition 32 &
           phkdfCtx_addArgs tags &
           phkdfCtx_finalizeStream (cycleByteStringWithNull domainTag) (word32 "OUT\x00") domainTag

-- | A tweakable, complete prehash protocol.   Note that this function is very
--   intentionally implemented in such a way that the following idiom is
--   efficient, and only performs the expensive key stretching phase once:
--
-- @
--  let mySeed = phkdfPass block args
--   in [ mySeed tweak1, mySeed tweak2, mySeed tweak3 ]
-- @
--
--   However in the case that you want or need to persist or serialize the
--   intermediate seed, then the plain-old-datatype 'PhkdfSeed' and its
--   companion functions 'phkdfPass_seedInit' and 'phkdfPass_seedFinalize'
--   are likely to be more appropriate.

phkdfPass :: PhkdfInputBlock -> PhkdfInputArgs -> PhkdfInputTweak -> Stream ByteString
phkdfPass block args = phkdfPass_seedInit block args & phkdfPass_seedFinalize

-- | This generates a seed, which encapsulates the expensive key-stretching component of 'phkdfPass' into a reusable, tweakable cryptographic value.  This function is way slower than it's companion, 'phkdfPass_seedFinalize'.  Broadly comparable to HKDF-Extract, though with key stretching built-in.

phkdfPass_seedInit :: PhkdfInputBlock -> PhkdfInputArgs -> PhkdfSeed
phkdfPass_seedInit block args =
    PhkdfSeed {
      phkdfSeed_seguid = seguid,
      phkdfSeed_seguidKey = seguidKey,
      phkdfSeed_domainTag = domainTag,
      phkdfSeed_secret = secret
    }
  where
    domainTag = phkdfInputBlock_domainTag block
    seguid = phkdfInputBlock_seguid block
    longTag = phkdfInputBlock_longTag block
    seedTags = phkdfInputBlock_tags block
    rounds = phkdfInputBlock_rounds block

    username = phkdfInputArgs_username args
    password = phkdfInputArgs_password args
    credentials = phkdfInputArgs_credentials args

    headerExtract = [ "phkdf-pass-v0 username", username ]

    headerUsername = headerExtract ++ [ usernamePadding headerExtract domainTag domainTag ]

    -- password field goes here

    headerLongTag =
      [ longTag
      , B.concat
        [ "password hash & key derivation function: phkdf-pass-v0"
        , bareEncode rounds
        ]
      ]

    seguidKey = hmacKey_init seguid

    secret =
        phkdfCtx_initFromHmacKey seguidKey &
        phkdfCtx_addArgs headerUsername &
        phkdfCtx_assertBufferPosition 32 &
        phkdfCtx_addArg  password &
        -- FIXME: fusing addArg and longPadding can save ~ 8 KiB RAM
        phkdfCtx_addArgs headerLongTag &
        phkdfCtx_addArg  (passwordPadding headerUsername headerLongTag longTag domainTag password) &
        phkdfCtx_assertBufferPosition 32 &
        phkdfCtx_addArgs credentials &
        phkdfCtx_addArg (credentialsPadding credentials longTag domainTag) &
        phkdfCtx_assertBufferPosition 29 &
        phkdfCtx_addArgs seedTags &
        phkdfCtx_addArg (bareEncode (V.length seedTags)) &
        phkdfSlowCtx_extract
            (cycleByteStringWithNull domainTag)
            (word32 "go\x00\x00" + 2023) domainTag
            "phkdf-pass-v0 compact" rounds &
        phkdfSlowCtx_assertBufferPosition 32 &
        phkdfSlowCtx_addArgs seedTags &
        phkdfSlowCtx_finalize (cycleByteStringWithNull domainTag)

-- | This consumes a seed and tweaks to produce the final output stream.
-- This function is the output expansion phase of 'phkdfPass'.  This function
-- is way faster than it's companion 'phkdfPass_seedInit'.  Broadly comparable to
-- HKDF-Expand.

phkdfPass_seedFinalize :: PhkdfSeed ->  PhkdfInputTweak -> Stream ByteString
phkdfPass_seedFinalize seed tweak = echo
  where
    seguidKey = phkdfSeed_seguidKey seed
    domainTag = phkdfSeed_domainTag seed
    secret = phkdfSeed_secret seed

    role = phkdfInputTweak_role tweak
    echoTag = phkdfInputTweak_echoTag tweak

    headerCombine = B.concat ["phkdf-pass-v0 combine", secret]
    secretKey =
        phkdfCtx_initFromHmacKey seguidKey &
        phkdfCtx_addArg  headerCombine &
        phkdfCtx_addArgs role &
        phkdfCtx_finalize (cycleByteStringWithNull domainTag) (word32 "KEY\x00") domainTag

    headerEcho = cycleByteString (domainTag <> "\x00phkdf-pass-v0 echo\x00") 32

    echo = hmacKey_init secretKey &
           phkdfGen_initFromHmacKey headerEcho (word32 "OUT\x00") echoTag &
           phkdfGen_finalizeStream