g3p-hash-2.0.0.0: lib/Crypto/G3P/V2/Foxtrot.hs
{-# LANGUAGE OverloadedStrings #-}
-------------------------------------------------------------------------------
-- |
-- Module: Crypto.G3P.V2.Foxtrot
-- Copyright: (c) 2024 Auth Global
-- License: Apache2
--
-------------------------------------------------------------------------------
{- |
Stripped-down version of G3Pb2 charlie, primarily intended for server-side
application. This uses HMAC-SHA256 both to generate inputs to bcrypt and to
summarize the resulting bcrypt state.
Assuming your deployment is basically a traditional give-me-your-password
authentication protocol but with prehashing, I recommend a two-step approach
to counteracting prehash precomputation attacks. First, I recommend that the
server apply at least as much key-stretching as the client. Second, I recommend
this server-side key-stretching be protected by a secret HMAC key.
In such a deployment, a cracker could front-load half of the computation
needed to guess a password at significant storage expense. This is far less
appealing to the cracker than being able to front-load nearly all of
the computation needed while incurring the same expense to store the
intermediate guesses.
The test suite's MyCorpExample.hs contains an example of how one might use
these functions in a deployment. By using reduced-round calls to 'g3pFoxtrot'
in conjuction with argon2, one can make precomputation an even less appealing
strategy, as the key-stretching occurs server-side is significantly more
expensive than the prehash itself.
-}
module Crypto.G3P.V2.Foxtrot where
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Function((&))
import Data.Word
import Data.Vector(Vector)
import qualified Data.Vector as V
import Crypto.G3P.BCrypt (bcryptXsFree)
import Crypto.PHKDF
import Crypto.Encoding.PHKDF (takeBs, nullBuffer)
import Network.ByteOrder(bytestring64)
data G3PFoxtrotSalt = G3PFoxtrotSalt
{ g3pFoxtrotSalt_key :: !HmacKey
, g3pFoxtrotSalt_longTag :: !ByteString
, g3pFoxtrotSalt_contextTags :: !(Vector ByteString)
, g3pFoxtrotSalt_domainTag :: !ByteString
, g3pFoxtrotSalt_bcryptRounds :: !Word32
}
-- | G3Pb2 foxtrot is a function that incorporates a bcrypt-like key-stretching
-- phase. Stripped down version of G3Pb2 charlie, without a built-in continuation
-- control key. @test/MyCorpExample.hs@ uses this as a server-side cryptoacoustic
-- component that sandwiches the comparatively silent argon2.
g3pFoxtrot
:: (Foldable f, Foldable g)
=> G3PFoxtrotSalt
-> f ByteString
-> g ByteString
-> Word32
-> ByteString
g3pFoxtrot salt inputs = doTweak
where
foxtrot = "G3Pb2 foxtrot"
key = g3pFoxtrotSalt_key salt
longTag = g3pFoxtrotSalt_longTag salt
contextTags = g3pFoxtrotSalt_contextTags salt
domainTag = g3pFoxtrotSalt_domainTag salt
rounds = g3pFoxtrotSalt_bcryptRounds salt
spark =
phkdfCtx_init key &
phkdfCtx_feedArg foxtrot &
phkdfCtx_feedArgs inputs &
phkdfCtx_toHmacKeyPrefixed (B.concat . flip takeBs [domainTag, "\x00", longTag, nullBuffer] . fromIntegral)
-- G3Pb2 foxtrot doesn't ever explicitly encode the length of the syntax
-- generated by bcryptXsFree in the plaintext of the HMAC message itself.
-- This length of this syntax is determined by the number of superrounds,
-- which in turn is determined by the number of bcrypt rounds.
-- This doesn't create any homophones, a.k.a. "canonicalization attacks",
-- which is terminology that the algebraist in me isn't fond of.
-- It can't cause homophones because the number of rounds, and thus this
-- syntax length, is encoded in the bcrypt key-stretching phase well before
-- the very first bcrypt output byte is generated. As these bytes are
-- then consumed by HMAC, their overall length is implicitly encoded into
-- the HMAC message.
-- Not to mention that there are heurstic methods to parse the generated
-- HMAC syntax without knowing the number of bcrypt rounds up front
-- that will perform with perfect accuracy on most or all actual
-- deployments. Every superround results in a fixed-length, multi-kilobyte
-- syntax string appended to the message, which is much much longer than
-- the expected length of the remaining parameters. Futhermore each
-- multi-kilobyte superround syntax string always contains the literal
-- sequence of bytes "OrpheanBeholderScryDoubt" in a fixed location,
-- which in practice won't appear in other parameters.
bcryptName = B.concat
[ "G3Pb2 bcrypt-xs-free"
, bytestring64 (8 * fromIntegral (B.length longTag))
]
(_tagPos, seed) =
bcryptXsFree id bcryptName V.empty longTag contextTags domainTag
rounds spark
sprout =
phkdfCtx_initPrefixed (B.concat $ takeBs 32 [domainTag, "\x00", foxtrot, nullBuffer]) seed &
phkdfCtx_feedArgs contextTags
doTweak tweak counter =
phkdfCtx_feedArgs tweak sprout &
phkdfCtx_finalize (B.concat . flip takeBs (cycle [domainTag, "\x00"]) . fromIntegral) counter domainTag
-- | G3Pb2 tango: a simple application of PHKDF used to derive secret server-side
-- salts in @test/MyCorpExample.hs@.
-- TODO: rewrite this in a more point-free style, in order to better support partial application
g3pTango
:: (Foldable f)
=> HmacKey
-> f ByteString -- ^ inputs
-> Word32 -- ^ counter
-> ByteString -- ^ domain tag
-> ByteString -- ^ 32-byte output hash
g3pTango key inputs counter domainTag = out
where
tango = "G3Pb2 tango"
out =
phkdfCtx_init key &
phkdfCtx_feedArg tango &
phkdfCtx_feedArgs inputs &
phkdfCtx_finalize (B.concat . flip takeBs (cycle [domainTag, "\x00"]) . fromIntegral) counter domainTag