packages feed

g3p-hash-2.0.0.0: test/MyCorpExample.hs

{-# LANGUAGE OverloadedStrings, OverloadedLists #-}

{- |

This is intended to be a fairly realistic sketch of what a reasonable-quality
deployment of the G3P might look like for authentication purposes.

TODO: this file may appear to use base16-encoded inputs, but it's literally
base16-encoded inputs, not things decoded to binary and then used as an input.
Fix this.

It starts to sketch how end-to-end encryption might work, but this is intended
more to stimulate the imagination than be a complete sketch.

-}

-- TODO: get the JSON test harness capable of handling partial evaluation
-- TODO: actually set up haddock example testing
-- TODO: include good examples of rehearsals
--           G3Pb2 full dress rehearsal on password change
--           G3Pb2 login tech rehearsal on login page load
-- TODO: G3Pb2 suggested approaches to handling second secrets

module MyCorpExample where

import Data.ByteString(ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Base16 as B
import Data.Text(Text)
import Data.Vector()
import Test.Tasty
import Test.Tasty.HUnit

import Crypto.G3P.V2
import Crypto.G3P.V2.Foxtrot
import Crypto.PHKDF(phkdfGen_head)
import Crypto.PHKDF.HMAC(hmacKey)
import Crypto.Argon2

tests :: [TestTree]
tests =
  [ testCase "My Corporation" $ map B.encodeBase16 results @?= auResults
  ]

results :: [ByteString]
results =
  let myDomain = "my.domain.example"
      myLoginDomain = "login.my.domain.example"
      myStorageDomain = "cloud.my.domain.example"
      myLongTag = "My Corporation, Inc. https://my.domain.example/.well-known/security.txt" :: ByteString
      mySeguid = hmacKey "9c08053b7e507a78b571b5b93e1326674540d7106da6408fcafeddcfcdf1ed76"
      -- If your deployment uses a public salt server, I recommend keeping
      -- random salts directly in a database. More specifically, I do not
      -- recommend deriving public salts from non-public information, as an
      -- evesdropper could use this as evidence that they have actually
      -- compromised your stuff. Or, your deployment could apply key-stretching
      -- to a login name to derive a salt in a transparent way, avoiding the
      -- pitfalls of running a public salt server.

      userPublicSalt = "60473b8010e16d46"
      userSecondSecretHash = "0c06f683f093cb899b4a1e9836fc7281"
      userSalt =
        G3PSalt {
          g3pSalt_seguid = mySeguid,
          g3pSalt_longTag = myLongTag,
          g3pSalt_contextTags = [userPublicSalt],
          g3pSalt_domainTag = myDomain,
          g3pSalt_phkdfRounds = 20240
        }
      userInputs =
        G3PInputs {
          g3pInputs_username = userPublicSalt,
          g3pInputs_password = "correct horse battery staple",
          g3pInputs_credentials = [userSecondSecretHash]
        }
      mySeedInputs =
        G3PSeedInputs {
          g3pSeedInputs_bcryptSeguid = mySeguid,
          g3pSeedInputs_bcryptCredentials = [],
          g3pSeedInputs_bcryptLongTag = myLongTag,
          g3pSeedInputs_bcryptContextTags = [],
          g3pSeedInputs_bcryptDomainTag = myDomain,
          g3pSeedInputs_bcryptRounds = 4202
        }
      userSprout = g3pHash userSalt userInputs mySeedInputs mySeguid
      userHeader = userPublicSalt <> myDomain
      userAuthPrehash =
        mySprout ["auth",userPublicSalt] myLoginDomain
                 userHeader userHeader (word32 "AUTH") myLongTag

      -- Now, everything above would ideally happen on the client device, not
      -- the server. However, the server needs to hash the result further before
      -- storage. To deter precomputation attacks on an account, this sketch
      -- of a hypothetical deployment combines two somewhat crude but effective
      -- strategies:
      --
      --   1. The auth servers perform the main key-stretching computation, and
      --
      --   2. The auth servers perform this computation behind a secret HMAC key

      -- In my estimation, in many contexts argon2 is likely to be the most
      -- easily accepted hash function for key-stretching. However without
      -- modification argon2 doesn't have much cryptoacoustic potential,
      -- unlike yescrypt and Catena which appear to have some (probably
      -- largely "accidental") cryptoacoustic potential.

      -- I'd love to build a new hash function based closely on argon2, but
      -- this is delicate, time-consuming, and unpredictable work. For the
      -- time being, combining argon2 and g3pFoxtrot is almost certainly an
      -- excellent choice for server-side hashing.

      -- If the overall authentication flow is based on sending a plaintext
      -- prehash to the server which is then hashed further, I recommend using
      -- a secret, server-side salt per account. As this salt is never intended
      -- to be publicly acknowledged, one could derive this salt from
      -- non-public information without directly providing an evedropper the
      -- ability to prove to others they've been in your infrastructure.

      -- However, I would still recommend always storing a random secret per
      -- account so that an evesdropper cannot steal your entire secret salt
      -- database, possibly including secret salts that aren't yet in use,
      -- by stealing a single key. On the other hand, deriving the secret salt
      -- using a relatively small number of keys stored outside the database
      -- means that even if somebody steals your auth database, they won't
      -- necessarily have access to your secret salts.

      mySecretSeguid = hmacKey "7db250698fe555f6832f33189f97e14ef3c1c2dcada5807119aa7676c24f3fac"

      userPrivateSeed = "4314a11c2620a8ad"
      userPrivatePreSalt = g3pTango mySecretSeguid [userPrivateSeed,userPublicSalt, "user private presalt"] (word32 "SALT") myLoginDomain
      userPrivateSalt = g3pTango mySeguid [userPrivatePreSalt, "user private salt"] (word32 "SALT") myLoginDomain

      -- This derivation scheme allows My Corp to prove that its secret
      -- salts are in fact its trade secrets even in the face of the most dogged
      -- liars. Moreover this fact can possibly remain plausibly deniable even
      -- after the derivation has been stolen and published, so neither does
      -- this necessarily commit My Corp to claiming its secrets.

      -- Deriving a secret HMAC key per account allows My Corp to outsource
      -- offline cracking attacks on individual accounts without revealing an
      -- offline cracking attack on every account.

      -- Moreover, this derivation allows the proof-of-trade-secret to also be
      -- revealed/claimed on a per-account basis.

      foxtrot = g3pFoxtrot (G3PFoxtrotSalt
        { g3pFoxtrotSalt_key = hmacKey (userPrivateSalt <> B.take 32 userHeader)
        , g3pFoxtrotSalt_longTag = myLongTag
        , g3pFoxtrotSalt_contextTags = [userPublicSalt]
        , g3pFoxtrotSalt_domainTag = myLoginDomain
        , g3pFoxtrotSalt_bcryptRounds = 383
        })

      argon2 = hash $ HashOptions
        { hashIterations = 3
        , hashMemory = 384 * 1024 -- 384 MiB
        , hashParallelism = 1
        , hashVariant = Argon2id
        , hashVersion = Argon2Version13
        , hashLength = 32
        }

      userPrestoreHash = foxtrot ("P" <> userAuthPrehash) [] (word32 "PASS")
      (Right userArgon2Hash) = argon2 userRandomSalt (userPrestoreHash <> myLongTag)
      foxtrot' = foxtrot ("A" <> userArgon2Hash)
      userStoredHash = foxtrot' [] (word32 "HASH")

      -- userStoredHash is suitable to be stored in an auth database, and
      -- subsequent authentication attempts can compare this hash against the
      -- database. If this authentication is successful, we can efficiently
      -- compute a storage key that includes all of the key-stretching work
      -- performed thus far:

      userStorageKey = foxtrot' ["storage-key"] (word32 "KEY\x00")

      -- Note that this storage key will be re-combined with the original
      -- client-side seed before end-to-end encrypted files can be unlocked.
      -- The storage key is useless on its own, and therefore the auth server
      -- never gains the information needed to unlock the files without first
      -- guessing "correct horse battery staple".

      -- An attacker who has access to the user's encrypted files but does not
      -- have that user's secret server-side salt would not be able to confirm
      -- or deny that the user's password is "correct horse battery staple",
      -- unless the user has a backup method to unlock that particular file
      -- without talking to the auth server, and that backup method is the
      -- user's reused password.

      userDiskKey = mySprout
                      ["disk",myStorageDomain,myLongTag,
                      "key",userStorageKey]
                     myStorageDomain userHeader userHeader (word32 "DISK")

      -- myLongTag is included above because it is sufficiently long to be able
      -- to commit to the "disk" and myStorageDomain values by partially
      -- evalating the sprout and then forgetting the seed.

   in [ userAuthPrehash
      , userStoredHash
      , userDiskKey "filename0.txt"
      , userDiskKey "quarterly-report.pdf"
      ]

-- FIXME: these are currently wrong, change these once this part of the test
-- suite is working again.
auResults :: [Text]
auResults =
  [ "3759cc63959878c79e9077f7c8dc401cad1700e03bab7ca52ef2982553c37197"
  , "e8c26138add0f16e49ad1e2b55ff333eda42fa7330969146f55ac48a49f7166e"
  , "d0a3b6c432b6b612fb82a60554fa3fa906e8a4cc324c6f1de38e52d8eec254cf"
  , "c2fb84c71dbe52280bd0d481c770e4e476a5e0daeeddc3e9eee00423bef9a7e4"
  ]