phkdf (empty) → 0.0.0.0
raw patch · 15 files changed
+2265/−0 lines, 15 filesdep +Streamdep +aesondep +basesetup-changed
Dependencies added: Stream, aeson, base, base16, bytestring, containers, cryptohash-sha256, network-byte-order, phkdf, tasty, tasty-hunit, text, tuplehash-utils, vector
Files
- ChangeLog.md +10/−0
- LICENSE +202/−0
- Setup.hs +2/−0
- lib/Crypto/Encoding/PHKDF.hs +115/−0
- lib/Crypto/PHKDF.hs +384/−0
- lib/Crypto/PHKDF/HMAC.hs +66/−0
- lib/Crypto/PHKDF/HMAC/Subtle.hs +49/−0
- lib/Crypto/PHKDF/Primitives.hs +479/−0
- lib/Crypto/PHKDF/Primitives/Assert.hs +42/−0
- lib/Crypto/PHKDF/Primitives/Subtle.hs +69/−0
- phkdf-test-vectors.json +271/−0
- phkdf.cabal +69/−0
- test/HMAC.hs +99/−0
- test/Main.hs +15/−0
- test/PHKDF.hs +393/−0
+ ChangeLog.md view
@@ -0,0 +1,10 @@+# Revision history for phkdf++## Version 0.0.0.0 (2024-03-21)++* The definition of Crypto.PHKDF.Primitives is quite stable.++* Note that Crypto.PHKDF is intended to be more of a cookbook demonstrating+ how to use and understand PHKDF than anything intended to be deployed.+ The definition and results, and possibly the interface of phkdfSimple and+ phkdfHardened will be completely revamped for Version 0.1
+ LICENSE view
@@ -0,0 +1,202 @@++ Apache License+ Version 2.0, January 2004+ http://www.apache.org/licenses/++ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++ 1. Definitions.++ "License" shall mean the terms and conditions for use, reproduction,+ and distribution as defined by Sections 1 through 9 of this document.++ "Licensor" shall mean the copyright owner or entity authorized by+ the copyright owner that is granting the License.++ "Legal Entity" shall mean the union of the acting entity and all+ other entities that control, are controlled by, or are under common+ control with that entity. For the purposes of this definition,+ "control" means (i) the power, direct or indirect, to cause the+ direction or management of such entity, whether by contract or+ otherwise, or (ii) ownership of fifty percent (50%) or more of the+ outstanding shares, or (iii) beneficial ownership of such entity.++ "You" (or "Your") shall mean an individual or Legal Entity+ exercising permissions granted by this License.++ "Source" form shall mean the preferred form for making modifications,+ including but not limited to software source code, documentation+ source, and configuration files.++ "Object" form shall mean any form resulting from mechanical+ transformation or translation of a Source form, including but+ not limited to compiled object code, generated documentation,+ and conversions to other media types.++ "Work" shall mean the work of authorship, whether in Source or+ Object form, made available under the License, as indicated by a+ copyright notice that is included in or attached to the work+ (an example is provided in the Appendix below).++ "Derivative Works" shall mean any work, whether in Source or Object+ form, that is based on (or derived from) the Work and for which the+ editorial revisions, annotations, elaborations, or other modifications+ represent, as a whole, an original work of authorship. For the purposes+ of this License, Derivative Works shall not include works that remain+ separable from, or merely link (or bind by name) to the interfaces of,+ the Work and Derivative Works thereof.++ "Contribution" shall mean any work of authorship, including+ the original version of the Work and any modifications or additions+ to that Work or Derivative Works thereof, that is intentionally+ submitted to Licensor for inclusion in the Work by the copyright owner+ or by an individual or Legal Entity authorized to submit on behalf of+ the copyright owner. For the purposes of this definition, "submitted"+ means any form of electronic, verbal, or written communication sent+ to the Licensor or its representatives, including but not limited to+ communication on electronic mailing lists, source code control systems,+ and issue tracking systems that are managed by, or on behalf of, the+ Licensor for the purpose of discussing and improving the Work, but+ excluding communication that is conspicuously marked or otherwise+ designated in writing by the copyright owner as "Not a Contribution."++ "Contributor" shall mean Licensor and any individual or Legal Entity+ on behalf of whom a Contribution has been received by Licensor and+ subsequently incorporated within the Work.++ 2. Grant of Copyright License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ copyright license to reproduce, prepare Derivative Works of,+ publicly display, publicly perform, sublicense, and distribute the+ Work and such Derivative Works in Source or Object form.++ 3. Grant of Patent License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ (except as stated in this section) patent license to make, have made,+ use, offer to sell, sell, import, and otherwise transfer the Work,+ where such license applies only to those patent claims licensable+ by such Contributor that are necessarily infringed by their+ Contribution(s) alone or by combination of their Contribution(s)+ with the Work to which such Contribution(s) was submitted. If You+ institute patent litigation against any entity (including a+ cross-claim or counterclaim in a lawsuit) alleging that the Work+ or a Contribution incorporated within the Work constitutes direct+ or contributory patent infringement, then any patent licenses+ granted to You under this License for that Work shall terminate+ as of the date such litigation is filed.++ 4. Redistribution. You may reproduce and distribute copies of the+ Work or Derivative Works thereof in any medium, with or without+ modifications, and in Source or Object form, provided that You+ meet the following conditions:++ (a) You must give any other recipients of the Work or+ Derivative Works a copy of this License; and++ (b) You must cause any modified files to carry prominent notices+ stating that You changed the files; and++ (c) You must retain, in the Source form of any Derivative Works+ that You distribute, all copyright, patent, trademark, and+ attribution notices from the Source form of the Work,+ excluding those notices that do not pertain to any part of+ the Derivative Works; and++ (d) If the Work includes a "NOTICE" text file as part of its+ distribution, then any Derivative Works that You distribute must+ include a readable copy of the attribution notices contained+ within such NOTICE file, excluding those notices that do not+ pertain to any part of the Derivative Works, in at least one+ of the following places: within a NOTICE text file distributed+ as part of the Derivative Works; within the Source form or+ documentation, if provided along with the Derivative Works; or,+ within a display generated by the Derivative Works, if and+ wherever such third-party notices normally appear. The contents+ of the NOTICE file are for informational purposes only and+ do not modify the License. You may add Your own attribution+ notices within Derivative Works that You distribute, alongside+ or as an addendum to the NOTICE text from the Work, provided+ that such additional attribution notices cannot be construed+ as modifying the License.++ You may add Your own copyright statement to Your modifications and+ may provide additional or different license terms and conditions+ for use, reproduction, or distribution of Your modifications, or+ for any such Derivative Works as a whole, provided Your use,+ reproduction, and distribution of the Work otherwise complies with+ the conditions stated in this License.++ 5. Submission of Contributions. Unless You explicitly state otherwise,+ any Contribution intentionally submitted for inclusion in the Work+ by You to the Licensor shall be under the terms and conditions of+ this License, without any additional terms or conditions.+ Notwithstanding the above, nothing herein shall supersede or modify+ the terms of any separate license agreement you may have executed+ with Licensor regarding such Contributions.++ 6. Trademarks. This License does not grant permission to use the trade+ names, trademarks, service marks, or product names of the Licensor,+ except as required for reasonable and customary use in describing the+ origin of the Work and reproducing the content of the NOTICE file.++ 7. Disclaimer of Warranty. Unless required by applicable law or+ agreed to in writing, Licensor provides the Work (and each+ Contributor provides its Contributions) on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+ implied, including, without limitation, any warranties or conditions+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+ PARTICULAR PURPOSE. You are solely responsible for determining the+ appropriateness of using or redistributing the Work and assume any+ risks associated with Your exercise of permissions under this License.++ 8. Limitation of Liability. In no event and under no legal theory,+ whether in tort (including negligence), contract, or otherwise,+ unless required by applicable law (such as deliberate and grossly+ negligent acts) or agreed to in writing, shall any Contributor be+ liable to You for damages, including any direct, indirect, special,+ incidental, or consequential damages of any character arising as a+ result of this License or out of the use or inability to use the+ Work (including but not limited to damages for loss of goodwill,+ work stoppage, computer failure or malfunction, or any and all+ other commercial damages or losses), even if such Contributor+ has been advised of the possibility of such damages.++ 9. Accepting Warranty or Additional Liability. While redistributing+ the Work or Derivative Works thereof, You may choose to offer,+ and charge a fee for, acceptance of support, warranty, indemnity,+ or other liability obligations and/or rights consistent with this+ License. However, in accepting such obligations, You may act only+ on Your own behalf and on Your sole responsibility, not on behalf+ of any other Contributor, and only if You agree to indemnify,+ defend, and hold each Contributor harmless for any liability+ incurred by, or claims asserted against, such Contributor by reason+ of your accepting any such warranty or additional liability.++ END OF TERMS AND CONDITIONS++ APPENDIX: How to apply the Apache License to your work.++ To apply the Apache License to your work, attach the following+ boilerplate notice, with the fields enclosed by brackets "[]"+ replaced with your own identifying information. (Don't include+ the brackets!) The text should be enclosed in the appropriate+ comment syntax for the file format. We also recommend that a+ file or class name and description of purpose be included on the+ same "printed page" as the copyright notice for easier+ identification within third-party archives.++ Copyright [yyyy] [name of copyright owner]++ Licensed under the Apache License, Version 2.0 (the "License");+ you may not use this file except in compliance with the License.+ You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++ Unless required by applicable law or agreed to in writing, software+ distributed under the License is distributed on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ See the License for the specific language governing permissions and+ limitations under the License.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ lib/Crypto/Encoding/PHKDF.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE OverloadedStrings, ViewPatterns #-}++module Crypto.Encoding.PHKDF where++import Data.Monoid((<>))+import Data.Bits(Bits, (.&.))+import Data.ByteString(ByteString)+import Data.Foldable(Foldable)+import qualified Data.ByteString as B+import Crypto.Encoding.SHA3.TupleHash++import Debug.Trace++-- FIXME: several functions in here have opportunites for optimization++cycleByteStringToList :: ByteString -> Int -> [ByteString]+cycleByteStringToList str outBytes =+ if outBytes <= 0+ then []+ else if n == 0+ then [ B.replicate outBytes 0 ]+ else replicate q str ++ [B.take r str]+ where+ n = B.length str+ (q,r) = outBytes `quotRem` n++cycleByteStringWithNullToList :: ByteString -> Int -> [ByteString]+cycleByteStringWithNullToList str outBytes = out+ where+ out = cycleByteStringToList (str <> "\x00") outBytes++cycleByteString :: ByteString -> Int -> ByteString+cycleByteString str outBytes = B.concat (cycleByteStringToList str outBytes)++cycleByteStringWithNull :: ByteString -> Int -> ByteString+cycleByteStringWithNull str outBytes =+ B.concat (cycleByteStringWithNullToList str outBytes)++extendTagToList :: ByteString -> [ByteString]+extendTagToList tag = if n <= 19 then [tag] else tag'+ where+ n = B.length tag+ x = (18 - n) `mod` 64+ tag' = cycleByteStringWithNullToList tag (n+x)+ ++ [B.singleton (fromIntegral x)]++extendTag :: ByteString -> ByteString+extendTag = B.concat <$> extendTagToList++trimExtTag :: ByteString -> Maybe ByteString+trimExtTag extTag+ | n <= 19 = Just extTag+ | extTag /= extendTag tag = Nothing+ | otherwise = Just tag+ where+ n = B.length extTag+ x = B.last extTag+ tag = B.take (n - fromIntegral x - 1) extTag++{--++FIXME: as written, this only works on signed arithmetic, unless the modulus @a@+is a power of 2, such as 64++-- | @addWhileLt a b c@ is equivalent to @while (b < c) { b += a }; return b@+addWhileLt :: Integral a => a -> a -> a -> a+addWhileLt a b c+ | b >= c = b+ | otherwise = c + ((b - c) `mod` a)++--}++-- | @add64WhileLt b c@ is equivalent to @while (b < c) { b += 64 }; return b@++add64WhileLt :: (Ord a, Num a, Bits a) => a -> a -> a+add64WhileLt b c+ | b >= c = b+ | otherwise = c + ((b - c) .&. 63)++add64WhileLt' :: (Ord a, Num a, Bits a, Show a) => a -> a -> a+add64WhileLt' b c+ | b >= c = b+ | otherwise = let d = c + ((b - c) .&. 63)+ in trace (show b ++ " -> " ++ show d) d++usernamePadding :: Foldable f => f ByteString -> ByteString -> ByteString -> ByteString+usernamePadding headerExtract fillerTag domainTag+ = cycleByteStringWithNull fillerTag (a-32)+ <> cycleByteStringWithNull domainTag 32+ where+ al = encodedVectorByteLength headerExtract+ a = add64WhileLt (157 - al) 32++passwordPaddingBytes :: Foldable f => Int -> f ByteString -> f ByteString -> ByteString -> ByteString -> ByteString -> ByteString+passwordPaddingBytes bytes headerUsername headerLongTag fillerTag domainTag password+ = cycleByteStringWithNull fillerTag (c-32)+ <> cycleByteStringWithNull domainTag 32+ where+ al = encodedVectorByteLength headerLongTag+ a = add64WhileLt (bytes - al) 3240+ bl = encodedVectorByteLength headerUsername+ b = add64WhileLt (a - bl) 136+ cl = encodedByteLength password+ c = add64WhileLt (b - cl) 32++passwordPadding :: Foldable f => f ByteString -> f ByteString -> ByteString -> ByteString -> ByteString -> ByteString+passwordPadding = passwordPaddingBytes 8413++credentialsPadding :: Foldable f => f ByteString -> ByteString -> ByteString -> ByteString+credentialsPadding credentials fillerTag domainTag+ = cycleByteStringWithNull fillerTag (a-29)+ <> cycleByteStringWithNull domainTag 29+ where+ al = encodedVectorByteLength credentials+ a = add64WhileLt (122 - al) 32
+ lib/Crypto/PHKDF.hs view
@@ -0,0 +1,384 @@+{-# 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
+ lib/Crypto/PHKDF/HMAC.hs view
@@ -0,0 +1,66 @@+{- |++An alternate implementation of HMAC in terms of cryptohash-sha256, because+the HMAC implementation provided there doesn't support precomputed keys or+streaming inputs. TODO: prepare a patch for cryptohash-sha256.++-}+++module Crypto.PHKDF.HMAC+ ( HmacCtx+ , HmacKey+ , hmacKey_init+ , hmacKey_run+ , hmacCtx_init+ , hmacCtx_initFromHmacKey+ , hmacCtx_update+ , hmacCtx_updates+ , hmacCtx_finalize+ ) where++import qualified Crypto.Hash.SHA256 as SHA256+import Data.Bits(xor)+import Data.ByteString (ByteString)+import qualified Data.ByteString as B++import Crypto.PHKDF.HMAC.Subtle++-- | Precompute an HMAC key for some literal HMAC key.++hmacKey_init :: ByteString -> HmacKey+hmacKey_init = HmacKey . hmacCtx_init++-- | Initialize a new empty HMAC context from a literal HMAC key.++hmacCtx_init :: ByteString -> HmacCtx+hmacCtx_init key =+ HmacCtx { hmacCtx_ipad = tweak 0x36, hmacCtx_opad = tweak 0x5c }+ where+ tweak c = SHA256.update SHA256.init $ B.map (xor c) k2+ k1 = if B.length key > 64 then SHA256.hash key else key+ k2 = B.append k1 (B.replicate (64 - B.length k1) 0)++-- | Initialize a new empty HMAC context from a precomputed HMAC key.++hmacCtx_initFromHmacKey :: HmacKey -> HmacCtx+hmacCtx_initFromHmacKey = hmacKey_run++-- | Append a bytestring onto the end of the message argument to HMAC.++hmacCtx_update :: ByteString -> HmacCtx -> HmacCtx+hmacCtx_update b (HmacCtx ic oc) = HmacCtx (SHA256.update ic b) oc++-- | Append zero or more bytestrings onto the end of the message argument to+-- HMAC.++hmacCtx_updates :: [ByteString] -> HmacCtx -> HmacCtx+hmacCtx_updates bs (HmacCtx ic oc) = HmacCtx (SHA256.updates ic bs) oc++-- | Finish computing the final 32-byte hash for an HMAC context.++hmacCtx_finalize :: HmacCtx -> ByteString+hmacCtx_finalize (HmacCtx ic oc) = outer+ where+ inner = SHA256.finalize ic+ outer = SHA256.finalize (SHA256.update oc inner)
+ lib/Crypto/PHKDF/HMAC/Subtle.hs view
@@ -0,0 +1,49 @@+{- |++"Internal" data structures representing precomputed HMAC keys and partial HMAC+contexts, supporting incremental computation and backtracking.++-}++module Crypto.PHKDF.HMAC.Subtle+ ( HmacCtx(..)+ , HmacKey(..)+ , hmacKey_ipad+ , hmacKey_opad+ ) where++import qualified Crypto.Hash.SHA256 as SHA256++-- | Fixed-size context representing the state of a partial HMAC computation+-- with a complete HMAC key and a partial message parameter.++data HmacCtx = HmacCtx+ { hmacCtx_ipad :: !SHA256.Ctx+ , hmacCtx_opad :: !SHA256.Ctx+ } deriving (Eq)++-- | 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++newtype HmacKey = HmacKey { hmacKey_run :: HmacCtx } deriving (Eq)++hmacKey_ipad :: HmacKey -> SHA256.Ctx+hmacKey_ipad (HmacKey ctx) = hmacCtx_ipad ctx++hmacKey_opad :: HmacKey -> SHA256.Ctx+hmacKey_opad (HmacKey ctx) = hmacCtx_opad ctx
+ lib/Crypto/PHKDF/Primitives.hs view
@@ -0,0 +1,479 @@+{-# LANGUAGE OverloadedStrings, BangPatterns, ScopedTypeVariables #-}++{- |++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 "IN\x00\x00"+ outCtr = word32 "OUT\x00"+@++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 "IN\x00\x00"+ outCtr = word32 "OUT\x00"+@++@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.+-}++module Crypto.PHKDF.Primitives+ ( HmacKey()+ , hmacKey_init+ , PhkdfCtx()+ , phkdfCtx_init+ , phkdfCtx_initFromHmacKey+ , phkdfCtx_hmacKey+ , phkdfCtx_resetCtx+ , phkdfCtx_reset+ , phkdfCtx_addArg+ , phkdfCtx_addArgs+ , phkdfCtx_addArgsBy+ , phkdfCtx_finalize+ , phkdfCtx_finalizeHmac+ , phkdfCtx_finalizeHmacCtx+ , phkdfCtx_finalizeStream+ , phkdfCtx_finalizeGen+ , PhkdfSlowCtx()+ , phkdfSlowCtx_extract+ , phkdfSlowCtx_addArg+ , phkdfSlowCtx_addArgs+ , phkdfSlowCtx_finalize+ , phkdfSlowCtx_finalizeStream+ , PhkdfGen()+ , phkdfGen_initFromHmacKey+ , phkdfGen_read+ , phkdfGen_peek+ , phkdfGen_finalizeStream+ ) where++import Data.Bits((.&.))+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Data.Function((&))+import Data.Foldable(Foldable, foldl')+import Data.Int+import Data.Word+import Data.Stream (Stream(..))+import qualified Data.Stream as Stream+import Network.ByteOrder (bytestring32)++import qualified Crypto.Hash.SHA256 as SHA256+import Crypto.PHKDF.HMAC+import Crypto.PHKDF.HMAC.Subtle+import Crypto.PHKDF.Primitives.Subtle+import Crypto.Encoding.PHKDF+import Crypto.Encoding.SHA3.TupleHash++import Control.Exception(assert)++-- | initialize an empty @phkdfStream@ context from a plaintext HMAC key.++phkdfCtx_init :: ByteString -> PhkdfCtx+phkdfCtx_init = phkdfCtx_initFromHmacKey . hmacKey_init++-- | initialize an empty @phkdfStream@ context from a precomputed HMAC key.++phkdfCtx_initFromHmacKey :: HmacKey -> PhkdfCtx+phkdfCtx_initFromHmacKey key =+ PhkdfCtx {+ phkdfCtx_byteLen = 0,+ phkdfCtx_state = hmacKey_ipad key,+ phkdfCtx_hmacKey = key+ }++-- | initialize a new empty @phkdfStream@ context from the HMAC key+-- originally supplied to the context, discarding all arguments already added.++phkdfCtx_reset :: PhkdfCtx -> PhkdfCtx+phkdfCtx_reset = phkdfCtx_initFromHmacKey . phkdfCtx_hmacKey+++-- | initialize a new empty HMAC context from the key originally supplied to+-- the PHKDF context, discarding all arguments already added.++phkdfCtx_resetCtx :: PhkdfCtx -> HmacCtx+phkdfCtx_resetCtx = hmacKey_run . phkdfCtx_hmacKey++-- FIXME? what should happen when the SHA256 counters overflow?++-- | append a single string onto the end of @phkdfStream@'s list of+-- arguments.++phkdfCtx_addArg :: ByteString -> PhkdfCtx -> PhkdfCtx+phkdfCtx_addArg b ctx = phkdfCtx_unsafeFeed [ leftEncodeFromBytes (B.length b), b ] ctx++-- | append zero or more strings onto the end of @phkdfStream@'s list of+-- arguments.++phkdfCtx_addArgs :: Foldable f => f ByteString -> PhkdfCtx -> PhkdfCtx+phkdfCtx_addArgs params ctx = foldl' (flip phkdfCtx_addArg) ctx params++phkdfCtx_addArgsBy :: Foldable f => (a -> ByteString) -> f a -> PhkdfCtx -> PhkdfCtx+phkdfCtx_addArgsBy f params ctx0 = foldl' delta ctx0 params+ where delta ctx a = phkdfCtx_addArg (f a) ctx+++-- | 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_finalize :: (Int -> ByteString) -> Word32 -> ByteString -> PhkdfCtx -> ByteString+phkdfCtx_finalize genFillerPad counter tag ctx =+ phkdfCtx_finalizeGen genFillerPad counter tag ctx &+ phkdfGen_read &+ fst++-- | 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_finalizeHmacCtx :: PhkdfCtx -> HmacCtx+phkdfCtx_finalizeHmacCtx ctx =+ (phkdfCtx_resetCtx ctx) {+ hmacCtx_ipad = phkdfCtx_state ctx+ }++-- | "improperly" close out a 'PhkdfCtx' as if it were a call to @hmac@ instead+-- of @phkdfStream@, though with a TupleHash message encoding.++phkdfCtx_finalizeHmac :: PhkdfCtx -> ByteString+phkdfCtx_finalizeHmac = hmacCtx_finalize . phkdfCtx_finalizeHmacCtx++-- | close out a @phkdfStream@ context with a given counter and tag++phkdfCtx_finalizeStream :: (Int -> ByteString) -> Word32 -> ByteString -> PhkdfCtx -> Stream ByteString+phkdfCtx_finalizeStream genFillerPad counter0 tag ctx =+ phkdfCtx_finalizeGen genFillerPad counter0 tag ctx &+ phkdfGen_finalizeStream++phkdfCtx_finalizeGen :: (Int -> ByteString) -> Word32 -> ByteString -> PhkdfCtx -> PhkdfGen+phkdfCtx_finalizeGen genFillerPad counter0 tag ctx =+ PhkdfGen+ { phkdfGen_hmacKey = phkdfCtx_hmacKey ctx+ , phkdfGen_extTag = extendTag tag+ , phkdfGen_counter = counter0+ , phkdfGen_state = ""+ , phkdfGen_initCtx = Just context0+ }+ where+ n = phkdfCtx_byteLen ctx+ endPadLen = fromIntegral ((31 - n) .&. 63)++ endPadding = genFillerPad endPadLen++ ctx' = phkdfCtx_unsafeFeed ["\x00",endPadding] ctx++ endPaddingIsValid = phkdfCtx_byteLen ctx' `mod` 64 == 32+ && B.length endPadding == endPadLen++ context0 = assert endPaddingIsValid $ phkdfCtx_state ctx'++phkdfGen_initFromHmacKey :: ByteString -> Word32 -> ByteString -> HmacKey -> PhkdfGen+phkdfGen_initFromHmacKey state0 counter0 tag hmacKey = PhkdfGen+ { phkdfGen_hmacKey = hmacKey+ , phkdfGen_extTag = extendTag tag+ , phkdfGen_counter = counter0+ , phkdfGen_state = state0+ , phkdfGen_initCtx = Just $ hmacKey_ipad hmacKey+ }++phkdfGen_peek :: PhkdfGen -> Maybe ByteString+phkdfGen_peek gen =+ case phkdfGen_initCtx gen of+ Nothing -> Just $ phkdfGen_state gen+ Just _ -> Nothing++phkdfGen_finalizeHmacCtx :: PhkdfGen -> HmacCtx+phkdfGen_finalizeHmacCtx gen =+ (hmacKey_run (phkdfGen_hmacKey gen)) {+ hmacCtx_ipad = SHA256.update ipad (phkdfGen_state gen)+ }+ where+ ipad =+ case phkdfGen_initCtx gen of+ Nothing -> hmacCtx_ipad . hmacKey_run $ phkdfGen_hmacKey gen+ Just x -> x++phkdfGen_read :: PhkdfGen -> (ByteString, PhkdfGen)+phkdfGen_read gen = (state', gen')+ where+ state' =+ phkdfGen_finalizeHmacCtx gen &+ hmacCtx_updates [ bytestring32 (phkdfGen_counter gen)+ , phkdfGen_extTag gen+ ] &+ hmacCtx_finalize++ hmacKey = phkdfGen_hmacKey gen++ gen' = PhkdfGen+ { phkdfGen_hmacKey = hmacKey+ , phkdfGen_initCtx = Nothing+ , phkdfGen_state = state'+ , phkdfGen_counter = phkdfGen_counter gen + 1+ , phkdfGen_extTag = phkdfGen_extTag gen+ }++phkdfGen_finalizeStream :: PhkdfGen -> Stream ByteString+phkdfGen_finalizeStream = Stream.unfold phkdfGen_read++-- | 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_extract :: (Int -> ByteString) -> Word32 -> ByteString -> ByteString -> Word32 -> PhkdfCtx -> PhkdfSlowCtx+phkdfSlowCtx_extract genFillerPad counter tag fnName rounds ctx0 = out+ where+ (Cons block0 innerStream) = phkdfCtx_finalizeStream genFillerPad counter tag ctx0++ approxByteLen = ((fromIntegral rounds :: Int64) + 1) * 64 + 32+ encodedLengthByteLen = lengthOfLeftEncodeFromBytes approxByteLen+ exactByteLen = approxByteLen - fromIntegral encodedLengthByteLen+ encodedLength = leftEncodeFromBytes exactByteLen+ -- Encoding the length won't ever cause the length of encodedLength+ -- to change, which would cause the loss of buffer alignment.+ -- Fact:+ -- lengthOfLeftEncodeBytes exactByteLen+ -- == lengthOfLeftEncodeBytes approxByteLen+ -- Because:+ -- approxByteLen >= 96+ -- && approxByteLen <= 2^32 * 64 + 32+ -- && approxByteLen `mod` 64 == 32++ extFnNameByteLen = 32 - encodedLengthByteLen++ fnNameByteLen = B.length fnName++ extFnName =+ if fnNameByteLen >= extFnNameByteLen+ then encodedLength <> B.take extFnNameByteLen fnName+ else let padLen = 31 - encodedLengthByteLen - fnNameByteLen+ pad = cycleByteStringWithNull tag padLen+ in B.concat [encodedLength, fnName, "\x00", pad]++ outerCtx =+ phkdfCtx_reset ctx0 &+ phkdfCtx_unsafeFeed [extFnName, block0]++ fillerTag = flip cycleByteString 32 $ B.concat+ [ tag, "\x00", fnName, "\x00"]++ go n !ctx ~(Cons block stream)+ | n <= 0 = PhkdfSlowCtx {+ phkdfSlowCtx_phkdfCtx = phkdfCtx_unsafeFeed [fillerTag] ctx,+ phkdfSlowCtx_counter = counter + rounds + 1,+ phkdfSlowCtx_tag = tag+ }+ | otherwise = go (n-1) (phkdfCtx_unsafeFeed [fillerTag, block] ctx) stream++ out = go rounds outerCtx innerStream++-- | Add a tweak to a call to @phkdfSlowExtract@.++phkdfSlowCtx_addArg :: ByteString -> PhkdfSlowCtx -> PhkdfSlowCtx+phkdfSlowCtx_addArg = phkdfSlowCtx_lift . phkdfCtx_addArg++-- | Add zero or more tweaks to a call to @phkdfSlowExtract@.++phkdfSlowCtx_addArgs :: Foldable f => f ByteString -> PhkdfSlowCtx -> PhkdfSlowCtx+phkdfSlowCtx_addArgs = phkdfSlowCtx_lift . phkdfCtx_addArgs++-- | finalize a call to @phkdfSlowExtract@, discarding all but the first block+-- of the output stream++phkdfSlowCtx_finalize :: (Int -> ByteString) -> PhkdfSlowCtx -> ByteString+phkdfSlowCtx_finalize genFillerPad = Stream.head . phkdfSlowCtx_finalizeStream genFillerPad++-- | finalize a call to @phkdfSlowExtract@++phkdfSlowCtx_finalizeStream :: (Int -> ByteString) -> PhkdfSlowCtx -> Stream ByteString+phkdfSlowCtx_finalizeStream genFillerPad ctx =+ phkdfCtx_finalizeStream genFillerPad+ (phkdfSlowCtx_counter ctx)+ (phkdfSlowCtx_tag ctx)+ (phkdfSlowCtx_phkdfCtx ctx)
+ lib/Crypto/PHKDF/Primitives/Assert.hs view
@@ -0,0 +1,42 @@+module Crypto.PHKDF.Primitives.Assert where++import Data.Bits+import Data.Word+import Crypto.PHKDF.Primitives.Subtle++phkdfCtx_assertBufferPosition' :: Word64 -> PhkdfCtx -> PhkdfCtx+phkdfCtx_assertBufferPosition' n ctx+ | len .&. 63 /= n .&. 63+ = error ("phkdf buffer position mismatch: " ++ show len ++ " /= " ++ show n ++ " (mod 64)")+ | otherwise = ctx+ where len = phkdfCtx_byteLen ctx++phkdfSlowCtx_assertBufferPosition' :: Word64 -> PhkdfSlowCtx -> PhkdfSlowCtx+phkdfSlowCtx_assertBufferPosition' n ctx+ | len .&. 63 /= n .&. 63+ = error ("phkdf buffer position mismatch: " ++ show len ++ " /= " ++ show n ++ " (mod 64)")+ | otherwise = ctx+ where len = phkdfCtx_byteLen (phkdfSlowCtx_phkdfCtx ctx)++-- TODO: set up a cabal flag and CPP to select between assertions enabled/not++{--}++phkdfCtx_assertBufferPosition :: Word64 -> PhkdfCtx -> PhkdfCtx+phkdfCtx_assertBufferPosition = phkdfCtx_assertBufferPosition'+++phkdfSlowCtx_assertBufferPosition :: Word64 -> PhkdfSlowCtx -> PhkdfSlowCtx+phkdfSlowCtx_assertBufferPosition = phkdfSlowCtx_assertBufferPosition'++--}++{--++phkdfCtx_assertBufferPosition :: Word64 -> PhkdfCtx -> PhkdfCtx+phkdfCtx_assertBufferPosition _ = id++phkdfSlowCtx_assertBufferPosition :: Word64 -> PhkdfSlowCtx -> PhkdfSlowCtx+phkdfSlowCtx_assertBufferPosition _ = id++--}
+ lib/Crypto/PHKDF/Primitives/Subtle.hs view
@@ -0,0 +1,69 @@+module Crypto.PHKDF.Primitives.Subtle+ ( PhkdfCtx(..)+ , phkdfCtx_unsafeFeed+ , PhkdfSlowCtx(..)+ , phkdfSlowCtx_lift+ , PhkdfGen(..)+ ) where++import Prelude hiding (null)+import qualified Crypto.Hash.SHA256 as SHA256+import Crypto.PHKDF.HMAC (HmacKey)+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Data.Foldable(foldl', null)+import Data.Word++-- I should be using the counter inside the sha256 ctx, but this is a Proof of Concept++-- TODO: should phkdfCtx_length count bytes, or bits? Double-check how SHA256 internal counter+-- works. Decide how this should work. Then export it from Primitives module.+-- For truly bulletproof code, we probably need to be returning (Maybe Ctx), so that we don't+-- overflow SHA256's internal counter. This would be a bit of a conceptual problem with the+-- cryptohash-style interface I'm mimicking, not to mention the cryptohash implementation I+-- am depending upon.++-- note that there's an offset error w.r.t the sha256 internal counter and phkdfCtx_length, but+-- it's always 64 bytes. As the internals of this module only care about the internal counter+-- modulo 64, this doesn't matter. However we should probably export the SHA256 counter itself++data PhkdfCtx = PhkdfCtx+ { phkdfCtx_byteLen :: !Word64+ , phkdfCtx_state :: !SHA256.Ctx+ , phkdfCtx_hmacKey :: !HmacKey+ }++data P = P !Word64 !SHA256.Ctx++phkdfCtx_unsafeFeed :: Foldable f => f ByteString -> PhkdfCtx -> PhkdfCtx+phkdfCtx_unsafeFeed strs ctx0 =+ if null strs then ctx0+ else ctx0 {+ phkdfCtx_byteLen = byteLen',+ phkdfCtx_state = state'+ }+ where+ delta (P len ctx) str = P (len + (fromIntegral (B.length str))) (SHA256.update ctx str)++ p0 = P (phkdfCtx_byteLen ctx0) (phkdfCtx_state ctx0)++ P byteLen' state' = foldl' delta p0 strs++data PhkdfSlowCtx = PhkdfSlowCtx+ { phkdfSlowCtx_phkdfCtx :: !PhkdfCtx+ , phkdfSlowCtx_counter :: !Word32+ , phkdfSlowCtx_tag :: !ByteString+ }++phkdfSlowCtx_lift :: (PhkdfCtx -> PhkdfCtx) -> PhkdfSlowCtx -> PhkdfSlowCtx+phkdfSlowCtx_lift f ctx = ctx {+ phkdfSlowCtx_phkdfCtx = f (phkdfSlowCtx_phkdfCtx ctx)+ }++data PhkdfGen = PhkdfGen+ { phkdfGen_hmacKey :: !HmacKey+ , phkdfGen_extTag :: !ByteString+ , phkdfGen_counter :: !Word32+ , phkdfGen_state :: !ByteString+ , phkdfGen_initCtx :: !(Maybe SHA256.Ctx)+ }
+ phkdf-test-vectors.json view
@@ -0,0 +1,271 @@+[{+ "name":"phkdf first light",+ "args": {+ "username":"Yuri",+ "password":"default remote access code",+ "long-tag":"Please leave the location of America's nuclear wessels after the beep.",+ "domain-tag":"1-800-CALL-SPY",+ "rounds":1998+ },+ "results":{+ "phkdf-pass":"85c2cce906bbce9b507de1ddb415c9703d635d4f14f3e81c1299ceaa00d86541129d9020319d3fb0053c69ce6f0d279f6c4dd28bda70d3c1c0f91e259a8a15e0",+ "phkdf-simple":"3d5628f4834c15a114f0055d2da659ea6564c7fc574bf63c7f413810fda442712631cabfd1a1b8d0a21b70b471d070549ea040d16a00b6112e9c59ca78e27d95"+ }+},{+ "name":"phkdf first light, elaborated",+ "args": {+ "username":"Yuri",+ "password":"default remote access code",+ "credentials":["The Indiana Academy for Science, Mathematics, and Humanities"],+ "long-tag":"Please leave the location of America's nuclear wessels after the beep.",+ "domain-tag":"1-800-CALL-SPY",+ "seed-tags":["United States Army Counterintelligence Tip Line"],+ "role":["prankster"],+ "echo-tag":"Star Trek IV: The Voyage Home https://www.youtube.com/watch?v=MdSJFrhb-HM",+ "rounds":1998+ },+ "results":{+ "phkdf-pass":"61f3c4a90166fc2251b80af8bad4810c0b9dce56c7ddfbac45c3f7b96c12ca67d2d2d4ee9f92483b045b67dba3de5b1c51abaf0be8580f4ed9c061c255912256",+ "phkdf-simple":"a2150b311d2727b8592265150935d910e9717fdab169a03149ff07c49bee8fb2aa4f74513251a4546cd8a35b347a08771b5305105cc6fb393e779669b632b7f5"+ }+},{+ "name":"phkdf unicode test, inset naval story with a reference",+ "args": {+ "username":"Р-360 «Нептун»",+ "password":"Русский военный корабль, иди нахуй",+ "seed-tags":["Державне Київське конструкторське бюро «Луч»"],+ "echo-tag":"Збройні Cили України «ЗСУ»",+ "domain-tag":"Україна",+ "rounds":2022+ },+ "results":[+ {"phkdf-pass":"18d55590dd5f6665aa4bdeaab904f4c1b3a7090a7e1d597d3a8a707b0bf4aacc60c16809b066faa2e9aa08c5757f0a92bbe536249dc3cce50e16f83e9f9b066c",+ "phkdf-simple":"e1b2b5b36693e30451c15699e8ac87d5642ac0f6a648e45f3073860fad014243d095798308c76e0a5a860e1695208869cbc0c2e52eb9590d257fa202f8610361"},+ {"args":{"seguid":{"hex":"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40"}},+ "phkdf-pass":"898e874852c46aa168465813e68982d82bdb0fae4ff65ab846a7784c6cf591b97ef79eb647b07896cc099c5c5a3d29acadcf8f62fc0244eb33326c6669816a5d",+ "phkdf-simple":"2b04923bde6d77364c3141603c6f3c87e9e99724d7de408244db3814e3ac22624b89e17454b23921f08dc75b50f81a35ef61ebaf7bf16d8efdb38a949535aa7a"},+ {"args":{"seguid":{"hex":"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041"}},+ "phkdf-pass":"6e21277c164beb8f7d51c3d0766239e668ea4516040a0eb9d526f179c73cee9bbd7a53fabf50166ad01b33111ad20d96dd8736c113fad1f74b18220ab34358ac",+ "phkdf-simple":"f9979f9f5572a0b2906dbd1d09894d40ff916829c4560caaaf57754d1d62ca418f077021c478d92160fef64728bfc86c97548d366e07bd38da256b4b09d8426c"}+ ]+},{+ "name":"phkdf unicode test, lightly extended on top",+ "args": {+ "username":"Лев Николаевич Толстой",+ "password":"Книга моя, как я и ожидал, была задержана русской цензурой, но отчасти вследствие моей репутации как писателя, отчасти потому, что она заинтересовала людей, книга эта распространилась в рукописях и литографиях в России и в переводах за границей и вызвала, с одной стороны, от людей, разделяющих мои мысли, ряд сведений о сочинениях, писанных об этом же предмете, с другой стороны, ряд критик на мысли, высказанные в самой книге.",+ "domain-tag":"Царство Божие внутри вас",+ "long-tag":"В другой брошюре, под заглавием: «Сколько нужно людей, чтобы преобразить злодейство в праведность», Адина Баллу говорит: «Один человек не должен убивать. Если он убил, он преступник, он убийца. Два, десять, сто человек, если они делают это, — они убийцы. Но государство или народ может убивать, сколько он хочет, и это не будет убийство, а хорошее, доброе дело. Только собрать побольше народа, и бойня десятков тысяч людей становится невинным делом. Но сколько именно нужно людей для этого? Вот в чем вопрос. Один не может красть, грабить, но целый народ может. Но сколько именно нужно для этого? Почему 1, 10, 100 человек не должны нарушать закона бога, а очень много могут?»",+ "seed-tags":["Мы будем стараться распространять свои взгляды среди всех людей, к каким бы народам, исповеданиям и слоям общества они ни принадлежали. Для этой цели мы будем устраивать публичные чтения, распространять печатные объявления и брошюры, составлять общества и подавать прошения во всякие правительственные учреждения. Вообще будем стремиться всеми доступными для нас средствами к достижению коренного переворота во взглядах, чувствах и действиях нашего общества относительно греховности насилия по отношению к внешним и внутренним врагам. Принимаясь за это великое дело, мы вполне сознаем, что наша искренность может быть подвергнута жестоким испытаниям. Наша задача может навлечь на нас оскорбления, обиды, страдания и даже смерть. Нас ожидает непонимание, ложное толкование и клевета. Против нас должна подняться буря. Гордость и фарисейство, честолюбие и жестокость, правители и власти, — всё это может соединиться, чтоб уничтожить нас. Таким образом поступали с мессией, которому мы стремимся подражать по мере сил своих. Но нас не пугают эти ужасы. Мы надеемся не на людей, а на всемогущего господа. Если мы отказались от человеческого заступничества, что же может поддержать нас, как не одна вера, побеждающая мир? Мы не будем удивляться тем испытаниям, которым мы подвергнемся, а будем радоваться тому, что удостоимся разделить страдания Христа.","Вильям Ллойд Гаррисон"],+ "rounds":1894+ },+ "results":{+ "phkdf-pass":"14fe2a87d7e8bdaebbbfe8a4a10b20adc9867e4a4ede7609f430e148f774ae8c5f00ab74b9444698bc037b4ef0204af7ebb6dd62076e727e0555af31d2e18d7d",+ "phkdf-simple":"182d3cfa57c4c0f5a672b082f8a1bbd18d25c65c4ef2d701b849f0bd0cf7632a0897dbb5eaf2b0bb1369f36fa6f5748007db1c92c1302f5fbd47bd6394f0840d"+ }+},{+ "name":"xkcd account problems",+ "args":{+ "username":"Cueball",+ "password":{"hex":"7061737300776f7264"},+ "credentials":["input method not included"],+ "domain-tag":"https://xkcd.com/2700/",+ "rounds":2022+ },+ "results":{+ "phkdf-pass":"120721ef9341e37d582ea8616405e28cd944c672cfbc611aa64296c0cd711967c9b48f3333674eb9af9ed705164cb05f9b1dae5e3a3118ca39f71940476bdf7e",+ "phkdf-simple":"09d0c23f626afddaf7c80afaab025487663eb1e0de54a8e96c7e3ba27c1a15f38f23706f46e7a98b9b7f1c4a7d179327c38fc71939da707b5a8bff4a96b732d5"+ }+},{+ "name":"xkcd password strength #1",+ "args":{+ "username":"Anonymous Coward",+ "password":"Tr0ub4dor&3",+ "domain-tag":"https://xkcd.com/936/",+ "rounds":2011+ },+ "results":{+ "phkdf-pass":"07e0ab490c6afbdf5dc7e0c8adbe4c41c9bf612182cb8760cbe182466ca4d869b93e12148fd07845298e2bba69793016c0b19bb64495fbc8ebf6a647aa436680",+ "phkdf-simple":"702fe250b3ce99795fa2c7f0589e49ddaa2d415b79946665d3110c7c093be15db9b097adc2e27968c3a5e431ed22d613fac919640e2e31ed50ed104341f79ac3"+ }+},{+ "name":"xkcd password strength #2",+ "args":{+ "username":"Randall Munroe",+ "password":"correct horse battery staple",+ "tags":[+ "Diceware","https://theworld.com/~reinhold/diceware.html","Arnold Reinhold",+ "https://www.eff.org/dice","Electronic Frontier Foundation"+ ],+ "domain-tag":"xkcd.com/936/",+ "rounds":2011+ },+ "results":[+ {"phkdf-pass":"9e7c2a86e38e68d82f29eb6c5f5e347962ed247373e029edf5c6c4ca1cc3102eb04fcdff1f8ee4f002845b2e0f82bfab0a0a8499a9d7b984094057be9df70396",+ "phkdf-simple":"94dc6265c671726b20377acc5e3745df3678d61d9cbe16ae9a648b64d86cc0f2addc06f3575f4e9f964ac698a24227bd066f5ca5a23cd2205aab9deec0979776"},+ {"args":{"rounds":255},+ "phkdf-pass":"549d63a1236da84b2c1ba408ebb5b5151ea77e3be23b8430c0f97c7ba21c659648c2314c005d677025e57126bb97ff1c4d2a643fed3f2f5df55b0f0a769f453f",+ "phkdf-simple":"3c3c9b609dd876c57ab442b12327921aea05a0fc0d15cd7506444e5d5138c3d6be563fcc5266dd9b1b1c4498d7e7686b931ac565d0aefbc15988c0c5dd794f62"},+ {"args":{"rounds":256},+ "phkdf-pass":"dc0b80f7b8a2e1d968c7e3dc5a1bc8df42c9a215194bf2d311559be5071449690c0ecd3fce96d462f05036e732631e7420194d0177f17d01df58844daa135d5f",+ "phkdf-simple":"e3a57824759647037e9ffd41897d35cf407e27018aed9059b5087b99d9029f48df8cd41069ae6b540fb6c7454678ca89d7600c30c8cc352f31f196ec609fd680"},+ {"args":{"rounds":65535},+ "phkdf-pass":"9c855afd60a462983efa18eb154aa17dcb49a6cb971624c105d9d0a3a9c50ff870267d1893f694128d665103aa4dd0ccb38471bc149987489f90c129dcbd4226",+ "phkdf-simple":"df7e0b5a9446d7cf591c6000700acfd0bb77bd79eb7eb5716028dc4aa0f059260b8500ac34ab7827a515359ed6c08ed492e57a65204322adfdf85a8a4a0cd362"},+ {"args":{"rounds":65536},+ "phkdf-pass":"a358062764aebe3cbb48975087c4f4c8e8566ae4374e9c11ea6174e78276d8479ffb24f4243028e9e6578a93472f5a9074a58820b7d25c3d588f3be344681db7",+ "phkdf-simple":"4296add104a87cf36097b419b7d746ff4ee7dd2b74ae86f108a01e2d3d90442abf0ae28bb20d0b5df93ee9052ce7f1684c506a77ca9479448cf72aac01031b34"},+ {"args":{"rounds":250000},+ "phkdf-pass":"c103e328eb7cc399cd6a3428b1fca25492916afe4e9a58b127b45f88831fd4b75fb9a2305fa7e1053445df26fcdaa38cd4a1923374a496a692e63b12541228dd",+ "phkdf-simple":"5d45e6a76ead2e32f87af30717fc740cd885460cd6e9bb3992d3b45f4b1fee2011e93aaf41c5adee97956aeee59c911130da5c2217bb0b5f9aa589cb5ccefac5"}+ ]+},{+ "name":"xkcd password reuse #1",+ "args":{+ "username":"Imperfect User",+ "password":"Password reuse between multiple deployments of the G3P can be safe(-ish), but adds risk and fragility, often meaningfully so. See https://xkcd.com/792/",+ "domain-tag":"Every authentication service provider inherently has offline cracking attacks against its own password database, even if PAKE is used. When you set a password, you are inherently providing that service with the means to verify your password. If a password is reused and its entropy is low enough to be cracked, access to the right authentication database can be used to bootstrap attacks on other accounts.",+ "rounds":2010+ },+ "results":{+ "phkdf-pass":"3fddedbcff2fa169a25ea4c2371aba40c7a318203c83ae798571edf152548a888b3abffb4a12a11eaddbcb07ab76cce6fe1c65c2877f88b982fa7697f3c79755",+ "phkdf-simple":"8f0d5efddc97d3243b45641646ea259a4f7d5a2b30e6712f003120c59cf1ea89a2441ed8d851978180cdd3a1bfb6716879b3cbfa0449762fd76304adc42924f8"+ }+},{+ "name":"xkcd password reuse #2",+ "args":{+ "username":"Imperfect User",+ "password":"Password reuse between multiple deployments of the G3P can be safe(-ish), but adds risk and fragility, often meaningfully so. See https://xkcd.com/792/",+ "domain-tag":"How do you know that your password is really being prehashed on your own device? Even if this does indeed happen, how do you know that it happens every single time? In the context of a webapp, it's relatively easy to covertly inject arbitrary code into selected page loads. Thus it is possible that the first 99 times you log into a webpage, you get a completely legitimate login page that properly prehashes your password in your browser. But on that 100th login, maybe you are instead presented with a webpage that steals your password in addition to its normal login functionality. It would be relatively easy to make such an attack covertly in such a way that is likely to turn surreptitious, leaving no trace that the attack ever happened. This is easiest to accomplish if the attacker is providing the webapp, or has the cooperation of those who do. However this isn't always necessary. At the start of the Arab Spring, Facebook's login page was not delivered over a TLS-authenticated channel. This oversight allowed state actors to steal user's passwords by injecting their own malicious code into Facebook's login page as it traversed Internet Service Providers inside their respective countries. Installed applications can be somewhat more resistant to selective-pageload style attacks, but more generally, malware on endpoints is a significant attack vector to be concerned about. Shoulder-surfing attacks upon password entry are another. Consider these things carefully when making decisions with security implications, such as whether or not to reuse a password and in what context(s).",+ "rounds":2010+ },+ "results":{+ "phkdf-pass":"6af1c0ebc88ffa062e64c8b8e8036afd67541a82f9ac6294e8f6484b9374b42e267b493673768da981ed39be7b6dd1bcf6969553c12f4ece2bf326849ced5c76",+ "phkdf-simple":"2557aa139b168ff55c0ef4fc7f461d196b71d65285dc170bb12400be213f45b784bd867bf05043321259a22e2db1dd7ce1437b2ad1e2666538c88ed7761a1925"+ }+},{+ "name":"trivial inputs",+ "args":{+ "username":"",+ "password":"",+ "credentials":[],+ "seguid":"",+ "domain-tag":"",+ "long-tag":"",+ "echo-tag":"",+ "rounds":0,+ "role":[],+ "seed-tags":[]+ },+ "results":{+ "phkdf-pass":"c9ff8125f0f0b41ff5cfc8898dd8c6b845cfc2087d061d79909180c2ec7655d122bf83e7530f71b14188d05c601789451d49a499ef13a818e29f878bc6d0886d",+ "phkdf-simple":"f6a475fb6b69e1f430be7ce0f2239734cd47ad74befcd9138ac540fabb2db9a9743ced90de72317dd92f12aefc4532beb33919a41a550f5db3aa9c8f755c889b"+ }+},{+ "name":"password padding",+ "args":{+ "username":"tests with very long inputs",+ "password":"back references",+ "domain-tag":"your-domain-name-here.example",+ "long-tag":{"ref":"phkdf first light, elaborated","len":5112},+ "rounds":255+ },+ "results":[+ {"phkdf-pass":"8ac05ffc273d77cf3e9f6b8774846d468a7e3161eafd121eae55b64faf87b846",+ "phkdf-simple":"cc69d0eef4cd4ae9dc8f48e40cf0b66559ab658c15ca14113e93f200035df26f"},+ {"args":{"long-tag":{"ref":"phkdf first light, elaborated","len":5113}},+ "phkdf-pass":"012ed22757a0ef88a5f70f413670b9cd4fdebddcd1ea8cb20255af136d9e0ae8",+ "phkdf-simple":"dc0e25c54ccfb6219ceb0432216594310a72662f26fb09f806a4fee7f46f44c5"},+ {"args":{+ "username":{+ "ref":"phkdf unicode test, lightly extended on top",+ "len":3042,+ "algorithm":"phkdf-simple"+ }},+ "phkdf-pass":"c3d2f4a40940a2594f6bb14fc61875b90c5a2bdceb987cedfd1ae0ba7180f099",+ "phkdf-simple":"df7cfabc0d5b3c917351e5b5ea080ce75a9207656ee2d1c2a24020776f9abb93"+ },+ {"args":{+ "username":{+ "ref":"phkdf unicode test, lightly extended on top",+ "len":3043,+ "algorithm":"phkdf-simple"+ }},+ "phkdf-pass":"e6fa06a3c290fa89717704357033fd2a22616612d486c5d5659a1af71017f5bd",+ "phkdf-simple":"0db07b29582746244cf97cd2e9c33c4ade4f962f9b10dc068e0e241befc371c3"+ },+ {"args":{+ "username":{+ "ref":"phkdf unicode test, lightly extended on top",+ "len":3042,+ "algorithm":"phkdf-simple"+ },+ "password":{+ "ref":"phkdf unicode test, inset naval story with a reference",+ "len":101,+ "index":1+ }},+ "phkdf-pass":"19e939911583b77ca7135c5ef6b2aecf113c5083e9aa4a52479a5a8cc543caa3",+ "phkdf-simple":"00bfac3be31d3bd2ca166529ea314490d2df56f833a7c1f5a14716f55ce4a993"+ },+ {"args":{+ "username":{+ "ref":"phkdf unicode test, lightly extended on top",+ "len":3042,+ "algorithm":"phkdf-simple"+ },+ "password":{+ "ref":"phkdf unicode test, inset naval story with a reference",+ "len":102,+ "index":1+ }},+ "phkdf-pass":"892bdd032e9a71ef92d6fcbfea5c753058dc1c13c4ceeac00fa8eb75bb2a897c",+ "phkdf-simple":"0984cf6357aaee83b273081cac1791d61aee1839217472c13477834293d04229"+ },+ {"args":{+ "long-tag":{+ "ref":"phkdf first light, elaborated",+ "len":5049,+ "algorithm":"phkdf-simple"+ },+ "username":{+ "ref":"phkdf unicode test, lightly extended on top",+ "len":3106+ }+ },+ "phkdf-pass":"fa8d31b1bef03ddc5732df60f514df0d91ed4cb08664a932821a80c331607b60",+ "phkdf-simple":"0d58861c36abeb344feed044ce34b19ef68abdbf96e2dbe9afcc6768d17f0dc3"+ },+ {"args":{+ "long-tag":{+ "ref":"phkdf first light, elaborated",+ "len":5049,+ "algorithm":"phkdf-simple"+ },+ "username":{+ "ref":"phkdf unicode test, lightly extended on top",+ "len":3106+ },+ "password":{+ "ref":"phkdf unicode test, inset naval story with a reference",+ "len":164,+ "index":2+ }},+ "phkdf-pass":"fe8dd527cfec4dbea25f3d09e75aa68e529d92b4c5e1c4520b074823977b1cce",+ "phkdf-simple":"b6b06d79e41921a7dabcc196e567325ee5322cf051816e86e5c0b5b19ebc62e9"+ },+ {"args":{+ "long-tag":{"ref":"phkdf first light, elaborated","len":5049},+ "username":{+ "ref":"phkdf unicode test, lightly extended on top",+ "len":3106+ },+ "password":{+ "ref":"phkdf unicode test, inset naval story with a reference",+ "len":165,+ "index":2+ }},+ "phkdf-pass":"35940281d66bf0b53dde2b5627c7eab749931511bd0883e8cb888ce72b08d288",+ "phkdf-simple":"d936b839c5ff1a8bfef208b9f8c2918bbc9c41ba7db9d75b9e17df94f2221fc7"+ }+ ]+}]
+ phkdf.cabal view
@@ -0,0 +1,69 @@+name: phkdf+version: 0.0.0.0+synopsis:+ Toolkit for self-documenting password hash and key derivation functions.++description:+ Inspired by PBKDF2, HKDF, and TupleHash. Uses HMAC-SHA256 as a primitive.+ + This is primarily intended to be a highly reliable reference implementation+ for the underlying PHKDF primitives. It also aspires to be production+ ready-ish. The main limitation is that it implemented without mutation,+ meaning that potentially sensitive secrets persist in memory longer than+ necessary.++license: Apache-2.0+license-file: LICENSE+author: Leon P Smith+maintainer: Auth Global+copyright: Auth Global+category: Cryptography+build-type: Simple+extra-source-files: ChangeLog.md+ phkdf-test-vectors.json+cabal-version: >=1.10++library+ exposed-modules:+ Crypto.PHKDF+ Crypto.Encoding.PHKDF+ Crypto.PHKDF.Primitives+ Crypto.PHKDF.Primitives.Assert+ Crypto.PHKDF.Primitives.Subtle+ Crypto.PHKDF.HMAC+ Crypto.PHKDF.HMAC.Subtle++ build-depends: base < 5+ , bytestring+ , cryptohash-sha256+ , network-byte-order+ , Stream+ , tuplehash-utils+ , vector++ ghc-options: -Wall++ hs-source-dirs: lib+ default-language: Haskell2010++test-suite test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs++ other-modules: HMAC+ PHKDF++ build-depends: base < 5+ , aeson >= 2+ , base16+ , bytestring+ , containers+ , phkdf+ , Stream+ , tasty+ , tasty-hunit+ , text+ , vector++ default-language: Haskell2010
+ test/HMAC.hs view
@@ -0,0 +1,99 @@+-- Test Vectors for HMAC-SHA256++{-# LANGUAGE OverloadedStrings #-}++module HMAC where++import Data.ByteString(ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as B+import Data.Function((&))+import Test.Tasty+import Test.Tasty.HUnit++import Crypto.PHKDF.HMAC++d :: ByteString -> ByteString+d = B.decodeBase16Lenient++tests :: [TestTree]+tests =+ [ testGroup "rfc4231 test vectors"+ [ testCase ("rfc4231-" ++ show n) (run x)+ | (n,x) <- zip [1..] testVectors+ ]+ ]+ where+ hmac :: ByteString -> ByteString -> ByteString+ hmac k m = hmacCtx_init k & hmacCtx_update m & hmacCtx_finalize+ run x = B.encodeBase16 (hmac (key x) (msg x)) @?= B.encodeBase16 (out x)++testVectors :: [TestVector]+testVectors =+ [ rfc4231_testCase1+ , rfc4231_testCase2+ , rfc4231_testCase3+ , rfc4231_testCase4+ , rfc4231_testCase5+ , rfc4231_testCase6+ , rfc4231_testCase7+ ]++data TestVector = TestVector+ { key :: !ByteString+ , msg :: !ByteString+ , out :: !ByteString+ }++rfc4231_testCase1 = TestVector+ { key = B.replicate 20 0x0b+ , msg = "Hi There"+ , out = d "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"+ }++rfc4231_testCase2 = TestVector+ { key = "Jefe"+ , msg = "what do ya want for nothing?"+ , out = d "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"+ }++rfc4231_testCase3 = TestVector+ { key = B.replicate 20 0xaa+ , msg = B.replicate 50 0xdd+ , out = d "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe"+ }++rfc4231_testCase4 = TestVector+ { key = d "0102030405060708090a0b0c0d0e0f10111213141516171819"+ , msg = B.replicate 50 0xcd+ , out = d "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b"+ }++{-++RFC4231 doesn't provide the second half of the output. This seem odd.+Unlike HKDF, truncation is not explicitly part of the HMAC interface.+Is this test vector supposed teach that this is a safe way to use HMAC?++To avoid unnecessary complications here, I just provided the (unofficial)+second half of the output.++-}++rfc4231_testCase5 = TestVector+ { key = B.replicate 20 0x0c+ , msg = "Test With Truncation"+ , out = d "a3b6167473100ee06e0c796c2955552bfa6f7c0a6a8aef8b93f860aab0cd20c5"+ }++rfc4231_testCase6 = TestVector+ { key = B.replicate 131 0xaa+ , msg = "Test Using Larger Than Block-Size Key - Hash Key First"+ , out = d "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"+ }++rfc4231_testCase7 = TestVector+ { key = B.replicate 131 0xaa+ , msg = "This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm."+ , out = d "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2"+ }
+ test/Main.hs view
@@ -0,0 +1,15 @@+import Test.Tasty+import Data.Monoid+import qualified HMAC+import qualified PHKDF++main = do+ let fileName = PHKDF.testVectorDefaultFileName+ phkdfTvs <- PHKDF.readTestVectorsFromFile fileName+ defaultMain (tests phkdfTvs)++tests :: (String, Either String PHKDF.TestVectors) -> TestTree+tests phkdfTvs = testGroup "Test" [+ testGroup "hmac" HMAC.tests,+ testGroup "phkdf" [PHKDF.testFile phkdfTvs]+ ]
+ test/PHKDF.hs view
@@ -0,0 +1,393 @@+{-# LANGUAGE OverloadedStrings, LambdaCase, RecordWildCards, ViewPatterns, ScopedTypeVariables #-}++module PHKDF where++import Control.Exception(try)+import Control.Applicative+import Data.Aeson(Object, Value(..), parseJSON, (.:), (.:?), withObject)+import Data.Aeson.Types(Parser)+import qualified Data.Aeson as Aeson+import Data.Aeson.Key(Key)+import qualified Data.Aeson.Key as K+import Data.Aeson.KeyMap(KeyMap)+import qualified Data.Aeson.KeyMap as KM+import Data.ByteString(ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as B+import Data.Function(fix)+import Data.Map(Map)+import qualified Data.Map as Map+import Data.Maybe(fromMaybe)+import Data.Text(Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Data.Text.Encoding.Base16 as T+import Data.Stream(Stream(..))+import qualified Data.Stream as S+import Data.Vector(Vector, (!))+import qualified Data.Vector as V++import Debug.Trace++import Crypto.PHKDF+import Test.Tasty+import Test.Tasty.HUnit++type Args = KeyMap Val++data Val+ = Int !Int+ | Str !ByteString+ | Vec !(Vector ByteString)+ | Nul+ | Ref !TestId !Int+ deriving (Show)++data Result = Result+ { result_args :: !Args+ , result_hashes :: !(KeyMap ByteString)+ }++data TestVector = TestVector+ { testVector_name :: !Text+ , testVector_arguments :: !Args+ , testVector_results :: !(Vector Result)+ }++data TestId = TestId+ { testId_name :: !Text+ , testId_index :: !Int+ , testId_algorithm :: !Text+ } deriving (Eq, Ord, Show)++data SimpleTestVector = SimpleTestVector+ { simpleTestVector_id :: !TestId+ , simpleTestVector_arguments :: !Args+ , simpleTestVector_result :: !ByteString+ }++type TestVectors = Vector TestVector++type SimpleTestVectors = Vector SimpleTestVector++type ResultEnv = Map TestId (Either String (Stream ByteString))++blankResult :: Result+blankResult = Result+ { result_args = KM.empty+ , result_hashes = KM.fromList [ ("phkdf-pass",""),("phkdf-simple","") ]+ }++flattenTestVectors :: TestVectors -> SimpleTestVectors+flattenTestVectors tvs =+ V.fromList $+ [ SimpleTestVector+ { simpleTestVector_id =+ TestId { testId_name = testVector_name tv+ , testId_index = i+ , testId_algorithm = alg+ }+ , simpleTestVector_arguments = args+ , simpleTestVector_result = outHash+ }+ | tv <- V.toList tvs+ , (i, res) <- zip [0..] (seedEmpty (V.toList (testVector_results tv)))+ , let args = KM.union (result_args res) (testVector_arguments tv)+ , (K.toText -> alg, outHash) <- KM.toAscList (result_hashes res)+ ]+ where+ seedEmpty xs+ | null xs = [blankResult]+ | otherwise = map addBlankResult xs+ addBlankResult x+ | null (result_hashes x) = x { result_hashes = result_hashes blankResult }+ | otherwise = x++genResultEnv :: SimpleTestVectors -> ResultEnv+genResultEnv tvs =+ -- FIXME? The resulting scoping rules in the test vector file is analogous+ -- to Haskell or scheme's letrec, whereas I really want let* here+ fix $ \resultEnv ->+ Map.fromList $+ [ (simpleTestVector_id tv, interpret tv resultEnv)+ | tv <- V.toList tvs+ ]+ where+ interpret tv resultEnv+ | alg == "phkdf-pass" =+ case getPhkdfPassInputs resultEnv args of+ Just inputs -> Right (uncurry3 phkdfPass inputs)+ Nothing -> Left "arguments not parsed"+ | alg == "phkdf-simple" =+ case getPhkdfSimpleInputs resultEnv args of+ Just inputs -> Right (uncurry phkdfSimple inputs)++ Nothing -> Left "arguments not parsed"+ | otherwise = Left "algorithm name not recognized"+ where+ alg = testId_algorithm $ simpleTestVector_id tv+ args = simpleTestVector_arguments tv++genSimpleTestCases :: SimpleTestVectors -> ResultEnv -> [ TestTree ]+genSimpleTestCases tvs resultEnv =+ [ testCase testName $ runTest tv resultEnv+ | tv <- V.toList tvs+ , let testId = simpleTestVector_id tv+ name = T.unpack (testId_name testId)+ idx = show (testId_index testId)+ alg = T.unpack (testId_algorithm testId)+ testName = name ++ " | " ++ idx ++ " " ++ alg+ ]++genTestCases :: TestVectors -> [ TestTree ]+genTestCases tvs = genSimpleTestCases stvs (genResultEnv stvs)+ where+ stvs = flattenTestVectors tvs++uncurry3 :: (a -> b -> c -> d) -> (a,b,c) -> d+uncurry3 f (a,b,c) = f a b c++instance Aeson.FromJSON Val where+ parseJSON val =+ (Int <$> parseJSON val) <|>+ (Str <$> parseJSONByteString val) <|>+ (Vec <$> parseJSONVectorByteString val) <|>+ (parseRef val) <|>+ (parseNul val)++instance Aeson.FromJSON Result where+ parseJSON = \case+ Object obj -> do+ mArgs <- obj .:? "args"+ args <- maybe (pure KM.empty) parseJSON mArgs+ hashes <- KM.traverse parseJSONHash (KM.delete "args" obj)+ pure (Result args hashes)+ _ -> empty++instance Aeson.FromJSON TestVector where+ parseJSON = withObject "TestVector" $ \v -> TestVector+ <$> v .: "name"+ <*> v .: "args"+ <*> parseResults v++parseRef :: Value -> Parser Val+parseRef = \case+ Object obj -> do+ ref <- obj .: "ref"+ len <- obj .: "len"+ mAlg <- obj .:? "algorithm"+ mIdx <- obj .:? "index"+ let alg = fromMaybe "phkdf-pass" mAlg+ idx = fromMaybe 0 mIdx+ testId = TestId ref idx alg+ return $ Ref testId len+ _ -> empty++parseNul :: Value -> Parser Val+parseNul = \case+ Null -> return Nul+ _ -> empty++parseJSONByteString :: Value -> Parser ByteString+parseJSONByteString = \case+ String txt -> pure (T.encodeUtf8 txt)+ Object obj | KM.size obj == 1 -> do+ txt <- obj .: "hex"+ case B.decodeBase16 (T.encodeUtf8 txt) of+ Left _ -> empty+ Right x -> pure x+ _ -> empty++parseJSONVectorByteString :: Value -> Parser (Vector ByteString)+parseJSONVectorByteString val =+ (V.singleton <$> parseJSONByteString val) <|>+ case val of+ Array bs -> V.generateM (V.length bs) (\i -> parseJSONByteString (bs ! i))+ _ -> empty++parseJSONHash :: Value -> Parser ByteString+parseJSONHash = \case+ String txt ->+ case B.decodeBase16 (T.encodeUtf8 txt) of+ Left _ -> empty+ Right x -> pure x+ _ -> empty++parseResults :: Object -> Parser (Vector Result)+parseResults v =+ case KM.lookup "results" v of+ Nothing -> pure V.empty+ Just v@(Object _) ->+ V.singleton <$> parseJSON v+ Just (Array v) ->+ V.generateM (V.length v) (\i -> parseJSON (v ! i))+ _ -> empty++readTestVectorsFromFile :: String -> IO (String, Either String TestVectors)+readTestVectorsFromFile fileName =+ try (Aeson.eitherDecodeFileStrict' fileName) >>= \case+ Left (err :: IOError) -> return (fileName, Left (show err))+ Right result -> return (fileName, result)++testVectorDefaultFileName :: String+testVectorDefaultFileName = "phkdf-test-vectors.json"++testFile :: (String, Either String TestVectors) -> TestTree+testFile (fileName, mTestVectors) =+ case mTestVectors of+ Left err -> testCase testName $ assertFailure err+ Right tvs -> testGroup testName $ genTestCases tvs+ where+ testName = "testfile: " ++ fileName++runTest :: SimpleTestVector -> ResultEnv -> Assertion+runTest tv resultEnv =+ case Map.lookup (simpleTestVector_id tv) resultEnv of+ Nothing -> assertFailure "test result not found (this shouldn't be possible)"+ Just (Left err) -> assertFailure err+ Just (Right result) -> compareAu alg goldenOutput result+ where+ alg = T.unpack . testId_algorithm $ simpleTestVector_id tv+ goldenOutput = simpleTestVector_result tv++compareAu :: String -> ByteString -> Stream ByteString -> Assertion+compareAu name bs outStream+ | B.null bs = assertFailure ("\"" ++ name ++ "\":\"" ++ concatMap toHex (S.take 2 outStream) ++ "\"")+ | otherwise = B.encodeBase16 (takeBytes (B.length bs) outStream) @?= B.encodeBase16 bs+ where+ toHex = T.unpack . B.encodeBase16++takeBytes :: Int -> Stream ByteString -> ByteString+takeBytes n stream = B.concat (go n stream)+ where+ go n ~(Cons out outStream')+ | n <= 0 = []+ | n <= B.length out = [B.take n out]+ | otherwise = out : go (n - B.length out) outStream'++-- FIXME? Allow computation of tweaks without recomputing seed++-- I initially liked this ViewPattern approach to high-level parsing, but now+-- I don't, because of error messages and ResultEnv handling++-- TODO: Rewrite getPhkdf*Inputs and their helpers++getPhkdfPassInputs :: ResultEnv -> KeyMap Val -> Maybe (PhkdfInputBlock, PhkdfInputArgs, PhkdfInputTweak)+getPhkdfPassInputs env = \case+ (getPhkdfPassBlock env -> Just (block,+ getPhkdfPassArgs env -> Just (args,+ getPhkdfPassTweak env -> Just (tweak,+ args')))) | KM.null args'+ -> Just (block, args, tweak)+ _ -> Nothing++getPhkdfSimpleInputs :: ResultEnv -> KeyMap Val -> Maybe (PhkdfInputBlock, PhkdfInputArgs)+getPhkdfSimpleInputs env = \case+ (getPhkdfSimpleBlock env -> Just (block,+ getPhkdfSimpleArgs env -> Just (args,+ args'))) | KM.null args'+ -> Just (block, args)+ _ -> Nothing++getPhkdfPassArgs :: ResultEnv -> KeyMap Val -> Maybe (PhkdfInputArgs, KeyMap Val)+getPhkdfPassArgs env = \case+ (matchKey env "username" -> (Just (Str phkdfInputArgs_username),+ matchKey env "password" -> (Just (Str phkdfInputArgs_password),+ matchKey env "credentials" -> (+ getByteStringVector_defaultEmpty -> Just phkdfInputArgs_credentials,+ args'))))+ -> Just (PhkdfInputArgs {..}, args')+ _ -> Nothing++getPhkdfSimpleArgs :: ResultEnv -> KeyMap Val -> Maybe (PhkdfInputArgs, KeyMap Val)+getPhkdfSimpleArgs env = \case+ (getPhkdfPassArgs env -> Just (inputArgs,+ matchKey env "role" -> (getByteStringVector_defaultEmpty -> Just role,+ args')))+ -> let creds = phkdfInputArgs_credentials inputArgs+ inputArgs' = inputArgs {+ phkdfInputArgs_credentials = creds <> role+ }+ in Just (inputArgs' , args')+ _ -> Nothing++getByteStringVector_defaultEmpty :: Maybe Val -> Maybe (Vector ByteString)+getByteStringVector_defaultEmpty = \case+ Nothing -> Just V.empty+ Just Nul -> Just V.empty+ Just (Str str) -> Just (V.singleton str)+ Just (Vec vec) -> Just vec+ _ -> Nothing++getByteString_defaultEmpty :: Maybe Val -> Maybe ByteString+getByteString_defaultEmpty+ = fmap (fromMaybe B.empty) . getMaybeByteString++getByteString :: Maybe Val -> Maybe ByteString+getByteString = \case+ Just (Str str) -> Just str+ _ -> Nothing++getMaybeByteString :: Maybe Val -> Maybe (Maybe ByteString)+getMaybeByteString = \case+ Just (Str str) -> Just (Just str)+ Just Nul -> Just Nothing+ Nothing -> Just Nothing+ _ -> Nothing++getPhkdfPassBlock :: ResultEnv -> KeyMap Val -> Maybe (PhkdfInputBlock, KeyMap Val)+getPhkdfPassBlock env = \case+ (matchKey' env "domain-tag" -> (Just (Str phkdfInputBlock_domainTag),+ matchKey env "seguid" -> (getByteString_defaultEmpty -> Just phkdfInputBlock_seguid,+ matchKey env "long-tag" -> (getMaybeByteString -> Just mLongTag,+ -- use matchKey' to leave the "tags" argument behind for getPhkdfPassTweak+ matchKey' env "tags" -> (getByteStringVector_defaultEmpty -> Just tags,+ matchKey env "seed-tags" -> (getByteStringVector_defaultEmpty -> Just seedTags,+ matchKey env "rounds" -> (Just (Int (fromIntegral -> phkdfInputBlock_rounds)),+ args')))))))+ -> let phkdfInputBlock_tags = tags <> seedTags+ phkdfInputBlock_longTag = fromMaybe phkdfInputBlock_domainTag mLongTag+ in Just (PhkdfInputBlock {..}, args')+ _ -> Nothing++getPhkdfSimpleBlock :: ResultEnv -> KeyMap Val -> Maybe (PhkdfInputBlock, KeyMap Val)+getPhkdfSimpleBlock env = \case+ (getPhkdfPassBlock env -> Just (block,+ matchKey env "echo-tag" -> (getMaybeByteString -> Just echoTag,+ args')))+ -> let args'' = KM.delete "tags" (KM.delete "domain-tag" args')+ in case echoTag of+ Nothing -> Just (block, args'')+ Just echoTag ->+ let block' = block {+ phkdfInputBlock_tags = V.snoc (phkdfInputBlock_tags block) echoTag+ }+ in Just (block',args'')+ _ -> Nothing++getPhkdfPassTweak :: ResultEnv -> KeyMap Val -> Maybe (PhkdfInputTweak, KeyMap Val)+getPhkdfPassTweak env = \case+ (matchKey env "role" -> (getByteStringVector_defaultEmpty -> Just phkdfInputTweak_role,+ matchKey env "tags" -> (getByteStringVector_defaultEmpty -> Just tags,+ matchKey env "echo-tag" -> (getMaybeByteString -> Just mEchoTag,+ matchKey env "domain-tag" -> (getByteString -> Just domainTag,+ args')))))+ -> case mEchoTag of+ Nothing ->+ let phkdfInputTweak_echoTag = domainTag+ in Just (PhkdfInputTweak{..}, args')+ Just phkdfInputTweak_echoTag ->+ Just (PhkdfInputTweak{..}, args')+ _ -> Nothing++matchKey, matchKey' :: ResultEnv -> Key -> KeyMap Val -> (Maybe Val, KeyMap Val)+matchKey env key map = (interpRefs env (KM.lookup key map), KM.delete key map)+matchKey' env key map = (interpRefs env (KM.lookup key map), map)++interpRefs :: ResultEnv -> Maybe Val -> Maybe Val+interpRefs env (Just ref@(Ref testId bytes)) =+ case Map.lookup testId env of+ Nothing -> Just ref+ Just (Left _) -> Just ref+ Just (Right echo) -> Just (Str (takeBytes bytes echo))+interpRefs _ val = val