packages feed

hstratus-auth (empty) → 0.1.0.0

raw patch · 48 files changed

+11428/−0 lines, 48 filesdep +QuickCheckdep +aesondep +aeson-casingsetup-changed

Dependencies added: QuickCheck, aeson, aeson-casing, base, base16-bytestring, base64-bytestring, benri-hspec, bytestring, case-insensitive, containers, crypto-srp, cryptohash-sha1, cryptohash-sha256, cryptohash-sha512, directory, filepath, hspec, hstratus-auth, http-client, http-client-tls, http-types, main-tester, optparse-applicative, random, silently, simple-prompt, string-conv, temporary, text, time, transformers, unix, uuid, wai, warp, web-cookiejar, xdg-basedir

Files

+ ChangeLog.md view
@@ -0,0 +1,9 @@+# Revision history for icloud-auth++`icloud-auth` uses [PVP Versioning][1].++## 0.1.0.0 -- 2026-07-20++* Initial version.++[1]: https://pvp.haskell.org
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2023, Tim Emiola++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Tim Emiola nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,92 @@+# hstratus-auth — unofficial authentication for iCloud services++`hstratus-auth` authenticates with iCloud using Apple ID credentials stored on+disk.  The full sign-in flow — SRP credential exchange followed by any required+two-factor (2FA) or legacy two-step (2SA) challenge — runs automatically,+prompting the terminal for verification codes when needed.  On success it caches+a session token for use with other iCloud services.+++## Disclaimer — use at your own risk++- This library is **unofficial** and not supported by Apple.+- The iCloud authentication protocol it uses is undocumented and may change+  without notice.++## Usage++The same two steps — saving credentials and authenticating — are available+programmatically.++### Saving credentials++Write a `credentials.json` file directly, or call `saveCredentials`:++```haskell+import Network.HStratus.Session (Credentials (..), saveCredentials)++saveCreds :: IO ()+saveCreds =+  saveCredentials $ Credentials+    { accountName = "your-apple-id@example.com"+    , password    = "your-password"+    }+```++### Authenticating++Create an `Api` handle with `mkApi`, then call `login`:++```haskell+import Network.HStratus.Http (mkApi, login, AuthState (..))+import Network.HStratus.Http.Endpoints (Realm (..))++example :: IO ()+example = do+  api <- mkApi Usual  -- or China for mainland China accounts+  result <- login api+  case result of+    Authenticated _session _accountData -> putStrLn "Authenticated!"+    _                                   -> putStrLn "Unexpected result"+```++### Injectable callbacks++Pass your own callbacks to `loginWith` to replace the interactive prompts —+useful in automation or tests.  The snippet below shows the code-reader; the+phone-selector and device-selector arguments follow the same pattern.++```haskell+import Network.HStratus.Http (loginWith)+import qualified Data.Text.IO as Text++exampleWith :: Api -> IO AuthState+exampleWith api = loginWith readCode (\_ -> pure Nothing) chooseDevice api+ where+  readCode codeLen = do+    Text.putStrLn $ "Enter the " <> Text.pack (show codeLen) <> "-digit code:"+    Text.getLine+  chooseDevice (d :| _) = pure d+```++If you already hold a `Requires2FA` or `Requires2SA` value from a prior call,+resume with `completeTwoFactor` / `completeTwoFactorWith` or `complete2SA` /+`complete2SAWith`.++## Reauthentication++The session token expires after a period set by iCloud (approximately two months+at the time of writing).  When it does, authenticate again to refresh it.+++## CLI usage++A command-line interface using this behaviour is provided by the [`hstratus`](https://github.com/adetokunbo/hstratus/tree/main/hstratus/#readme)+package.  Use [`hstratus auth init`](https://github.com/adetokunbo/hstratus/tree/main/hstratus/#hstratus-auth-init) and [`hstratus auth login`](https://github.com/adetokunbo/hstratus/tree/main/hstratus/#hstratus-auth-login) to save credentials and authenticate.++++---++Apple and the Apple logo are trademarks of Apple Inc., registered in the U.S. and other countries and regions.+iCloud is a service mark of Apple Inc., registered in the U.S. and other countries and regions.
+ Setup.hs view
@@ -0,0 +1,4 @@+import Distribution.Simple+++main = defaultMain
+ hstratus-auth.cabal view
@@ -0,0 +1,175 @@+cabal-version:      3.0+name:               hstratus-auth+version:            0.1.0.0+synopsis:           Authenticate with iCloud+description:+  Authenticate with iCloud using Apple ID credentials stored on disk.++  The full sign-in flow — SRP credential exchange followed by any required+  two-factor (2FA) or legacy two-step (2SA) challenge — is handled by a+  single login call. On success the library caches a session token for use+  with other iCloud services.++  This library is unofficial and not supported by Apple. It may break+  without warning if Apple changes their authentication protocol.+license:            BSD-3-Clause+license-file:       LICENSE+maintainer:         Tim Emiola <adetokunbo@emio.la>+category:           Network+build-type:         Simple+tested-with:+    GHC == 9.2.8+  , GHC == 9.4.8+  , GHC == 9.6.7+  , GHC == 9.8.4+  , GHC == 9.10.2+  , GHC == 9.12.1+extra-doc-files:+  ChangeLog.md+  README.md+data-files:         testdata/*.json++source-repository head+  type:     git+  location: https://github.com/adetokunbo/hstratus.git+  subdir:   hstratus-auth++library hstratus-auth-cli+  visibility:       public+  exposed-modules:  Network.HStratus.Http.Cli+  hs-source-dirs:   src-cli+  build-depends:+    , base                 >=4.12 && <5+    , directory            >=1.3 && <1.4+    , filepath             >=1.4 && <1.6+    , http-client-tls      >=0.3 && <0.4+    , hstratus-auth+    , optparse-applicative >=0.18 && <0.19+    , xdg-basedir          >=0.2 && <0.3+  default-language: Haskell2010+  ghc-options:      -Wall -Wincomplete-uni-patterns -Wpartial-fields -fwarn-tabs++library+  exposed-modules:  Network.HStratus.Http+                    Network.HStratus.Http.Endpoints+                    Network.HStratus.Http.Common+                    Network.HStratus.Session+                    Network.HStratus.Trust+  other-modules:    Network.HStratus.Internal.Http.Api+                    Network.HStratus.Internal.Http.Login+                    Network.HStratus.Internal.Http.Signin+  hs-source-dirs:   src+  build-depends:+    , aeson                >=2.0 && <2.3+    , aeson-casing         >=0.1 && <0.3+    , base                 >=4.12 && <5+    , base16-bytestring    >=1.0 && <1.1+    , base64-bytestring    >=1.0 && <2.1+    , bytestring           >=0.10.8 && <0.11 || >=0.11.3 && <0.13+    , case-insensitive     >=1.2 && <1.3+    , containers           >=0.6 && <0.8+    , crypto-srp           >=0.1 && <0.2+    , cryptohash-sha256    >=0.11 && <0.12+    , directory            >=1.3 && <1.4+    , filepath             >=1.4 && <1.6+    , http-client          >=0.5 && <0.8+    , http-client-tls      >=0.3 && <0.4+    , http-types           >=0.12.1 && <0.13+    , random               >=1.1 && <1.4+    , simple-prompt        >=0.2 && <0.3+    , string-conv          >=0.1 && <0.3+    , text                 >=1.2.3 && <2.2+    , time                 >=1.8 && <1.15+    , transformers         >=0.5 && <0.7+    , unix                 >=2.7 && <2.9+    , uuid                 >=1.3 && <1.4+    , web-cookiejar        >=0.1.3 && <0.2+    , xdg-basedir          >=0.2 && <0.3+    , hstratus-auth:hstratus-auth-internal+  default-language: Haskell2010+  ghc-options:      -Wall -Wincomplete-uni-patterns -Wpartial-fields -fwarn-tabs+++library hstratus-auth-internal+  exposed-modules:  Network.HStratus.Internal.Endpoints+                    Network.HStratus.Internal.Http+                    Network.HStratus.Internal.HttpErrors+                    Network.HStratus.Internal.LoginFSM+                    Network.HStratus.Internal.PBKDF2+                    Network.HStratus.Internal.Session+                    Network.HStratus.Internal.Trust+  hs-source-dirs:   src-internal+  build-depends:+    , aeson                >=2.0   && <2.3+    , aeson-casing         >=0.1 && <0.3+    , base                 >=4.12 && <5+    , base16-bytestring    >=1.0 && <1.1+    , bytestring           >=0.10.8 && <0.11 || >=0.11.3 && <0.13+    , case-insensitive     >=1.2 && <1.3+    , containers           >=0.6 && <0.8+    , crypto-srp           >=0.1 && <0.2+    , directory            >=1.3 && <1.4+    , filepath             >=1.4 && <1.6+    , http-client          >=0.5 && <0.8+    , http-types           >=0.12.1 && <0.13+    , simple-prompt        >=0.2 && <0.3+    , string-conv          >=0.1 && <0.3+    , text                 >=1.2.3 && <2.2+    , unix                 >=2.7 && <2.9+    , uuid                 >=1.3 && <1.4+    , xdg-basedir          >=0.2 && <0.3+  default-language: Haskell2010+  ghc-options:      -Wall -Wincomplete-uni-patterns -Wpartial-fields -fwarn-tabs+++test-suite test+  type:             exitcode-stdio-1.0+  main-is:          Spec.hs+  hs-source-dirs:   test+  autogen-modules:  Paths_hstratus_auth+  other-modules:    HStratus.ApiLoggerSpec+                    HStratus.Examples+                    HStratus.Http.CliSpec+                    HStratus.HttpMockSpec+                    HStratus.HttpSpec+                    HStratus.Http.EndpointsSpec+                    HStratus.Http.ErrorsSpec+                    HStratus.Http.HeadersSpec+                    HStratus.LoginFSMSpec+                    HStratus.Mock+                    HStratus.PBKDF2Spec+                    HStratus.SessionSpec+                    HStratus.TrustSpec+                    Paths_hstratus_auth+  default-language: Haskell2010+  ghc-options:      -threaded -rtsopts -with-rtsopts=-N -Wall -fwarn-tabs+  build-depends:+    , aeson+    , base+    , base16-bytestring+    , bytestring+    , containers+    , cryptohash-sha1      >=0.11 && <0.12+    , cryptohash-sha256    >=0.11 && <0.12+    , cryptohash-sha512    >=0.11 && <0.12+    , crypto-srp           >=0.1 && <0.2+    , directory            >=1.3.6  && <1.5+    , filepath             >=1.4 && <1.6+    , benri-hspec          >=0.1 && <0.3+    , hspec                >=2.1 && < 3.0+    , http-client          >=0.5 && <0.8+    , http-types+    , QuickCheck           >= 2.13 && < 2.16+    , temporary            >= 1.2 && < 1.4+    , hstratus-auth+    , hstratus-auth:hstratus-auth-cli+    , optparse-applicative >=0.18 && <0.19+    , silently             >= 1.2 && < 1.3+    , string-conv+    , text+    , wai                  >=3.2 && <3.3+    , warp                 >=3.2 && <3.5+    , main-tester          >= 0.2 && < 0.3+    , unix                 >=2.7 && <2.9+    , hstratus-auth:hstratus-auth-internal+
+ src-cli/Network/HStratus/Http/Cli.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Network.HStratus.Http.Cli+  ( -- * Common CLI options+    CommonOpts (..)+  , commonOptsParser++    -- * Log target resolution+  , resolveLogTarget+  , defaultLogFile++    -- * Logger selection+  , mkLoggerFor++    -- * Authenticated API runner+  , runWithApi++    -- * Error handler+  , onServiceError+  )+where++import Control.Exception (catch, displayException)+import Network.HStratus.Http+  ( Api+  , ApiLogger+  , AuthError+  , AuthState (..)+  , HStratusError+  , fileLogger+  , login+  , mkApiWith+  , redactingLogger+  , verboseLogger+  , withLogger+  )+import Network.HStratus.Http.Endpoints (Realm (..), realmEndpoints)+import Network.HStratus.Session (AccountData, Session, loadSession)+import Network.HTTP.Client.TLS (newTlsManager)+import Options.Applicative+import System.Directory (createDirectoryIfMissing)+import System.Environment.XDG.BaseDir (getUserCacheDir)+import System.Exit (exitFailure)+import System.FilePath ((</>))+import System.IO (Handle, IOMode (..), stdout, withFile)+++-- | Options shared by all icloud CLI commands.+data CommonOpts = CommonOpts+  { optChina :: Bool+  -- ^ Use mainland China endpoints instead of the worldwide endpoints.+  , optLog :: Bool+  -- ^ Append HTTP exchanges to the default log file.+  , optLogFile :: Maybe FilePath+  -- ^ Append HTTP exchanges to this file instead of the default.+  , optLogBodies :: Bool+  -- ^ Include request bodies in the HTTP exchange log.+  , optRedact :: Bool+  -- ^ Redact sensitive headers (tokens, cookies) in the log.+  }+  deriving (Eq, Show)+++-- | Parser for 'CommonOpts'.+commonOptsParser :: Parser CommonOpts+commonOptsParser =+  CommonOpts+    <$> switch (long "china" <> help "Use mainland China endpoints")+    <*> switch (long "log" <> help "Append HTTP exchanges to the default log file")+    <*> optional+      (strOption (long "log-file" <> metavar "FILE" <> help "Append HTTP exchanges to FILE"))+    <*> switch (long "log-bodies" <> help "Include request bodies in the HTTP exchange log")+    <*> switch (long "redact" <> help "Redact sensitive headers (tokens, cookies) in the log")+++-- | Resolve the log file path from 'CommonOpts', or 'Nothing' if logging is disabled.+resolveLogTarget :: CommonOpts -> IO (Maybe FilePath)+resolveLogTarget CommonOpts{optLogFile = Just fp} = pure (Just fp)+resolveLogTarget CommonOpts{optLog = True} = Just <$> defaultLogFile+resolveLogTarget _ = pure Nothing+++-- | Default log file path: @~\/.cache\/hs-icloud\/requests.log@.+defaultLogFile :: IO FilePath+defaultLogFile = do+  dir <- getUserCacheDir "hs-icloud"+  createDirectoryIfMissing True dir+  pure (dir </> "requests.log")+++-- | Select the appropriate logger constructor from 'CommonOpts'.+mkLoggerFor :: CommonOpts -> Handle -> ApiLogger+mkLoggerFor CommonOpts{optRedact = True} = redactingLogger+mkLoggerFor CommonOpts{optLogBodies = True} = verboseLogger+mkLoggerFor _ = fileLogger+++{- | Authenticate and run an action with the resulting 'Api'.++Handles session loading, TLS manager creation, logger wiring, and catches+'AuthError'. Additional error types should be caught by the caller.+-}+runWithApi+  :: CommonOpts+  -> (AccountData -> Session -> Api -> IO ())+  -> IO ()+runWithApi opts runAction = do+  session <- loadSession+  mgr <- newTlsManager+  let realm = if optChina opts then China else Usual+  api0 <- mkApiWith session (realmEndpoints realm) mgr+  mbLogPath <- resolveLogTarget opts+  let mkLogger' = mkLoggerFor opts+      run api = do+        result <- login api+        case result of+          Authenticated sess ad -> runAction ad sess api+          _ -> putStrLn "Not authenticated — run 'hstratus-auth login' first." >> exitFailure+      go = case mbLogPath of+        Just fp -> withFile fp AppendMode $ \h -> run (withLogger (mkLogger' h) api0)+        Nothing+          | optLogBodies opts && not (optRedact opts) -> run (withLogger (mkLogger' stdout) api0)+          | otherwise -> run api0+  go `catch` onServiceError @AuthError+++-- | Print a service error and exit. Use as the catch handler in CLI wrappers.+onServiceError :: (HStratusError e) => e -> IO a+onServiceError e = putStrLn ("Error: " <> displayException e) >> exitFailure
+ src-internal/Network/HStratus/Internal/Endpoints.hs view
@@ -0,0 +1,296 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : Network.HStratus.Internal.Endpoints+Copyright   : (c) 2025 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3+-}+module Network.HStratus.Internal.Endpoints+  ( -- * Types+    Endpoints (..)+  , Realm (..)++    -- * Region selection+  , realmEndpoints++    -- * Request builders+  , signinInitBase+  , signinCompleteBase+  , validateBase+  , accountLoginBase+  , twoSvTrust+  , twoFaOptionsBase+  , verifySecurityCodeReq+  , validateVerification+  , sendVerification+  , listDevices++    -- * Request modifiers+  , extendPath+  , toPut+  , withHeaders+  , withBody+  , withAcceptJson+  , withWidgetKey+  , withAppleOauthHeaders++    -- * Header helpers+  , homeHeaders+  , icloudBrowserHeaders++    -- * Shared service utilities+  , icloudHome+  , stripTrailingSlash+  , lookupWebservice+  )+where++import Control.Exception (throwIO)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BS8+import qualified Data.ByteString.Lazy as LBS+import Data.CaseInsensitive (mk)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.String.Conv (toS)+import Data.Text (Text)+import Network.HStratus.Internal.HttpErrors (AuthError (..))+import Network.HStratus.Internal.Session (Webservice (..))+import Network.HTTP.Client+  ( Request (..)+  , RequestBody (..)+  , defaultRequest+  , parseRequest+  )+import Network.HTTP.Types+  ( Header+  , HeaderName+  , RequestHeaders+  , hAccept+  , hReferer+  , hUserAgent+  , methodGet+  , methodPost+  , methodPut+  )+++-- | @RequestHeaders@ that include the @Endpoint@ @home@+homeHeaders :: Endpoints -> RequestHeaders+homeHeaders ep = [(hOrigin, epHome ep), (hReferer, epHome ep <> "/")]+++commonHeaders :: Endpoints -> RequestHeaders+commonHeaders ep = userAgent : homeHeaders ep+++-- | Construct a new @Request@ with that path changed by adding a suffix+extendPath :: Request -> ByteString -> Request+extendPath req suffix = req{path = path req <> suffix}+++-- | Construct a new @Request@ with the method changed to @GET@+toGet :: Request -> Request+toGet req = req{method = methodGet}+++-- | Construct a new @Request@ with the method changed to @PUT@+toPut :: Request -> Request+toPut req = req{method = methodPut}+++data Endpoints = Endpoints+  { epHome :: !ByteString+  -- ^ home origin, e.g. @https://www.icloud.com@; used in @Origin@ and @Referer@ headers+  , epAuth :: !Request+  -- ^ base request for the authentication endpoint (@idmsa.apple.com@)+  , epSetup :: !Request+  -- ^ base request for the setup\/account endpoint (@setup.icloud.com@)+  , epWidgetKey :: !ByteString+  -- ^ value sent as @X-Apple-Widget-Key@ and @X-Apple-OAuth-Client-Id@; override if Apple rotates it+  }+++data Realm = China | Usual+  deriving (Eq, Show)+++realmEndpoints :: Realm -> Endpoints+realmEndpoints China = chinaEndpoints+realmEndpoints Usual = usualEndpoints+++-- | The iCloud home origin used in @Origin@ and @Referer@ headers.+icloudHome :: ByteString+icloudHome = "https://www.icloud.com"+++usualEndpoints :: Endpoints+usualEndpoints =+  Endpoints+    { epHome = icloudHome+    , epAuth = authReq+    , epSetup = setupReq+    , epWidgetKey = iCloudKey+    }+++chinaEndpoints :: Endpoints+chinaEndpoints =+  Endpoints+    { epHome = "https://www.icloud.com.cn"+    , epAuth = authReq -- idmsa.apple.com is Apple's global SRP endpoint; not region-specific+    , epSetup = setupReq{host = "setup.icloud.com.cn"}+    , epWidgetKey = iCloudKey+    }+++apiRequest :: Request+apiRequest =+  defaultRequest+    { secure = True+    , port = 443+    , method = methodPost+    }+++authReq :: Request+authReq = apiRequest{host = "idmsa.apple.com", path = "/appleauth/auth"}+++setupReq :: Request+setupReq = apiRequest{host = "setup.icloud.com", path = "/setup/ws/1"}+++appleOauthHeaders :: ByteString -> [Header]+appleOauthHeaders key =+  [ ("X-Apple-OAuth-Client-Id", key)+  , ("X-Apple-OAuth-Client-Type", "firstPartyAuth")+  , ("X-Apple-OAuth-Redirect-URI", "https://www.icloud.com")+  , ("X-Apple-OAuth-Require-Grant-Code", "true")+  , ("X-Apple-OAuth-Response-Mode", "web_message")+  , ("X-Apple-OAuth-Response-Type", "code")+  , ("X-Apple-Widget-Key", key)+  ]+++iCloudKey :: ByteString+iCloudKey = "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d"+++browserAgent :: ByteString+browserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36"+++userAgent :: Header+userAgent = (hUserAgent, browserAgent)+++acceptJson :: Header+acceptJson = (hAccept, "application/json")+++-- | build the basic @Request@ to make the initiate signin+signinInitBase :: Endpoints -> Request+signinInitBase = (`extendPath` "/signin/init") . epAuth+++-- | build the basic @Request@ to complete signin+signinCompleteBase :: Endpoints -> Request+signinCompleteBase =+  let+    withQuery x = x{queryString = "isRememberMeEnabled=true"}+   in+    withQuery . (`extendPath` "/signin/complete") . epAuth+++-- | build the basic @Request@ to that validates user credentials+validateBase :: Endpoints -> Request+validateBase ep = withHeaders (commonHeaders ep) $ (`extendPath` "/validate") $ epSetup ep+++-- | build the basic @Request@ to that performs login+accountLoginBase :: Endpoints -> Request+accountLoginBase = (`extendPath` "/accountLogin") . epSetup+++-- | build the basic @Request@ to that makes the saved credentials trusted+twoSvTrust :: Endpoints -> Request+twoSvTrust = (`extendPath` "/2sv/trust") . toGet . epAuth+++-- | build the @Request@ to fetch the 2FA options after the 409 from signin/complete+twoFaOptionsBase :: Endpoints -> Request+twoFaOptionsBase = toGet . epAuth+++-- | build the @Request@ to that verifies makes a security code+verifySecurityCodeReq :: Text -> Endpoints -> Request+verifySecurityCodeReq codeType =+  (`extendPath` ("/verify/" <> toS codeType <> "/securitycode")) . epAuth+++validateVerification :: Endpoints -> Request+validateVerification = (`extendPath` "/validateVerificationCode") . epSetup+++sendVerification :: Endpoints -> Request+sendVerification = (`extendPath` "/sendVerificationCode") . epSetup+++listDevices :: Endpoints -> Request+listDevices = (`extendPath` "/listDevices") . toGet . epSetup+++withHeaders :: RequestHeaders -> Request -> Request+withHeaders newHeaders req = req{requestHeaders = newHeaders <> requestHeaders req}+++withBody :: LBS.LazyByteString -> Request -> Request+withBody b req = req{requestBody = RequestBodyLBS b}+++hOrigin :: HeaderName+hOrigin = mk "Origin"+++withAcceptJson :: RequestHeaders -> RequestHeaders+withAcceptJson = (acceptJson :)+++withWidgetKey :: ByteString -> RequestHeaders -> RequestHeaders+withWidgetKey key = (("X-Apple-Widget-Key", key) :)+++withAppleOauthHeaders :: ByteString -> RequestHeaders -> RequestHeaders+withAppleOauthHeaders key = (appleOauthHeaders key <>)+++-- | Standard browser-style headers sent with every iCloud service request.+icloudBrowserHeaders :: RequestHeaders+icloudBrowserHeaders =+  [ acceptJson+  , userAgent+  , (hOrigin, icloudHome)+  , (hReferer, icloudHome <> "/")+  ]+++-- | Strip a trailing @/@ from a strict 'ByteString' path.+stripTrailingSlash :: ByteString -> ByteString+stripTrailingSlash bs+  | not (BS8.null bs) && BS8.last bs == '/' = BS8.init bs+  | otherwise = bs+++{- | Look up a service URL by key in the webservices map and parse it into a+'Request'.  Fails with an informative message if the key is absent.+-}+lookupWebservice :: Text -> Map Text Webservice -> IO Request+lookupWebservice key ws =+  case Map.lookup key ws of+    Nothing -> throwIO $ WebserviceNotFound key+    Just (Webservice _ (Just "inactive")) -> throwIO $ WebserviceNotFound key+    Just (Webservice url _) -> parseRequest (toS url)
+ src-internal/Network/HStratus/Internal/Http.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_HADDOCK prune #-}++{- |+Module      : Network.HStratus.Internal.Http+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Internal HTTP request body builders and SRP authentication context types.+-}+module Network.HStratus.Internal.Http+  ( validateSetupBody+  , phoneCodeBody+  , phoneTriggerBody+  , needsRetry+  , PasswordProtocol (..)+  , KeyDeriver (..)+  , SrpContext (..)+  , hCounter+  , hCountry+  , hSessionId+  , hSessionToken+  , hTrustToken+  )+where++import Crypto.SRP+  ( FromClient (..)+  , FromServer (..)+  , XCalculator (..)+  , hashMany+  , hashText+  )+import Data.Aeson (FromJSON (..), Value (..), withText)+import Data.Aeson.KeyMap (fromList)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base16 as Base16+import Data.CaseInsensitive (mk)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Data.Word (Word64)+import Network.HStratus.Internal.PBKDF2 (FancyPseudoRandomF, deriveKey)+import Network.HStratus.Internal.Trust (Setup2SADevice (..), TrustedPhone (..))+import Network.HTTP.Types.Header (HeaderName)+++-- | Models the known values of password protocol+data PasswordProtocol+  = -- | legacy @s2k_fo@ protocol: password is hex-encoded before hashing+    Old+  | -- | current @s2k@ protocol: password is hashed directly+    New+  deriving (Eq, Show)+++instance FromJSON PasswordProtocol where+  parseJSON =+    let fromText "s2k" = Right New+        fromText "s2k_fo" = Right Old+        fromText alt = Left $ "unknown PasswordProtocol: " ++ show alt+     in withText "PasswordProtocol" $ either fail pure . fromText+++-- | Data used during key derivation and verification+data KeyDeriver = KeyDeriver+  { kdTag :: !Text+  -- ^ SRP session tag from Apple's server response+  , kdIterations :: !Word64+  -- ^ PBKDF2 iteration count from Apple's server response+  , kdProtocol :: !PasswordProtocol+  -- ^ password hashing protocol in use for this session+  , kdWrappedF :: !FancyPseudoRandomF+  -- ^ PBKDF2 pseudo-random function, pre-wrapped with the negotiated hash algorithm+  }+++instance XCalculator KeyDeriver where+  calcX = calcXUsingKeyDeriver+++calcXUsingKeyDeriver :: KeyDeriver -> FromClient -> FromServer -> BS.ByteString+calcXUsingKeyDeriver kd fc fs =+  let FromServer{fsSalt, fsKnownAlgorithm = hashAlgo} = fs+      h = hashMany hashAlgo+      KeyDeriver{kdIterations = count, kdWrappedF, kdProtocol} = kd+      useProtocol Old = Base16.encode+      useProtocol New = id+      hashed = useProtocol kdProtocol $ hashText hashAlgo $ fcPassword fc+      reallyHashed = deriveKey kdWrappedF hashed fsSalt count+   in h [fsSalt, h [":", reallyHashed]]+++-- | Bundles the SRP client\/server data and key deriver for a single auth attempt+data SrpContext = SrpContext+  { srpFromClient :: !FromClient+  -- ^ client-side SRP values (public key, password verifier input)+  , srpFromServer :: !FromServer+  -- ^ server-side SRP values (salt, public key, hash algorithm)+  , srpKeyDeriver :: !KeyDeriver+  -- ^ key derivation parameters negotiated during SRP init+  }+++-- | @HeaderName@s used to capture session info from HTTP responses+hCountry, hSessionId, hSessionToken, hTrustToken, hCounter :: HeaderName+hCountry = mk "X-Apple-ID-Account-Country"+hSessionId = mk "X-Apple-ID-Session-Id"+hSessionToken = mk "X-Apple-Session-Token"+hTrustToken = mk "X-Apple-TwoSV-Trust-Token"+hCounter = mk "scnt"+++-- | Build the JSON body to submit a legacy 2SA verification code for a given device.+validateSetupBody :: Setup2SADevice -> Text -> Value+validateSetupBody (Setup2SADevice fields) code =+  Object $ fields <> fromList [("verificationCode", String code), ("trustBrowser", Bool True)]+++-- | Build the JSON body to trigger an SMS code to the given phone+phoneTriggerBody :: TrustedPhone -> Value+phoneTriggerBody tp =+  Object $+    fromList+      [ ("phoneNumber", Object $ fromList [("id", Number $ fromIntegral $ tpnId tp)])+      , ("mode", String $ fromMaybe "sms" $ tpnPushMode tp)+      ]+++-- | @True@ when the HTTP status code warrants a single automatic retry (421, 450, or 500).+needsRetry :: Int -> Bool+needsRetry status = status == 421 || status == 450 || status == 500+++-- | Build the JSON body to verify an SMS code received on the given phone+phoneCodeBody :: TrustedPhone -> Text -> Value+phoneCodeBody tp code =+  Object $+    fromList+      [ ("phoneNumber", Object $ fromList [("id", Number $ fromIntegral $ tpnId tp)])+      , ("securityCode", Object $ fromList [("code", String code)])+      , ("mode", String $ fromMaybe "sms" $ tpnPushMode tp)+      ]
+ src-internal/Network/HStratus/Internal/HttpErrors.hs view
@@ -0,0 +1,134 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : Network.HStratus.Internal.HttpErrors+Copyright   : (c) 2022 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Datatypes that model the structured errors returned by the iCloud API.+-}+module Network.HStratus.Internal.HttpErrors+  ( -- * API response wrapper+    ApiResponse (..)++    -- * API error embedded in 'ApiResponse'+  , ApiError (..)++    -- * Public exception type+  , AuthError (..)++    -- * Common service-error marker+  , HStratusError++    -- * Extracting results+  , extractOr+  )+where++import Control.Applicative (Alternative (..), (<|>))+import Control.Exception (Exception, throwIO)+import Data.Aeson+  ( FromJSON (..)+  , Object+  , withObject+  , (.:)+  , (.:?)+  )+import Data.Aeson.KeyMap (member)+import Data.Aeson.Types (Parser)+import Data.Text (Text)+++-- | Represents an API response that may succeed or fail with @ApiError@+data ApiResponse a = Failed !ApiError | Succeeded !a+  deriving (Eq, Show)+++instance (FromJSON a) => FromJSON (ApiResponse a) where+  parseJSON v = (Failed <$> parseJSON v) <|> (Succeeded <$> parseJSON v)+++-- | Represents an API response that reports a failure.+data ApiError+  = ApiError+  { aeReason :: !Text+  , aeCode :: !(Maybe Text)+  }+  deriving (Eq, Show)+++instance FromJSON ApiError where+  parseJSON = withObject "ApiError" parseApiError+++-- | Structured errors thrown by the iCloud authentication layer+data AuthError+  = -- | The supplied credentials were rejected.+    InvalidCredentials+  | -- | The account has been locked due to too many failed sign-in attempts.+    AccountLocked+  | -- | The server requires the user to accept updated privacy terms before continuing.+    PrivacyAgreementRequired+  | -- | Session credentials were absent when the login flow required them.+    CredentialsMissing+  | -- | The artifact directory could not be created; the 'FilePath' names the directory.+    ArtifactDirCreationFailed !FilePath+  | -- | The SRP key exchange failed due to an invalid server public value.+    SrpProtocolError+  | -- | Two-factor authentication is locked after too many incorrect code attempts.+    TwoFactorLocked+  | -- | Two-factor authentication is still required after a verification attempt.+    TwoFactorStillRequired+  | -- | A required iCloud webservice URL was absent from 'AccountData'; the 'Text' is the service key.+    WebserviceNotFound !Text+  | -- | The API returned a structured service error with a reason and an optional error code.+    ServiceError !Text !(Maybe Text)+  | -- | An HTTP response that could not be interpreted; the 'Text' describes the failure.+    UnexpectedResponse !Text+  deriving (Eq, Show)+++instance Exception AuthError+++{- | Marker class for exceptions thrown by iCloud service libraries.++Declare an instance for each library-level error type so that 'onServiceError'+in "Network.HStratus.Http.Cli" can be used as a uniform catch handler.+-}+class (Exception e) => HStratusError e+++instance HStratusError AuthError+++-- | Extract the result from an 'ApiResponse', throwing 'ServiceError' on failure.+extractOr :: ApiResponse a -> IO a+extractOr (Succeeded x) = pure x+extractOr (Failed x) = throwIO $ ServiceError (aeReason x) (aeCode x)+++{-+In python, this looks like:++   if isinstance(data, dict):+       reason = data.get("errorMessage")+       reason = reason or data.get("reason")+       reason = reason or data.get("errorReason")+       if not reason and isinstance(data.get("error"), str):+           reason = data.get("error")+       if not reason and data.get("error"):+           reason = "Unknown reason"++       code = data.get("errorCode")+       if not code and data.get("serverErrorCode"):+           code = data.get("serverErrorCode")+-}+parseApiError :: Object -> Parser ApiError+parseApiError o =+  let reason = o .: "errorMessage" <|> o .: "reason" <|> o .: "errorReason" <|> orError+      hasError = member "error" o+      orError = o .: "error" <|> (if hasError then pure "unknown error" else empty)+      code = o .: "errorCode" <|> o .:? "serverErrorCode"+   in ApiError <$> reason <*> code
+ src-internal/Network/HStratus/Internal/LoginFSM.hs view
@@ -0,0 +1,348 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE StrictData #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_HADDOCK prune #-}++{- |+Module      : Network.HStratus.Internal.LoginFSM+Copyright   : (c) 2025 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides datatypes that represent the Finite State Machine that specifies the+Login process+-}+module Network.HStratus.Internal.LoginFSM where++import Data.Functor ((<&>))+import Data.Kind (Type)+import Data.List.NonEmpty (NonEmpty)+import Data.Text (Text)+import Data.Word (Word8)+import Network.HStratus.Internal.Http (SrpContext (..))+import Network.HStratus.Internal.Session (AccountData, Credentials, SavedHeaders)+import Network.HStratus.Internal.Trust (Setup2SADevice, TrustData, TrustedPhone)+++-- | Configuration for the 2FA challenge process.+data TwoFaConfig = TwoFaConfig+  { tfcPickPhone :: TrustData -> IO (Maybe TrustedPhone)+  -- ^ select a phone to receive an SMS code, or 'Nothing' to use a trusted device push+  , tfcReadCode :: Word8 -> IO Text+  -- ^ prompt the user for the verification code; receives the expected digit count+  }+++-- | Configuration for the 2SA challenge process.+data TwoSaConfig = TwoSaConfig+  { tscPickDevice :: NonEmpty Setup2SADevice -> IO Setup2SADevice+  -- ^ select the device to receive a verification code+  , tscReadCode :: IO Text+  -- ^ prompt the user for the verification code+  }+++{- | @LoginEvent@ represents the valid events of the Login FSM.++Each event is represent by a typeclass function that is constrained+to go between valid states+-}+class LoginEvent m where+  -- | Represents valid finite states at the endpoints of a 'LoginEvent'+  type State m :: Type -> Type+++  initial :: m (State m RatifyCredentials)+  ratifyCreds :: State m RatifyCredentials -> m (AfterCredentials (State m))+  ratifyArtifactDir :: State m RatifyArtifactDir -> m (AfterArtifactDir (State m))+  mkArtifactDir :: State m MkArtifactDir -> m (AfterMkArtifactDir (State m))+  loadSession :: State m LoadLastSession -> m (AfterLoadLastSession (State m))+  validateSession :: State m HasSavedSession -> m (AfterValidateSession (State m))+  srpInit :: State m ReadyToAuth -> m (State m SrpInitDone)+  srpComplete :: State m SrpInitDone -> m (AfterSrpComplete (State m))+  acctLogin :: State m DoAccountLogin -> m (AfterAcctLogin (State m))+  listTwoSaDevices :: State m NeedsTwoSa -> m (State m TwoSaReady)+  beginTwoFa :: State m ReadyForTwoFa -> TwoFaConfig -> m (State m TwoFaVerifying)+  verifyTwoFa :: State m TwoFaVerifying -> TwoFaConfig -> m (AfterTwoFaVerify (State m))+  doTrust :: State m DoTrust -> m (State m DoAccountLogin)+  beginTwoSa :: State m ReadyForTwoSa -> TwoSaConfig -> m (State m TwoSaVerifying)+  verifyTwoSa :: State m TwoSaVerifying -> TwoSaConfig -> m (AfterTwoSaVerify (State m))+++-- | The outcome of 'loginProcess'.+data LoginOutcome f+  = LoginAuthenticated (f AuthComplete)+  | LoginNeedsTwoFa (f NeedsTwoFa)+  | LoginNeedsTwoSa (f TwoSaReady)+  | LoginHaltCreds (f HaltMissingCredentials)+  | LoginHaltDir (f HaltCannotMkArtifactDir)+  | LoginHaltSrp (f HaltInvalidSrp)+  | LoginHaltTwoFaLocked (f HaltTwoFaLocked)+++-- | The outcome of 'twoFaProcess' and 'twoSaProcess'.+data CompletionOutcome f+  = CompletionAuthenticated (f AuthComplete)+  | CompletionNeedsTwoFa (f NeedsTwoFa)+  | CompletionNeedsTwoSa (f TwoSaReady)+  | CompletionTwoFaLocked (f HaltTwoFaLocked)+++-- | The canonical login process using events from 'LoginEvent'.+loginProcess+  :: ( LoginEvent m+     , Monad m+     )+  => m (LoginOutcome (State m))+loginProcess =+  initial >>= ratifyCreds >>= \case+    NoCreds e -> pure $ LoginHaltCreds e+    GotCreds x -> onCredsLoaded x+++onCredsLoaded+  :: (Monad m, LoginEvent m)+  => State m RatifyArtifactDir+  -> m (LoginOutcome (State m))+onCredsLoaded s =+  ratifyArtifactDir s >>= \case+    DirPresent x -> onArtifactDirPresent x+    DirAbsent a ->+      mkArtifactDir a >>= \case+        NotMade e -> pure $ LoginHaltDir e+        DirMade x -> onArtifactDirPresent x+++onArtifactDirPresent+  :: (Monad m, LoginEvent m)+  => State m LoadLastSession+  -> m (LoginOutcome (State m))+onArtifactDirPresent s =+  loadSession s >>= \case+    HasClientId x -> onReadyToAuth x+    HasPriorSession x ->+      validateSession x >>= \case+        SessionStillValid y -> pure $ LoginAuthenticated y+        SessionStale y -> onReadyToAuth y+++onReadyToAuth+  :: (Monad m, LoginEvent m)+  => State m ReadyToAuth+  -> m (LoginOutcome (State m))+onReadyToAuth s =+  srpInit s >>= srpComplete >>= \case+    SrpCompleteOk x -> acctLogin x >>= fmap completionToLogin . onAcctLoginDone+    SrpCompleteInvalidKey x -> pure $ LoginHaltSrp x+++onAcctLoginDone+  :: (Monad m, LoginEvent m)+  => AfterAcctLogin (State m)+  -> m (CompletionOutcome (State m))+onAcctLoginDone = \case+  AcctLoginOk y -> pure $ CompletionAuthenticated y+  AcctLogin2FA y -> pure $ CompletionNeedsTwoFa y+  AcctLogin2SA y -> listTwoSaDevices y <&> CompletionNeedsTwoSa+++completionToLogin :: CompletionOutcome f -> LoginOutcome f+completionToLogin (CompletionAuthenticated x) = LoginAuthenticated x+completionToLogin (CompletionNeedsTwoFa x) = LoginNeedsTwoFa x+completionToLogin (CompletionNeedsTwoSa x) = LoginNeedsTwoSa x+completionToLogin (CompletionTwoFaLocked x) = LoginHaltTwoFaLocked x+++-- | The 2FA completion process using events from 'LoginEvent'.+twoFaProcess+  :: (LoginEvent m, Monad m)+  => State m ReadyForTwoFa+  -> TwoFaConfig+  -> m (CompletionOutcome (State m))+twoFaProcess s cfg =+  beginTwoFa s cfg >>= flip verifyTwoFa cfg >>= \case+    TwoFaOk x -> doTrust x >>= acctLogin >>= onAcctLoginDone+    TwoFaRetry x -> twoFaProcess x cfg+    TwoFaLocked x -> pure $ CompletionTwoFaLocked x+++-- | The 2SA completion process using events from 'LoginEvent'.+twoSaProcess+  :: (LoginEvent m, Monad m)+  => State m ReadyForTwoSa+  -> TwoSaConfig+  -> m (CompletionOutcome (State m))+twoSaProcess s cfg =+  beginTwoSa s cfg >>= flip verifyTwoSa cfg >>= \case+    TwoSaOk x -> acctLogin x >>= onAcctLoginDone+    TwoSaRetry x -> twoSaProcess x cfg+++{- | The states of FSM defining the login process.++Each constructor specifies the concrete data required by the process in that+state, and is tagged with a distinct phantom type.+-}+data LoginFSM s where+  RatifyCredentials :: LoginFSM RatifyCredentials+  HaltMissingCredentials :: LoginFSM HaltMissingCredentials+  RatifyArtifactDir :: Credentials -> LoginFSM RatifyArtifactDir+  MkArtifactDir :: Credentials -> LoginFSM MkArtifactDir+  HaltCannotMkArtifactDir :: Credentials -> LoginFSM HaltCannotMkArtifactDir+  LoadLastSession :: Credentials -> LoginFSM LoadLastSession+  HasSavedSession :: Credentials -> SavedHeaders -> LoginFSM HasSavedSession+  ReadyToAuth :: Credentials -> SavedHeaders -> LoginFSM ReadyToAuth+  SrpInitDone :: Credentials -> SrpContext -> LoginFSM SrpInitDone+  DoAccountLogin :: Credentials -> LoginFSM DoAccountLogin+  AuthComplete :: Credentials -> AccountData -> LoginFSM AuthComplete+  NeedsTwoFa :: Credentials -> LoginFSM NeedsTwoFa+  ReadyForTwoFa :: Credentials -> TrustData -> LoginFSM ReadyForTwoFa+  TwoFaVerifying :: Credentials -> TrustData -> Maybe TrustedPhone -> LoginFSM TwoFaVerifying+  DoTrust :: Credentials -> LoginFSM DoTrust+  NeedsTwoSa :: Credentials -> LoginFSM NeedsTwoSa+  TwoSaReady :: Credentials -> NonEmpty Setup2SADevice -> LoginFSM TwoSaReady+  ReadyForTwoSa :: Credentials -> NonEmpty Setup2SADevice -> LoginFSM ReadyForTwoSa+  TwoSaVerifying :: Credentials -> Setup2SADevice -> NonEmpty Setup2SADevice -> LoginFSM TwoSaVerifying+  HaltInvalidSrp :: Credentials -> LoginFSM HaltInvalidSrp+  HaltTwoFaLocked :: Credentials -> LoginFSM HaltTwoFaLocked+++-- | Phantom type linked to a unique state in 'LoginFSM'+data RatifyCredentials+++-- | Phantom type linked to a unique state in 'LoginFSM'+data HaltMissingCredentials+++-- | Phantom type linked to a unique state in 'LoginFSM'+data RatifyArtifactDir+++-- | Phantom type linked to a unique state in 'LoginFSM'+data MkArtifactDir+++-- | Phantom type linked to a unique state in 'LoginFSM'+data HaltCannotMkArtifactDir+++-- | Phantom type linked to a unique state in 'LoginFSM'+data LoadLastSession+++-- | Phantom type linked to a unique state in 'LoginFSM'+data HasSavedSession+++-- | Phantom type linked to a unique state in 'LoginFSM'+data ReadyToAuth+++-- | Phantom type linked to a unique state in 'LoginFSM'+data SrpInitDone+++-- | Phantom type linked to a unique state in 'LoginFSM'+data DoAccountLogin+++-- | Phantom type linked to a unique state in 'LoginFSM'+data AuthComplete+++-- | Phantom type linked to a unique state in 'LoginFSM'+data NeedsTwoFa+++-- | Phantom type linked to a unique state in 'LoginFSM'+data ReadyForTwoFa+++-- | Phantom type linked to a unique state in 'LoginFSM'+data TwoFaVerifying+++-- | Phantom type linked to a unique state in 'LoginFSM'+data DoTrust+++-- | Phantom type linked to a unique state in 'LoginFSM'+data NeedsTwoSa+++-- | Phantom type linked to a unique state in 'LoginFSM'+data TwoSaReady+++-- | Phantom type linked to a unique state in 'LoginFSM'+data ReadyForTwoSa+++-- | Phantom type linked to a unique state in 'LoginFSM'+data TwoSaVerifying+++-- | Phantom type linked to a unique state in 'LoginFSM'+data HaltInvalidSrp+++-- | Phantom type linked to a unique state in 'LoginFSM'+data HaltTwoFaLocked+++-- | The valid states after 'loadSession'+data AfterLoadLastSession f+  = HasClientId (f ReadyToAuth)+  | HasPriorSession (f HasSavedSession)+++-- | The valid states after 'validateSession'+data AfterValidateSession f+  = SessionStillValid (f AuthComplete)+  | SessionStale (f ReadyToAuth)+++-- | The valid states after 'mkArtifactDir'+data AfterMkArtifactDir f+  = NotMade (f HaltCannotMkArtifactDir)+  | DirMade (f LoadLastSession)+++-- | The valid states after 'ratifyArtifactDir'+data AfterArtifactDir f+  = DirPresent (f LoadLastSession)+  | DirAbsent (f MkArtifactDir)+++-- | The valid states after 'ratifyCreds'+data AfterCredentials f+  = NoCreds (f HaltMissingCredentials)+  | GotCreds (f RatifyArtifactDir)+++-- | The valid states after 'srpComplete'+data AfterSrpComplete f+  = SrpCompleteOk (f DoAccountLogin)+  | SrpCompleteInvalidKey (f HaltInvalidSrp)+++-- | The valid states after 'acctLogin'+data AfterAcctLogin f+  = AcctLoginOk (f AuthComplete)+  | AcctLogin2FA (f NeedsTwoFa)+  | AcctLogin2SA (f NeedsTwoSa)+++-- | The valid states after 'verifyTwoFa'+data AfterTwoFaVerify f+  = TwoFaOk (f DoTrust)+  | TwoFaRetry (f ReadyForTwoFa)+  | TwoFaLocked (f HaltTwoFaLocked)+++-- | The valid states after 'verifyTwoSa'+data AfterTwoSaVerify f+  = TwoSaOk (f DoAccountLogin)+  | TwoSaRetry (f ReadyForTwoSa)
+ src-internal/Network/HStratus/Internal/PBKDF2.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BinaryLiterals #-}+{-# OPTIONS_HADDOCK prune #-}++{- |+Module      : Network.HStratus.PBKDF2+Copyright   : (c) 2025 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Copied then modified from an implementation in the package+[ppad-pbkdf](https://git.ppad.tech/pbkdf/file/lib/Crypto/KDF/PBKDF.hs.html)++Re-implemented here rather than making it direct dependency, because:+    - 1 fewer dependency => less future dependency-related maintenance+    - faster route for this package to stackage+        - as of (2025/04/01, ppad-ppbkdf was not on stackage)+-}+module Network.HStratus.Internal.PBKDF2+  ( -- * specify a pseudorandom function and derived key length+    FancyPseudoRandomF+  , wrap+  , wrapIO+  , PseudoRandomF+  , BadKeyLength (..)++    -- * perform PBKDF2 derivation+  , deriveKey++    -- * re-export+  , ByteString+  )+where++import Control.Exception (Exception, throwIO)+import Data.Bits (shiftR, xor, (.&.))+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.ByteString.Builder (byteString, toLazyByteString)+import Data.ByteString.Builder.Extra+  ( safeStrategy+  , smallChunkSize+  , toLazyByteStringWith+  )+import Data.Word (Word32, Word64)+++{- | A pseudorandom function for use in PBKDF2++See+[PBKDF-RFC/section5.2](https://datatracker.ietf.org/doc/html/rfc2898#section-5.2)+-}+type PseudoRandomF = ByteString -> ByteString -> ByteString+++-- | Indicates the derived key length is too long+data BadKeyLength = TooLong+  deriving (Eq, Show)+++instance Exception BadKeyLength+++{- | A 'PseudoRandomF' wrapped up with @dkLen@ and @hLen@'++where @dkLen@ the required length in octets of the derived key+and @hLen@ is the length of the output of the 'PseudoRandomF'++As per+[PBKDF-RFC/section5.2](https://datatracker.ietf.org/doc/html/rfc2898#section-5.2)++@dkLen@ must be at most 2^32 - 1 * @hLen@++The constructor `wrap` enforces this constraint+-}+newtype FancyPseudoRandomF = Fancy (PseudoRandomF, Word32, Word32)+++-- | Construct a 'FancyPseudoRandomF'+wrap :: PseudoRandomF -> Word32 -> Either BadKeyLength FancyPseudoRandomF+wrap f dkLen =+  let !hLen = toNum $ BS.length $ f mempty mempty+   in if dkLen > 0xffffffff * hLen+        then Left TooLong+        else Right $ Fancy (f, dkLen, hLen)+++-- | Like 'wrap', but fails by throwing 'BadKeyLength' in IO+wrapIO :: PseudoRandomF -> Word32 -> IO FancyPseudoRandomF+wrapIO f = either throwIO pure . wrap f+++blockInfoOf :: FancyPseudoRandomF -> (Word32, Int)+blockInfoOf (Fancy (_f, !dkLen, hLen)) =+  let numBlocks = ceiling (toNum dkLen / toNum hLen :: Double)+      lastBlockSize = toNum $ dkLen - (numBlocks - 1) * hLen+   in (numBlocks, lastBlockSize)+++{- | Derive a key from a secret using PBKDF2++Implements the key derivation algorithm described in+[PBKDF-RFC](https://datatracker.ietf.org/doc/html/rfc2898)++Usage - this example uses the SHA256 hmac function as the pseudorandom function++  >>> :set -XOverloadedStrings+  >>> import qualified Crypto.Hash.SHA256 as SHA256+  >>> pseudoF <- wrapIO SHA256.hmac 64+  >>> deriveKey pseudoF "passwd" "salt" 1000+-}+deriveKey+  :: FancyPseudoRandomF+  -- ^ a 'FancyPseudoRandomF'+  -> ByteString+  -- ^ the password from which to derive a key+  -> ByteString+  -- ^ the salt used in key derivation+  -> Word64+  -- ^ the iteration count+  -> ByteString+deriveKey fancy password salt count =+  let Fancy (!pseudoRandomF, !dkLen, !_notUsed) = fancy+      (!numBlocks, !lastBlockSize) = blockInfoOf fancy+      xorSum i =+        let initial = pseudoRandomF password $ salt <> asBytes i+            go j !current !_ignored | j == count = current+            go j !current !previous =+              let latest = pseudoRandomF password previous+               in go (j + 1) (current `xorBytes` latest) latest+         in go 1 initial initial+      {-# INLINE xorSum #-}++      smaller = safeStrategy 128 smallChunkSize+      strictBS =+        if dkLen <= 128+          then BS.toStrict . toLazyByteStringWith smaller mempty+          else BS.toStrict . toLazyByteString+      {-# INLINE strictBS #-}++      genBlocks i acc =+        if i < numBlocks+          then genBlocks (i + 1) (acc <> byteString (xorSum i))+          else strictBS $ acc <> byteString (BS.take lastBlockSize $ xorSum i)+   in genBlocks 1 mempty+++toNum :: (Integral a, Num b) => a -> b+toNum = fromIntegral+{-# INLINE toNum #-}+++asBytes :: Word32 -> ByteString+asBytes x =+  let !mask = 0b00000000000000000000000011111111+      !word0 = toNum (x `shiftR` 24) .&. mask+      !word1 = toNum (x `shiftR` 16) .&. mask+      !word2 = toNum (x `shiftR` 08) .&. mask+      !word3 = toNum x .&. mask+   in BS.cons word0 $ BS.cons word1 $ BS.cons word2 $ BS.singleton word3+{-# INLINE asBytes #-}+++xorBytes :: ByteString -> ByteString -> ByteString+xorBytes = BS.packZipWith xor+{-# INLINE xorBytes #-}
+ src-internal/Network/HStratus/Internal/Session.hs view
@@ -0,0 +1,572 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{-# OPTIONS_HADDOCK prune #-}++module Network.HStratus.Internal.Session+  ( -- * Credentials+    Credentials (..)++    -- ** paths related to @Credentials@+  , cookiePath+  , clientIdPath+  , credentialsPath+  , savedHeadersPath+  , loginMsgPath++    -- * Session+  , Session (..)+  , SavedHeaders (..)+  , loadSession+  , saveCredentials+  , saveCredentialsTo+  , loadSavedHeaders+  , updateSessionSavedHeaders+  , updateSavedHeaders+  , pristine+  , saveLoginMsg++    -- * AccountData+  , Webservice (..)+  , AccountData (..)+  , accountDataRequires2FA+  , accountDataRequires2SA+  , unknownAccountData+  , accountDataPath+  , saveAccountData+  , loadAccountData++    -- * path components+  , appBase+  , (</>)++    -- * Utilities+  , encodeFileAtomic++    -- * File security+  , checkSecureMode+  , requireSecureFile+  , checkSessionFiles+  )+where++import Control.Applicative ((<|>))+import Control.Exception (bracketOnError, throwIO)+import Control.Monad (forM, when, (>=>))+import Data.Aeson+  ( FromJSON (..)+  , KeyValue (..)+  , Options (..)+  , ToJSON (..)+  , Value+  , eitherDecodeFileStrict+  , encode+  , genericParseJSON+  , genericToEncoding+  , genericToJSON+  , object+  , withObject+  , (.:)+  , (.:?)+  )+import Data.Aeson.Casing (aesonPrefix, snakeCase)+import qualified Data.Aeson.Key as AesonKey+import qualified Data.Aeson.KeyMap as KeyMap+import Data.Bits ((.&.))+import qualified Data.ByteString.Lazy as LBS+import Data.Char (isAlphaNum)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe (catMaybes)+import Data.String.Conv (toS)+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import Data.UUID (toText)+import Data.UUID.V4 (nextRandom)+import GHC.Generics (Generic)+import Network.HStratus.Internal.Http+  ( hCounter+  , hCountry+  , hSessionId+  , hSessionToken+  , hTrustToken+  )+import Network.HTTP.Types.Header (Header)+import Numeric (showOct)+import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile, renameFile)+import System.Environment.XDG.BaseDir (getUserConfigDir)+import System.FilePath (takeDirectory, (</>))+import System.IO (hClose, openTempFile)+import System.Posix.Files (fileMode, getFileStatus, setFileMode)+import System.Posix.Types (FileMode)+++-- | Update the @SavedHeaders@ using some response headers+updateSavedHeaders :: [Header] -> SavedHeaders -> SavedHeaders+updateSavedHeaders hs sd =+  sd+    { shCountry = (toS <$> lookup hCountry hs) <|> shCountry sd+    , shSessionId = (toS <$> lookup hSessionId hs) <|> shSessionId sd+    , shSessionToken = (toS <$> lookup hSessionToken hs) <|> shSessionToken sd+    , shTrustToken = (toS <$> lookup hTrustToken hs) <|> shTrustToken sd+    , shCounter = (toS <$> lookup hCounter hs) <|> shCounter sd+    }+++data Session = Session+  { sessionCreds :: !Credentials+  -- ^ the credentials used to authenticate+  , sessionTopDir :: !FilePath+  -- ^ directory where session files (cookies, headers, account data) are stored+  , sessionClientId :: !Text+  -- ^ per-client OAuth state identifier sent with each request+  }+  deriving+    ( Eq+      -- ^ don't derive Show to avoid the risk of logging a password+    )+++-- | Generates a new client ID.+newClientId :: IO Text+newClientId = ("auth-" <>) . toText <$> nextRandom+++{- | Determine the path of file containing the HTTP response headers to be+preserved to maintain a user's authentication state+-}+savedHeadersPath :: FilePath -> Credentials -> FilePath+savedHeadersPath topDir creds = topDir </> Text.unpack (sessionBase creds)+++-- | Determine the Cookie Jar file for user with the given credentials+cookiePath :: FilePath -> Credentials -> FilePath+cookiePath topDir creds = topDir </> Text.unpack (cookieBase creds)+++{- | Determine the path of file containing the client ID for user with the given+credentials+-}+clientIdPath :: FilePath -> Credentials -> FilePath+clientIdPath topDir creds = topDir </> Text.unpack (clientIdBase creds)+++-- | Determine the path of file to save the message the api returns on logon+loginMsgPath :: FilePath -> Credentials -> FilePath+loginMsgPath topDir creds = topDir </> Text.unpack (loginMsgBase creds)+++-- | Save the login message to user specific filepath+saveLoginMsg :: Session -> Value -> IO ()+saveLoginMsg Session{sessionCreds = creds, sessionTopDir = topDir} = saveValue (loginMsgPath topDir creds)+++-- | Metadata for a single iCloud webservice entry.+data Webservice = Webservice+  { wsUrl :: !Text+  -- ^ the base URL for the service+  , wsStatus :: !(Maybe Text)+  -- ^ service status, e.g. @"active"@ or @"inactive"@; @Nothing@ if absent+  }+  deriving (Eq, Show)+++data AccountData = AccountData+  { adHsaVersion :: !Int+  -- ^ HSA protocol version; drives the two-factor flow selection+  , adHsaChallengeRequired :: !Bool+  -- ^ @True@ when a 2FA challenge must be completed before access is granted+  , adHsaTrustedBrowser :: !(Maybe Bool)+  {- ^ @Just True@ when this session is already trusted; @Just False@ when explicitly+  untrusted; @Nothing@ when the key was absent from Apple's response (treated as trusted)+  -}+  , adWebservices :: !(Map Text Webservice)+  -- ^ map of webservice name to service info; use 'lookupWebservice' to resolve a URL+  , adRaw :: !Value+  -- ^ the original JSON value from Apple; preserved so serialisation round-trips losslessly+  }+  deriving (Eq, Show)+++instance FromJSON AccountData where+  parseJSON v = withObject "AccountData" go v+   where+    go o = do+      dsInfo <- o .: "dsInfo"+      adHsaVersion <- withObject "dsInfo" (.: "hsaVersion") dsInfo+      adHsaChallengeRequired <- o .:? "hsaChallengeRequired" >>= maybe (pure False) pure+      adHsaTrustedBrowser <- o .:? "hsaTrustedBrowser"+      adWebservices <- do+        mbWs <- o .:? "webservices"+        maybe (pure Map.empty) (withObject "webservices" parseWebservices) mbWs+      pure AccountData{adHsaVersion, adHsaChallengeRequired, adHsaTrustedBrowser, adWebservices, adRaw = v}+    parseWebservices obj = do+      let pairs = KeyMap.toAscList obj+      wsPairs <- forM pairs $ \(k, wsVal) ->+        withObject+          "webservice"+          ( \sv -> do+              mbUrl <- sv .:? "url"+              mbStatus <- sv .:? "status"+              pure $ fmap (\u -> (AesonKey.toText k, Webservice u mbStatus)) mbUrl+          )+          wsVal+      pure $ Map.fromList $ catMaybes wsPairs+++instance ToJSON AccountData where+  toJSON AccountData{adRaw} = adRaw+  toEncoding AccountData{adRaw} = toEncoding adRaw+++-- | True when full 2FA (auth-endpoint) challenge is required+accountDataRequires2FA :: AccountData -> Bool+accountDataRequires2FA ad =+  adHsaVersion ad == 2+    && (adHsaChallengeRequired ad || adHsaTrustedBrowser ad == Just False)+++-- | True when legacy 2SA (setup-endpoint) challenge is required+accountDataRequires2SA :: AccountData -> Bool+accountDataRequires2SA ad = adHsaVersion ad == 1+++-- | Sentinel used when no saved @AccountData@ is available+unknownAccountData :: AccountData+unknownAccountData =+  AccountData+    { adHsaVersion = 0+    , adHsaChallengeRequired = False+    , adHsaTrustedBrowser = Nothing+    , adWebservices = Map.empty+    , adRaw = object []+    }+++accountDataBase :: Credentials -> Text+accountDataBase = (<> ".account-data.json") . sprucedName+++-- | Determine the path of the saved account-data file for the given credentials+accountDataPath :: FilePath -> Credentials -> FilePath+accountDataPath topDir creds = topDir </> Text.unpack (accountDataBase creds)+++-- | Persist @AccountData@ to the session's filesystem location+saveAccountData :: Session -> AccountData -> IO ()+saveAccountData Session{sessionCreds = creds, sessionTopDir = topDir} =+  secureEncodeFileAtomic (accountDataPath topDir creds)+++-- | Load persisted @AccountData@; returns @Nothing@ if the file is absent+loadAccountData :: Session -> IO (Maybe AccountData)+loadAccountData Session{sessionCreds = creds, sessionTopDir = topDir} = do+  let path = accountDataPath topDir creds+  requireSecureFile path+  exists <- doesFileExist path+  if not exists+    then pure Nothing+    else eitherDecodeFileStrict path >>= either (const (pure Nothing)) (pure . Just)+++{- | Determine the path of file containing the credentials in the configuration+   directory+-}+credentialsPath :: FilePath -> FilePath+credentialsPath topDir = topDir </> "credentials.json"+++saveCredentials :: Credentials -> IO ()+saveCredentials creds = getUserConfigDir appBase >>= (`saveCredentialsTo` creds)+++-- | Write 'Credentials' to @credentials.json@ inside @topDir@, creating @topDir@ if absent.+saveCredentialsTo :: FilePath -> Credentials -> IO ()+saveCredentialsTo topDir creds = do+  createDirectoryIfMissing True topDir+  secureEncodeFileAtomic (credentialsPath topDir) creds+++data Credentials = Credentials+  { credAccountName :: !Text+  -- ^ the account ID; typically an email address+  , credPassword :: !Text+  -- ^ the iCloud account password+  }+  deriving+    ( Eq+      -- ^ don't derive Show to avoid the risk of logging a password+    )+++instance FromJSON Credentials where+  parseJSON = withObject "Credentials" $ \o ->+    let accountName = o .: "accountName"+        password = o .: "password"+     in Credentials <$> accountName <*> password+++instance ToJSON Credentials where+  toJSON c =+    object+      [ "password" .= credPassword c+      , "accountName" .= credAccountName c+      ]+++sprucedName :: Credentials -> Text+sprucedName =+  let p aChar = isAlphaNum aChar || aChar == '@'+      replaceAt = Text.replace "@" "-"+   in replaceAt . Text.filter p . credAccountName+++cookieBase :: Credentials -> Text+cookieBase = (<> ".cookies.txt") . sprucedName+++sessionBase :: Credentials -> Text+sessionBase = (<> ".session.json") . sprucedName+++clientIdBase :: Credentials -> Text+clientIdBase = (<> ".client-id.txt") . sprucedName+++loginMsgBase :: Credentials -> Text+loginMsgBase = (<> ".last-logon.json") . sprucedName+++-- | Data obtained from HTTP response headers that define a user session+data SavedHeaders = SavedHeaders+  { shCountry :: !(Maybe Text)+  -- ^ X-Apple-ID-Country value from the last response+  , shSessionId :: !(Maybe Text)+  -- ^ X-Apple-ID-Session-Id value from the last response+  , shSessionToken :: !(Maybe Text)+  -- ^ X-Apple-Session-Token value from the last response+  , shTrustToken :: !(Maybe Text)+  -- ^ X-Apple-TwoSV-Trust-Token value from the last response+  , shCounter :: !(Maybe Text)+  -- ^ X-Apple-HC-Bits value from the last response; used to derive hashcash proofs+  }+  deriving (Eq, Show, Generic)+++instance FromJSON SavedHeaders where+  parseJSON = genericParseJSON simpleOptions+++instance ToJSON SavedHeaders where+  toJSON = genericToJSON simpleOptions+  toEncoding = genericToEncoding simpleOptions+++-- | A @SavedHeaders@ with nothing set+pristine :: SavedHeaders+pristine = SavedHeaders Nothing Nothing Nothing Nothing Nothing+++{- | Update the stored saved headers++if the sessionData file exists+then+  load it.+  update the session data from the headers+  save the updated data+else+  ensure its parent directory exists+  create the session data from the headers+  save it++not handled (thrown as IOException):+  cannot create directory+  cannot write due to permissions+  file exists, but data cannot be parsed+-}+updateSessionSavedHeaders+  :: Session+  -> (SavedHeaders -> SavedHeaders)+  -- ^ a function that modifies the session's saved headers+  -> IO ()+updateSessionSavedHeaders s modSavedHeaders = do+  let dataPath = savedHeadersPath (sessionTopDir s) (sessionCreds s)+      updateAndSave = secureEncodeFileAtomic dataPath . modSavedHeaders+      loadLast False = pure pristine+      loadLast True = eitherDecodeFileStrict dataPath >>= either (fail . show) pure++  doesFileExist dataPath >>= loadLast >>= updateAndSave+++loadSession :: IO Session+loadSession = do+  sessionTopDir <- getUserConfigDir appBase+  createDirectoryIfMissing True sessionTopDir+  s <-+    loadSessionOr sessionTopDir+      >>= orFail "Credentials are missing or corrupt; run 'hstratus auth login' to authenticate"+  checkSessionFiles s+  pure s+++-- | Saves a JSON @Value@ to @filepath@+saveValue :: FilePath -> Value -> IO ()+saveValue fp v = LBS.writeFile fp $ encode v+++orFail :: String -> Either String a -> IO a+orFail hint = either (\e -> fail (hint <> " (" <> e <> ")")) pure+++-- | Write a JSON-encodable value to @path@ atomically via a temp file and rename.+encodeFileAtomic :: (ToJSON a) => FilePath -> a -> IO ()+encodeFileAtomic path value =+  bracketOnError+    (openTempFile (takeDirectory path) ".tmp")+    (\(tmpPath, h) -> hClose h >> removeFile tmpPath)+    ( \(tmpPath, h) -> do+        LBS.hPut h (encode value)+        hClose h+        renameFile tmpPath path+    )+++{- | Like 'encodeFileAtomic' but sets mode @0o600@ on the temp file before+renaming, so the file is never visible at a more permissive mode.+-}+secureEncodeFileAtomic :: (ToJSON a) => FilePath -> a -> IO ()+secureEncodeFileAtomic path value =+  bracketOnError+    (openTempFile (takeDirectory path) ".tmp")+    (\(tmpPath, h) -> hClose h >> removeFile tmpPath)+    ( \(tmpPath, h) -> do+        LBS.hPut h (encode value)+        hClose h+        setFileMode tmpPath 0o600+        renameFile tmpPath path+    )+++-- | Write text to @path@ atomically, setting mode @0o600@ before renaming.+secureWriteTextFileAtomic :: FilePath -> Text -> IO ()+secureWriteTextFileAtomic path content =+  bracketOnError+    (openTempFile (takeDirectory path) ".tmp")+    (\(tmpPath, h) -> hClose h >> removeFile tmpPath)+    ( \(tmpPath, h) -> do+        Text.hPutStr h content+        hClose h+        setFileMode tmpPath 0o600+        renameFile tmpPath path+    )+++{- | Pure permission check using the SSH convention: no group or world bits may+be set.  Returns @Left@ with a descriptive message (including a @chmod 600@ hint)+when the mode is too permissive.+-}+checkSecureMode :: FileMode -> FilePath -> Either String ()+checkSecureMode mode path+  | mode .&. 0o077 /= 0 =+      Left $+        path+          <> " has unsafe permissions ("+          <> showOct (fromIntegral mode :: Int) ""+          <> "); fix with: chmod 600 "+          <> path+  | otherwise = Right ()+++{- | Verify that a file has secure permissions before it is read.  Does nothing+if the file does not exist; absence is handled by the caller.  Throws an+'IOError' when the file exists but its mode has group or world bits set.+-}+requireSecureFile :: FilePath -> IO ()+requireSecureFile path = do+  exists <- doesFileExist path+  when exists $ do+    mode <- fileMode <$> getFileStatus path+    either (throwIO . userError) pure (checkSecureMode mode path)+++{- | Check that every session file that exists has secure permissions.  Absent+files are skipped silently.  Throws an 'IOError' for the first file found with+group or world bits set.++Covers all five session paths: credentials, saved headers, client ID, account+data, and the cookie jar.+-}+checkSessionFiles :: Session -> IO ()+checkSessionFiles sess = do+  let topDir = sessionTopDir sess+      creds = sessionCreds sess+  mapM_+    requireSecureFile+    [ credentialsPath topDir+    , savedHeadersPath topDir creds+    , clientIdPath topDir creds+    , accountDataPath topDir creds+    , cookiePath topDir creds+    ]+++loadCredentials :: FilePath -> IO (Either String Credentials)+loadCredentials topDir = do+  let path = credentialsPath topDir+  requireSecureFile path+  eitherDecodeFileStrict path+++loadCredentials' :: FilePath -> IO (Either String (FilePath, Credentials))+loadCredentials' topDir = fmap (topDir,) <$> loadCredentials topDir+++loadSession' :: Either String (FilePath, Credentials) -> IO (Either String Session)+loadSession' (Left err) = pure $ Left err+loadSession' (Right (sessionTopDir, sessionCreds)) = do+  sessionClientId <- loadClientId sessionTopDir sessionCreds+  pure $ Right Session{sessionClientId, sessionCreds, sessionTopDir}+++loadSessionOr :: FilePath -> IO (Either String Session)+loadSessionOr = loadCredentials' >=> loadSession'+++-- | Load the @SavedHeaders@ for this session+loadSavedHeaders :: Session -> IO SavedHeaders+loadSavedHeaders Session{sessionTopDir, sessionCreds} =+  loadSavedHeaders' sessionTopDir sessionCreds+    >>= orFail "Session state is corrupt; run 'hstratus auth login' to re-authenticate"+++loadSavedHeaders' :: FilePath -> Credentials -> IO (Either String SavedHeaders)+loadSavedHeaders' topDir creds = do+  let dataPath = savedHeadersPath topDir creds+  requireSecureFile dataPath+  pathExists <- doesFileExist dataPath+  if not pathExists+    then pure $ Right pristine+    else eitherDecodeFileStrict dataPath+++loadClientId :: FilePath -> Credentials -> IO Text+loadClientId topDir creds = do+  let dataPath = clientIdPath topDir creds+  requireSecureFile dataPath+  pathExists <- doesFileExist dataPath+  if pathExists+    then Text.readFile dataPath+    else do+      anId <- newClientId+      secureWriteTextFileAtomic dataPath anId+      pure anId+++simpleOptions :: Options+simpleOptions = aesonPrefix snakeCase+++appBase :: FilePath+appBase = "hstratus"
+ src-internal/Network/HStratus/Internal/Trust.hs view
@@ -0,0 +1,336 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_HADDOCK prune #-}++{- |+Module      : Network.HStratus.Internal.Trust+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Internal types and functions for two-factor authentication trust management.+-}+module Network.HStratus.Internal.Trust+  ( -- * data types+    CodeStatus (..)+  , TrustedPhone (..)+  , TrustedDevice (..)+  , TrustedList (..)+  , TrustData (..)+  , Setup2SADevice (..)++    -- * functions+  , withSelectedPhoneOrDevice+  , pleaseReadCode+  , pleaseChooseN+  , selectPhone+  , selectDevice+  , selectTwoFaPhone+  , setup2SADeviceLabel+  , selectSetupDevice+  )+where++import Control.Applicative ((<|>))+import Control.Exception (IOException, catch, throwIO)+import Data.Aeson+  ( FromJSON (..)+  , KeyValue (..)+  , Object+  , Options (..)+  , SumEncoding (ObjectWithSingleField)+  , ToJSON (..)+  , Value (..)+  , genericParseJSON+  , genericToEncoding+  , genericToJSON+  , object+  , withObject+  , (.:)+  , (.:?)+  )+import Data.Aeson.Casing (aesonPrefix, camelCase)+import Data.Aeson.KeyMap (filterWithKey)+import qualified Data.Aeson.KeyMap as KeyMap+import Data.Aeson.Types (Parser)+import Data.List.NonEmpty (NonEmpty (..), toList)+import Data.Maybe (fromMaybe, listToMaybe)+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import Data.Word (Word8)+import GHC.Generics (Generic)+import SimplePrompt (promptNonEmpty)+import System.IO.Error (isEOFError)+import Text.Read (readMaybe)+++putDeviceChoice :: (Int, TrustedDevice) -> IO ()+putDeviceChoice (i, td)+  | tdModelName td == "" =+      Text.putStrLn $ Text.pack (show i) <> ") " <> tdName td <> "\tSMS\t" <> tdId td+  | otherwise =+      Text.putStrLn $ Text.pack (show i) <> ") " <> tdName td <> "\t" <> tdModelName td <> "\t" <> tdId td+++-- idx is 1-based and in [1, length xs], as enforced by pleaseChooseN+nthOf :: NonEmpty a -> Int -> a+nthOf xs idx = toList xs !! (idx - 1)+++-- | Prompt the user to choose one device from a non-empty list of trusted devices.+selectDevice :: NonEmpty TrustedDevice -> IO TrustedDevice+selectDevice xs = do+  Text.putStrLn "Please select a trusted device to send a code to"+  mapM_ putDeviceChoice $ zip ([1 ..] :: [Int]) (toList xs)+  idx <- pleaseChooseN 1 (length xs)+  pure (nthOf xs idx)+++-- | Prompt the user to choose one phone number from a non-empty list of trusted phones.+selectPhone :: NonEmpty TrustedPhone -> IO TrustedPhone+selectPhone xs = do+  let putPhoneChoice (i, x) = Text.putStrLn $ Text.pack (show i) <> ") " <> tpnNumberWithDialCode x+  Text.putStrLn "Please select a trusted phone number to send a code to"+  mapM_ putPhoneChoice $ zip ([1 ..] :: [Int]) (toList xs)+  idx <- pleaseChooseN 1 (length xs)+  pure (nthOf xs idx)+++-- | Prompt the user to enter an integer in the inclusive range @[low, high]@, retrying on invalid input.+pleaseChooseN :: Int -> Int -> IO Int+pleaseChooseN low high = do+  let prefix = "Please choose an option between " <> show low <> " and " <> show high+  result <- (readMaybe <$> promptNonEmpty prefix) `catch` onEof+  case result of+    Nothing -> pleaseChooseN low high+    Just x | x < low || x > high -> pleaseChooseN low high+    Just x -> pure x+ where+  onEof :: IOException -> IO (Maybe Int)+  onEof e+    | isEOFError e = throwIO (userError "unexpected end of input")+    | otherwise = throwIO e+++-- | Prompt the user to enter a security code of the given length.+pleaseReadCode :: Word8 -> IO Text+pleaseReadCode len = do+  let prefix = "Please enter the " <> show len <> "-digit code you just received"+  Text.pack <$> promptNonEmpty prefix+++-- | Information describing the status of the security code verification+data CodeStatus = CodeStatus+  { scLength :: !Word8+  -- ^ expected number of digits in the security code+  , scTooManyCodesSent :: !Bool+  -- ^ @True@ when Apple has refused to send further codes+  , scTooManyCodesValidated :: !Bool+  -- ^ @True@ when the verification attempt limit has been reached+  , scSecurityCodeLocked :: !Bool+  -- ^ @True@ when the security code gate is locked+  , scSecurityCodeCooldown :: !Bool+  -- ^ @True@ when a cooldown period is active before a new code can be sent+  }+  deriving (Eq, Show, Generic)+++instance FromJSON CodeStatus where+  parseJSON = withObject "CodeStatus" $ \o ->+    CodeStatus+      <$> o .: "length"+      <*> (fromMaybe False <$> o .:? "tooManyCodesSent")+      <*> (fromMaybe False <$> o .:? "tooManyCodesValidated")+      <*> (fromMaybe False <$> o .:? "securityCodeLocked")+      <*> (fromMaybe False <$> o .:? "securityCodeCooldown")+++instance ToJSON CodeStatus where+  toJSON = genericToJSON simpleOptions+  toEncoding = genericToEncoding simpleOptions+++-- | A trusted phone number registered for two-factor verification+data TrustedPhone = TrustedPhone+  { tpnId :: !Word8+  -- ^ Apple's internal identifier for this phone number+  , tpnNumberWithDialCode :: !Text+  -- ^ display string including the country dial code, e.g. @"+1 (•••) •••-1234"@+  , tpnPushMode :: !(Maybe Text)+  -- ^ push delivery mode (e.g. @"sms"@); @Nothing@ when absent+  }+  deriving (Eq, Show, Generic)+++instance FromJSON TrustedPhone where+  parseJSON = genericParseJSON simpleOptions+++instance ToJSON TrustedPhone where+  toJSON = genericToJSON simpleOptions+  toEncoding = genericToEncoding simpleOptions+++-- | Information about a trusted device+data TrustedDevice = TrustedDevice+  { tdId :: !Text+  -- ^ Apple's internal identifier for this device+  , tdName :: !Text+  -- ^ human-readable device name, e.g. @"Tim's iPhone"@+  , tdModelName :: !Text+  -- ^ model string, e.g. @"iPhone 15 Pro"@; empty string when absent+  }+  deriving (Eq, Show, Generic)+++instance FromJSON TrustedDevice where+  parseJSON = withObject "TrustedDevice" $ \o ->+    TrustedDevice+      <$> o .: "id"+      <*> o .: "name"+      <*> (fromMaybe "" <$> o .:? "modelName")+++instance ToJSON TrustedDevice where+  toJSON = genericToJSON simpleOptions+  toEncoding = genericToEncoding simpleOptions+++-- | A non-empty list of @TrustedPhone@ or @TrustedDevice@+data TrustedList+  = -- | the account has trusted phone numbers but no trusted devices+    TrustedPhoneNumbers !(NonEmpty TrustedPhone)+  | -- | the account has trusted devices (and may also have trusted phone numbers)+    TrustedDevices !(NonEmpty TrustedDevice)+  deriving (Eq, Show, Generic)+++instance FromJSON TrustedList where+  parseJSON = genericParseJSON trustedListOptions+++instance ToJSON TrustedList where+  toJSON = genericToJSON trustedListOptions+  toEncoding = genericToEncoding trustedListOptions+++trustedListOptions :: Options+trustedListOptions =+  ( simpleOptions+      { sumEncoding = ObjectWithSingleField+      , constructorTagModifier = camelCase+      }+  )+++data TrustData = TrustData+  { tdList :: !TrustedList+  -- ^ trusted phones or devices that can receive a verification code+  , tdSecurityCode :: !CodeStatus+  -- ^ current status of the security-code gate (length, lockout flags)+  , tdNoTrustedDevices :: !Bool+  -- ^ @True@ when no trusted devices are registered; only phone numbers available+  }+  deriving (Eq, Show)+++-- | Selects a phone/device and applies the appropriate handler+withSelectedPhoneOrDevice+  :: (TrustedPhone -> IO a) -> (TrustedDevice -> IO a) -> TrustData -> IO a+withSelectedPhoneOrDevice handlePhone handleDevice = do+  let ikou (TrustedDevices ys) = selectDevice ys >>= handleDevice+      ikou (TrustedPhoneNumbers (y :| [])) = handlePhone y+      ikou (TrustedPhoneNumbers ys) = selectPhone ys >>= handlePhone+  ikou . tdList+++toJSONTrustData :: TrustData -> Value+toJSONTrustData td =+  let asPairs (Object o) = KeyMap.toList o+      asPairs _other = []+      fromOthers =+        [ "securityCode" .= tdSecurityCode td+        , "noTrustedDevices" .= tdNoTrustedDevices td+        ]+      fromTrustedList = asPairs $ toJSON $ tdList td+   in object $ fromOthers <> fromTrustedList+++parseJSONTrustData :: Value -> Parser TrustData+parseJSONTrustData = withObject "TrustData" $ \o ->+  let securityCode = o .: "securityCode"+      noTrustedDevices = fromMaybe False <$> o .:? "noTrustedDevices"+      isListKey key _ignored = key == "trustedPhoneNumbers" || key == "trustedDevices"+      theList = parseJSON (Object $ filterWithKey isListKey o)+   in TrustData <$> theList <*> securityCode <*> noTrustedDevices+++instance ToJSON TrustData where+  toJSON = toJSONTrustData+++instance FromJSON TrustData where+  parseJSON = parseJSONTrustData+++-- | An opaque device record used in the legacy 2SA flow; fields are Apple-defined JSON.+newtype Setup2SADevice = Setup2SADevice {setup2SAFields :: Object}+  deriving (Eq, Show)+++instance FromJSON Setup2SADevice where+  parseJSON = withObject "Setup2SADevice" (pure . Setup2SADevice)+++instance ToJSON Setup2SADevice where+  toJSON (Setup2SADevice o) = Object o+++-- | Extract a human-readable label from a 2SA setup device, falling back to @"(unknown)"@.+setup2SADeviceLabel :: Setup2SADevice -> Text+setup2SADeviceLabel (Setup2SADevice o) = fromMaybe "(unknown)" $ do+  v <- lookup "phoneNumber" pairs <|> lookup "name" pairs+  case v of+    String t -> Just t+    _ -> Nothing+ where+  pairs = KeyMap.toList o+++-- | Select a trusted phone from 'TrustData' for 2FA, prompting the user when multiple phones are available.  Returns 'Nothing' when the user opts for a trusted device instead.+selectTwoFaPhone :: TrustData -> IO (Maybe TrustedPhone)+selectTwoFaPhone td =+  let phones = case tdList td of+        TrustedPhoneNumbers ps -> toList ps+        TrustedDevices _ -> []+   in if tdNoTrustedDevices td+        then pure (listToMaybe phones)+        else pickPhoneOrDevice phones+ where+  pickPhoneOrDevice [] = pure Nothing+  pickPhoneOrDevice phones = do+    mapM_+      (\(i, p) -> Text.putStrLn $ Text.pack (show (i :: Int)) <> ") " <> tpnNumberWithDialCode p)+      (zip [1 ..] phones)+    Text.putStrLn "Press Enter to use a trusted device, or select a phone number by its index to receive an SMS:"+    response <- Text.getLine+    if Text.null response+      then pure Nothing+      else case readMaybe (Text.unpack response) of+        Just n | n >= (1 :: Int) && n <= length phones -> pure $ listToMaybe $ drop (n - 1) phones+        _ -> pickPhoneOrDevice phones+++-- | Prompt the user to choose a trusted device to receive a legacy 2SA verification code.+selectSetupDevice :: NonEmpty Setup2SADevice -> IO Setup2SADevice+selectSetupDevice xs = do+  Text.putStrLn "Please select a trusted device to receive a verification code"+  mapM_ (\(i, d) -> Text.putStrLn $ Text.pack (show (i :: Int)) <> ") " <> setup2SADeviceLabel d) (zip [1 ..] (toList xs))+  idx <- pleaseChooseN 1 (length xs)+  pure (nthOf xs idx)+++simpleOptions :: Options+simpleOptions = aesonPrefix camelCase
+ src/Network/HStratus/Http.hs view
@@ -0,0 +1,97 @@+{- |+Module      : Network.HStratus.Http+Copyright   : (c) 2025 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++High-level HTTP client for the iCloud authentication API.++= Typical usage++Create an 'Api' handle with 'mkApi', choosing the 'Realm' that matches the+user's region.  Then call 'login' to run the full sign-in flow: SRP credential+exchange followed by any required two-factor ('completeTwoFactor') or two-step+('complete2SA') challenge, then the account-login request.  On success 'login'+returns 'Authenticated' carrying the refreshed 'Session' and 'AccountData'.++= Injectable alternatives++'login' resolves 2FA and 2SA challenges interactively using the prompts from+"Network.HStratus.Trust".  Pass your own code-reader and device-selector to+'loginWith' to bypass the interactive prompts — useful in tests or automation.++If you already hold a 'Requires2FA' or 'Requires2SA' result from a prior call,+resume the flow with 'completeTwoFactor' \/ 'completeTwoFactorWith' or+'complete2SA' \/ 'complete2SAWith'.+-}+module Network.HStratus.Http+  ( -- * API handle+    mkApi+  , mkApiWith++    -- * Login+  , login+  , loginWith++    -- * Fetching two-factor options+  , fetchTrustData++    -- * SMS phone code+  , requestSmsCode+  , verifySmsCode++    -- * Completing two-factor challenges+  , completeTwoFactor+  , completeTwoFactorWith++    -- * Completing two-step challenges+  , complete2SA+  , complete2SAWith++    -- * Types+  , Api+  , AuthState (..)+  , ApiLogger++    -- * Authenticated HTTP+  , rawRequest++    -- * Logging+  , withLogger+  , fileLogger+  , verboseLogger+  , redactingLogger++    -- * Errors+  , AuthError (..)+  , HStratusError+  )+where++import Network.HStratus.Internal.Http.Api+  ( Api+  , ApiLogger+  , fileLogger+  , mkApi+  , mkApiWith+  , rawRequest+  , redactingLogger+  , verboseLogger+  , withLogger+  )+import Network.HStratus.Internal.Http.Login+  ( AuthState (..)+  , complete2SA+  , complete2SAWith+  , completeTwoFactor+  , completeTwoFactorWith+  , login+  , loginWith+  )+import Network.HStratus.Internal.Http.Signin+  ( fetchTrustData+  , requestSmsCode+  , verifySmsCode+  )+import Network.HStratus.Internal.HttpErrors (AuthError (..), HStratusError)+
+ src/Network/HStratus/Http/Common.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : Network.HStratus.Http.Common+Copyright   : (c) 2025 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Shared HTTP utilities for iCloud service clients.++Re-exported from "Network.HStratus.Internal.Endpoints" for use by downstream+libraries (@hstratus-drive@, @hstratus-notes@, etc.) that need to build+authenticated service requests.+-}+module Network.HStratus.Http.Common+  ( -- * Headers+    icloudHome+  , icloudBrowserHeaders+  , withHeaders++    -- * Request helpers+  , stripTrailingSlash+  , lookupWebservice+  )+where++import Network.HStratus.Internal.Endpoints+  ( icloudBrowserHeaders+  , icloudHome+  , lookupWebservice+  , stripTrailingSlash+  , withHeaders+  )+
+ src/Network/HStratus/Http/Endpoints.hs view
@@ -0,0 +1,42 @@+{- |+Module      : Network.HStratus.Http.Endpoints+Copyright   : (c) 2025 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Base URLs for the iCloud HTTP API, selected by region.++iCloud operates two endpoint sets depending on the user's region:++* 'Usual' — targets @icloud.com@; for users outside mainland China.+* 'China' — targets @icloud.com.cn@; required for mainland China accounts.++Use 'realmEndpoints' to obtain the 'Endpoints' for the appropriate 'Realm',+then pass the result to 'Network.HStratus.Http.mkApi' or supply it directly to+'Network.HStratus.Http.mkApiWith'.+-}+module Network.HStratus.Http.Endpoints+  ( -- * Region selection++    {- | The two regional iCloud endpoint sets.++    * 'Usual' — @icloud.com@ family; for users outside mainland China.+    * 'China' — @icloud.com.cn@ family; required for mainland China accounts.+    -}+    Realm (..)+    -- | Return the 'Endpoints' for the given 'Realm'.+  , realmEndpoints++    -- * Endpoint bundle++    {- | Base URLs and default request templates for the iCloud HTTP API.++    Passed to 'Network.HStratus.Http.mkApi' or 'Network.HStratus.Http.mkApiWith'+    to construct properly-targeted API calls.+    -}+  , Endpoints (..)+  )+where++import Network.HStratus.Internal.Endpoints (Endpoints (..), Realm (..), realmEndpoints)+
+ src/Network/HStratus/Internal/Http/Api.hs view
@@ -0,0 +1,458 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_HADDOCK prune #-}++module Network.HStratus.Internal.Http.Api+  ( -- * API handle+    ApiLogger (..)+  , Api (..)+  , mkApi+  , mkApiWith+  , withLogger+  , fileLogger+  , verboseLogger+  , redactingLogger++    -- * Authenticated HTTP+  , rawRequest+  , rawRequest'+  , callApi+  , asJson+  , extractOr'+  , showStatusOf++    -- * Header helpers+  , authHeaders+  , requiredHeaders+  , callRequiredHeaders++    -- * Request builders+  , maybeValue+  , asObject+  , mkJsonRequest+  , withJsonRequestHeaders+  , callHandlingResponse++    -- * Constants+  , hClientId++    -- * Types+  , AuthCode+  )+where++import Control.Exception (throwIO)+import Control.Monad (unless, when)+import qualified Crypto.Hash.SHA256 as SHA256+import Crypto.SRP+  ( KnownAlgorithm (SHA256)+  , PrimeGroup (G2048)+  , digestSize+  )+import Data.Aeson+  ( FromJSON (..)+  , Key+  , eitherDecode+  , encode+  )+import Data.Aeson.KeyMap (fromList)+import Data.Aeson.Types (Value (..))+import Data.ByteString (ByteString, isPrefixOf)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LBS+import Data.CaseInsensitive (mk, original)+import Data.Maybe (catMaybes)+import qualified Data.Set as Set+import Data.String.Conv (toS)+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Time (getCurrentTime)+import Network.HStratus.Http.Endpoints (Endpoints (..), Realm, realmEndpoints)+import Network.HStratus.Internal.Endpoints+  ( homeHeaders+  , withAcceptJson+  , withAppleOauthHeaders+  , withBody+  , withHeaders+  , withWidgetKey+  )+import Network.HStratus.Internal.Http+  ( hCounter+  , hSessionId+  , needsRetry+  )+import Network.HStratus.Internal.HttpErrors+  ( ApiResponse+  , AuthError (..)+  , extractOr+  )+import Network.HStratus.Internal.PBKDF2 (FancyPseudoRandomF, wrapIO)+import Network.HStratus.Internal.Session+  ( SavedHeaders (..)+  , cookiePath+  , loadSavedHeaders+  , updateSavedHeaders+  , updateSessionSavedHeaders+  )+import Network.HStratus.Session (Session (..))+import qualified Network.HStratus.Session as Session+import Network.HTTP.Client+  ( Manager+  , Request (..)+  , RequestBody (..)+  , Response (..)+  , httpLbs+  )+import Network.HTTP.Client.TLS (newTlsManager)+import Network.HTTP.Types+  ( HeaderName+  , RequestHeaders+  , Status (..)+  , hAccept+  , hContentType+  )+import System.Directory (doesFileExist)+import System.IO (Handle, hPutStrLn)+import System.Posix.Files (setFileMode)+import Web.Cookie.Jar (usingCookiesFromFile)+++-- | A hook called after every HTTP response; receives the outgoing 'Request' and the 'Response'.+newtype ApiLogger = ApiLogger (Request -> Response LBS.ByteString -> IO ())+++{- | Bundles the HTTP manager, session state, and endpoint configuration+needed to call the iCloud API.  Created by 'mkApi' or 'mkApiWith'.+-}+data Api = Api+  { apiManager :: !Manager+  -- ^ shared TLS manager used for all requests+  , apiSession :: !Session+  -- ^ credentials and on-disk session paths+  , apiEndpoints :: !Endpoints+  -- ^ iCloud service URLs and the widget key+  , apiHashAlgorithm :: !KnownAlgorithm+  -- ^ hash algorithm used for SRP (always SHA-256 in practice)+  , apiWrappedPseudoRF :: !FancyPseudoRandomF+  -- ^ PBKDF2 pseudo-random function, pre-wrapped with the hash algorithm+  , apiGroup :: !PrimeGroup+  -- ^ SRP prime group (always G2048 in practice)+  , apiLogger :: !(Maybe ApiLogger)+  -- ^ optional logger invoked after every HTTP response+  }+++{- | Create an 'Api' using the endpoint set for the given 'Realm'.++Loads credentials and session state from disk via 'Network.HStratus.Session.loadSession'+and creates a new TLS manager.  Use 'mkApiWith' to supply a pre-built manager+and endpoint set — for example in tests.+-}+mkApi :: Realm -> IO Api+mkApi realm = do+  let apiHashAlgorithm = SHA256+      apiGroup = G2048+      apiEndpoints = realmEndpoints realm+      apiLogger = Nothing+  apiManager <- newTlsManager+  apiSession <- Session.loadSession+  apiWrappedPseudoRF <- wrapIO SHA256.hmac $ digestSize apiHashAlgorithm+  pure+    Api+      { apiGroup+      , apiEndpoints+      , apiManager+      , apiHashAlgorithm+      , apiSession+      , apiWrappedPseudoRF+      , apiLogger+      }+++-- | Create an 'Api' from a pre-built 'Session', 'Endpoints', and 'Manager'. Use this when you need to supply your own HTTP manager or a custom endpoint set — for example in tests.+mkApiWith :: Session -> Endpoints -> Manager -> IO Api+mkApiWith apiSession apiEndpoints apiManager = do+  let apiHashAlgorithm = SHA256+      apiGroup = G2048+      apiLogger = Nothing+  apiWrappedPseudoRF <- wrapIO SHA256.hmac $ digestSize apiHashAlgorithm+  pure+    Api+      { apiGroup+      , apiEndpoints+      , apiManager+      , apiHashAlgorithm+      , apiSession+      , apiWrappedPseudoRF+      , apiLogger+      }+++-- | Attach a logger to an 'Api'; it is called after every HTTP response.+withLogger :: ApiLogger -> Api -> Api+withLogger logger api = api{apiLogger = Just logger}+++{- | Build an 'ApiLogger' that appends one entry per response to a 'Handle'.++Each entry contains:++* a summary line: @TIMESTAMP METHOD URL STATUS@+* one response header per line: @Name: value@+* the raw response body+* a @---@ separator++__Security warning:__ all request and response headers are written verbatim,+including @Set-Cookie@, @X-Apple-Session-Token@, @X-Apple-TwoSV-Trust-Token@,+and @scnt@. Log files produced by this logger may contain live session tokens.+Use 'redactingLogger' when the log destination is not fully trusted.++Not safe for concurrent use from multiple threads against the same handle.+-}+fileLogger :: Handle -> ApiLogger+fileLogger h = ApiLogger $ \req resp -> do+  now <- getCurrentTime+  let scheme = if secure req then "https" else "http" :: String+      uri = scheme <> "://" <> toS (host req) <> toS (path req)+      status = statusCode (responseStatus resp)+      summary = show now <> " " <> toS (method req) <> " " <> uri <> " " <> show status+      fmtHdr (name, val) = toS (original name) <> ": " <> toS val+  hPutStrLn h summary+  mapM_ (hPutStrLn h . fmtHdr) (requestHeaders req)+  hPutStrLn h ""+  mapM_ (hPutStrLn h . fmtHdr) (responseHeaders resp)+  hPutStrLn h ""+  LBS.hPutStr h (responseBody resp)+  hPutStrLn h "\n---"+++{- | Like 'fileLogger' but also logs the query string in the URL and the+request body when present.++__Security warning:__ carries the same token-exposure risk as 'fileLogger' and+additionally logs request bodies, which may contain passwords or SRP parameters.+Use 'redactingLogger' when the log destination is not fully trusted.+-}+verboseLogger :: Handle -> ApiLogger+verboseLogger h = ApiLogger $ \req resp -> do+  now <- getCurrentTime+  let scheme = if secure req then "https" else "http" :: String+      qs = if BS.null (queryString req) then "" else "?" <> toS (queryString req)+      uri = scheme <> "://" <> toS (host req) <> toS (path req) <> qs+      status = statusCode (responseStatus resp)+      summary = show now <> " " <> toS (method req) <> " " <> uri <> " " <> show status+      fmtHdr (name, val) = toS (original name) <> ": " <> toS val+  hPutStrLn h summary+  mapM_ (hPutStrLn h . fmtHdr) (requestHeaders req)+  logReqBody (requestBody req)+  hPutStrLn h ""+  mapM_ (hPutStrLn h . fmtHdr) (responseHeaders resp)+  hPutStrLn h ""+  LBS.hPutStr h (responseBody resp)+  hPutStrLn h "\n---"+ where+  logReqBody (RequestBodyLBS lbs)+    | not (LBS.null lbs) = hPutStrLn h "" >> LBS.hPutStr h lbs+  logReqBody (RequestBodyBS bs)+    | not (BS.null bs) = hPutStrLn h "" >> LBS.hPutStr h (LBS.fromStrict bs)+  logReqBody _ = pure ()+++{- | Like 'fileLogger' but replaces the values of sensitive headers with+@\<redacted\>@ before writing.++The following headers are redacted in both request and response:+@Set-Cookie@, @Cookie@, @X-Apple-Session-Token@, @X-Apple-TwoSV-Trust-Token@,+@scnt@, @Authorization@.++Safe to write to shared or untrusted log destinations.+-}+redactingLogger :: Handle -> ApiLogger+redactingLogger h = ApiLogger $ \req resp -> do+  now <- getCurrentTime+  let scheme = if secure req then "https" else "http" :: String+      uri = scheme <> "://" <> toS (host req) <> toS (path req)+      status = statusCode (responseStatus resp)+      summary = show now <> " " <> toS (method req) <> " " <> uri <> " " <> show status+      fmtHdr (name, val) = toS (original name) <> ": " <> toS val+  hPutStrLn h summary+  mapM_ (hPutStrLn h . fmtHdr . redactHeader) (requestHeaders req)+  hPutStrLn h ""+  mapM_ (hPutStrLn h . fmtHdr . redactHeader) (responseHeaders resp)+  hPutStrLn h ""+  LBS.hPutStr h (responseBody resp)+  hPutStrLn h "\n---"+++sensitiveHeaderNames :: Set.Set HeaderName+sensitiveHeaderNames =+  Set.fromList+    [ mk "Set-Cookie"+    , mk "Cookie"+    , mk "X-Apple-Session-Token"+    , mk "X-Apple-TwoSV-Trust-Token"+    , mk "scnt"+    , mk "Authorization"+    ]+++redactHeader :: (HeaderName, BS.ByteString) -> (HeaderName, BS.ByteString)+redactHeader (name, val)+  | name `Set.member` sensitiveHeaderNames = (name, "<redacted>")+  | otherwise = (name, val)+++-- | Make a session request and obtain the raw byte results+rawRequest :: Api -> Request -> IO (Response LBS.ByteString)+rawRequest = rawRequest' True+++-- | Make a session request and obtain the raw byte results+rawRequest' :: Bool -> Api -> Request -> IO (Response LBS.ByteString)+rawRequest' mayRetry api req = do+  let Api{apiManager = mgr, apiSession = s, apiLogger = mbLogger} = api+      jarPath = cookiePath (sessionTopDir s) (sessionCreds s)+  resp <- usingCookiesFromFile jarPath req $ flip httpLbs mgr+  jarExists <- doesFileExist jarPath+  when jarExists $ setFileMode jarPath 0o600+  updateSessionSavedHeaders s $ updateSavedHeaders $ responseHeaders resp+  mapM_ (\(ApiLogger logFn) -> logFn req resp) mbLogger+  if mayRetry && needsRetry (statusCode (responseStatus resp))+    then rawRequest' False api req+    else pure resp+++{- | Make a session request to obtain a JSON payload++call api with request, obtain response+save the sessionData from the response headers+save any cookies from the response headers+if the response is JSON, parse it, and see if it parses as an ApiError+if JSON parsing fails, log to stderr+if it parses as an ApiError, indicate that++if the response is not JSON, use 'rawRequest' instead+-}+callApi+  :: (FromJSON a) => Api -> Request -> IO (Response (ApiResponse a))+callApi api req = do+  let isJsonType ct = "application/json" `isPrefixOf` ct || "text/json" `isPrefixOf` ct+  raw <- rawRequest api req+  let code = statusCode (responseStatus raw)+      theType = lookup hContentType $ responseHeaders raw+      isJson = maybe False isJsonType theType+  when (code >= 400 && LBS.null (responseBody raw)) $+    throwIO $+      UnexpectedResponse $+        showStatusOf raw+  unless (code >= 400 || isJson) $+    throwIO $+      UnexpectedResponse $+        "response was not JSON: " <> toS (show theType)+  mapM asJson raw+++-- confirm the content-type of the response before attempting to parse+-- if it's wrong, throw  InvalidContentType+-- try to parse, if that fails, throw WrongDataType+asJson :: (FromJSON a) => LBS.ByteString -> IO a+asJson resp = case eitherDecode resp of+  Left _err -> throwIO $ UnexpectedResponse "did not decode JSON response correctly"+  Right x -> pure x+++-- | Extract the successful value from a response, throwing 'UnexpectedResponse' for 4xx\/5xx status codes.+extractOr' :: Response (ApiResponse a) -> IO a+extractOr' r | statusCode (responseStatus r) >= 400 = throwIO $ UnexpectedResponse $ showStatusOf r+extractOr' r = extractOr $ responseBody r+++-- | Format the HTTP status of a response as a human-readable 'Text' string.+showStatusOf :: Response a -> Text+showStatusOf resp =+  let showResponse' x s | x >= 500 = "server error:" <> Text.pack (show s)+      showResponse' x s | x >= 400 = "bad request:" <> Text.pack (show s)+      showResponse' _x s = "ok:" <> Text.pack (show s)+      theStatus = responseStatus resp+      theCode = statusCode theStatus+   in showResponse' theCode theStatus+++-- | Build the full set of Apple OAuth request headers for an authenticated call.+authHeaders :: Api -> SavedHeaders -> RequestHeaders+authHeaders api savedHdrs =+  let Api{apiSession = session, apiEndpoints = ep} = api+      Session{sessionClientId = cid} = session+      headerOf name x = (name, toS x)+      maybeHeaderOf name = fmap (headerOf name)+      cidHeader = [(hClientId, toS cid)]+      sdHeaders =+        catMaybes+          [ maybeHeaderOf hCounter $ shCounter savedHdrs+          , maybeHeaderOf hSessionId $ shSessionId savedHdrs+          ]+   in withAppleOauthHeaders (epWidgetKey ep) $ homeHeaders ep <> sdHeaders <> cidHeader+++-- | Build the minimal request headers required by the iCloud API: the widget key, session counter, and session ID.+requiredHeaders :: ByteString -> SavedHeaders -> RequestHeaders+requiredHeaders key savedHdrs =+  let headerOf name x = (name, toS x)+      maybeHeaderOf name = fmap (headerOf name)+      sdHeaders =+        catMaybes+          [ maybeHeaderOf hCounter $ shCounter savedHdrs+          , maybeHeaderOf hSessionId $ shSessionId savedHdrs+          ]+   in withAcceptJson . withWidgetKey key $ sdHeaders+++-- | Make an authenticated API call with the required session headers, returning the decoded response body or throwing on error.+callRequiredHeaders :: (FromJSON a) => Api -> Request -> IO a+callRequiredHeaders api@Api{apiSession = s, apiEndpoints = ep} req = do+  savedHdrs <- loadSavedHeaders s+  callApi api (withHeaders (requiredHeaders (epWidgetKey ep) savedHdrs) req) >>= extractOr'+++-- | Convert a @Maybe@ to a JSON 'Value', using 'Null' for 'Nothing'.+maybeValue :: (a -> Value) -> Maybe a -> Value+maybeValue = maybe Null+++-- | Build a JSON 'Object' 'Value' from a list of key-value pairs.+asObject :: [(Key, Value)] -> Value+asObject = Object . fromList+++-- | Build a JSON request by combining a base-request builder, a body encoder, and their respective inputs.+mkJsonRequest :: (a -> Request) -> (b -> Value) -> a -> b -> Request+mkJsonRequest mkBase mkBody baseSrc bodySrc =+  withJsonRequestHeaders . withBody (encode $ mkBody bodySrc) $ mkBase baseSrc+++-- | Set @Accept: application/json@ and @Content-Type: application/json@ on a request.+withJsonRequestHeaders :: Request -> Request+withJsonRequestHeaders = withHeaders [(hAccept, "application/json"), (hContentType, "application/json")]+++-- | Build and execute an authenticated API call, applying an optional request modifier, and decode the response.+callHandlingResponse+  :: (FromJSON a)+  => (Endpoints -> b -> Request)+  -> (Request -> Request)+  -> Api+  -> b+  -> IO a+callHandlingResponse mkReq modReq api@Api{apiEndpoints} x =+  callApi api (modReq $ mkReq apiEndpoints x) >>= extractOr'+++-- | @HeaderName@ used to represent API session data+hClientId :: HeaderName+hClientId = mk "X-Apple-OAuth-State"+++{- | The code sent to a user device that the user must enter to confirm+authenticity+-}+type AuthCode = Text
+ src/Network/HStratus/Internal/Http/Login.hs view
@@ -0,0 +1,336 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_HADDOCK prune #-}++module Network.HStratus.Internal.Http.Login+  ( -- * Login state+    AuthState (..)++    -- * Login+  , login+  , loginWith++    -- * Completing two-factor challenges+  , completeTwoFactor+  , completeTwoFactorWith++    -- * Completing two-step challenges+  , complete2SA+  , complete2SAWith+  )+where++import Control.Exception (IOException, catch, throwIO)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Trans.Reader (ReaderT, runReaderT)+import qualified Control.Monad.Trans.Reader as Reader+import Crypto.SRP (calcResults, mkFromClient)+import Data.Aeson (FromJSON (..))+import Data.Aeson.Types (Value (..), parseEither)+import Data.List.NonEmpty (NonEmpty)+import qualified Data.Text as Text+import Data.Word (Word8)+import Network.HStratus.Internal.Http (SrpContext (..))+import Network.HStratus.Internal.Http.Api+  ( Api (..)+  , AuthCode+  )+import Network.HStratus.Internal.Http.Signin+  ( accountLogin+  , doTrustStep+  , fetchTrustData+  , listSetupDevices+  , requestSmsCode+  , runSigninComplete+  , runSigninInit+  , sendSetupVerification+  , triggerTwoFaPush+  , validate+  , validateSetupVerification+  , verifySmsCode+  , verifyTwoFaCode+  )+import Network.HStratus.Internal.HttpErrors (AuthError (..))+import Network.HStratus.Internal.LoginFSM+  ( AfterAcctLogin (..)+  , AfterArtifactDir (..)+  , AfterCredentials (..)+  , AfterLoadLastSession (..)+  , AfterMkArtifactDir (..)+  , AfterSrpComplete (..)+  , AfterTwoFaVerify (..)+  , AfterTwoSaVerify (..)+  , AfterValidateSession (..)+  , CompletionOutcome (..)+  , LoginEvent (..)+  , LoginFSM (..)+  , LoginOutcome (..)+  , TwoFaConfig (..)+  , TwoSaConfig (..)+  , loginProcess+  , twoFaProcess+  , twoSaProcess+  )+import Network.HStratus.Internal.Session+  ( accountDataRequires2FA+  , accountDataRequires2SA+  , loadAccountData+  , loadSavedHeaders+  , pristine+  , saveAccountData+  , saveLoginMsg+  , unknownAccountData+  )+import Network.HStratus.Internal.Trust+  ( CodeStatus (..)+  , TrustData (..)+  , TrustedPhone+  , pleaseReadCode+  , selectSetupDevice+  , selectTwoFaPhone+  )+import Network.HStratus.Session (AccountData (..), Credentials (..), Session (..))+import Network.HStratus.Trust (Setup2SADevice (..))+import System.Directory (createDirectoryIfMissing, doesDirectoryExist)+++newtype LoginM a = LoginM {runLoginM :: ReaderT Api IO a}+  deriving (Functor, Applicative, Monad, MonadIO)+++ask :: LoginM Api+ask = LoginM Reader.ask+++asks :: (Api -> b) -> LoginM b+asks = LoginM . Reader.asks+++{- | The result of a login attempt.++'login' and 'loginWith' return only 'Authenticated'; 2FA and 2SA challenges+are resolved internally.  'Requires2FA' and 'Requires2SA' are only produced by+'completeTwoFactor', 'completeTwoFactorWith', 'complete2SA', and 'complete2SAWith'.+-}+data AuthState+  = -- | Sign-in succeeded; the 'Session' is refreshed and 'AccountData' is available.+    Authenticated Session AccountData+  | -- | Sign-in requires a two-factor code; use 'completeTwoFactor' or 'completeTwoFactorWith' to proceed.+    Requires2FA Session+  | -- | Sign-in requires a legacy two-step code; use 'complete2SA' or 'complete2SAWith' to proceed.+    Requires2SA Session (NonEmpty Setup2SADevice)+++instance Show AuthState where+  show (Authenticated _ ad) = "Authenticated <session> " ++ show ad+  show (Requires2FA _) = "Requires2FA <session>"+  show (Requires2SA _ ds) = "Requires2SA <session> " ++ show ds+++-- | Logs into ICloud, completing any 2FA or 2SA challenge automatically+login :: Api -> IO AuthState+login = loginWith pleaseReadCode selectTwoFaPhone selectSetupDevice+++-- | Like 'login' with injectable code prompt, phone selector, and device selector, for testing+loginWith+  :: (Word8 -> IO AuthCode)+  -> (TrustData -> IO (Maybe TrustedPhone))+  -> (NonEmpty Setup2SADevice -> IO Setup2SADevice)+  -> Api+  -> IO AuthState+loginWith readCode pickPhone pickDevice api =+  runReaderT (runLoginM loginProcess) api >>= \case+    LoginAuthenticated (AuthComplete _ ad) -> pure $ Authenticated (apiSession api) ad+    LoginNeedsTwoFa (NeedsTwoFa _) -> completeTwoFactorWith readCode pickPhone api+    LoginNeedsTwoSa (TwoSaReady _ ds) -> complete2SAWith pickDevice (readCode 6) api ds+    LoginHaltCreds _ -> throwIO CredentialsMissing+    LoginHaltDir _ -> throwIO $ ArtifactDirCreationFailed (sessionTopDir (apiSession api))+    LoginHaltSrp _ -> throwIO SrpProtocolError+    LoginHaltTwoFaLocked _ -> throwIO TwoFactorLocked+++instance LoginEvent LoginM where+  type State LoginM = LoginFSM+++  initial = pure RatifyCredentials+++  ratifyCreds RatifyCredentials =+    asks (GotCreds . RatifyArtifactDir . sessionCreds . apiSession)+++  ratifyArtifactDir (RatifyArtifactDir creds) = do+    dir <- sessionTopDir . apiSession <$> ask+    exists <- liftIO $ doesDirectoryExist dir+    pure $+      if exists+        then DirPresent $ LoadLastSession creds+        else DirAbsent $ MkArtifactDir creds+++  mkArtifactDir (MkArtifactDir creds) = do+    dir <- sessionTopDir . apiSession <$> ask+    ok <- liftIO $ (createDirectoryIfMissing True dir >> pure True) `catch` (\(_ :: IOException) -> pure False)+    pure $+      if ok+        then DirMade $ LoadLastSession creds+        else NotMade $ HaltCannotMkArtifactDir creds+++  loadSession (LoadLastSession creds) = do+    savedHdrs <- ask >>= liftIO . loadSavedHeaders . apiSession+    pure $+      if savedHdrs == pristine+        then HasClientId $ ReadyToAuth creds savedHdrs+        else HasPriorSession $ HasSavedSession creds savedHdrs+++  validateSession (HasSavedSession creds savedHdrs) = do+    valid <- ask >>= liftIO . validate+    if not valid+      then pure $ SessionStale $ ReadyToAuth creds savedHdrs+      else do+        mbAd <- ask >>= liftIO . loadAccountData . apiSession+        pure $ case mbAd of+          Just ad | accountDataRequires2FA ad -> SessionStale $ ReadyToAuth creds savedHdrs+          Just ad | accountDataRequires2SA ad -> SessionStale $ ReadyToAuth creds savedHdrs+          Just ad -> SessionStillValid $ AuthComplete creds ad+          Nothing -> SessionStillValid $ AuthComplete creds unknownAccountData+++  srpInit (ReadyToAuth creds _) = do+    api <- ask+    let user = credAccountName creds+        pass = credPassword creds+    fc <- liftIO $ mkFromClient user pass (apiGroup api)+    (fs, kd) <- liftIO $ runSigninInit api fc+    pure $ SrpInitDone creds (SrpContext fc fs kd)+++  srpComplete (SrpInitDone creds ctx) = do+    api <- ask+    let SrpContext{srpFromClient = fc, srpFromServer = fs, srpKeyDeriver = kd} = ctx+    case calcResults kd fc fs of+      Nothing -> pure $ SrpCompleteInvalidKey $ HaltInvalidSrp creds+      Just results -> do+        liftIO $ runSigninComplete api kd results+        pure $ SrpCompleteOk $ DoAccountLogin creds+++  acctLogin (DoAccountLogin creds) = do+    api <- ask+    loginReply <- liftIO $ accountLogin api+    ad <- liftIO $ parseAccountData loginReply+    liftIO $ saveLoginMsg (apiSession api) loginReply+    liftIO $ saveAccountData (apiSession api) ad+    pure $+      if+        | accountDataRequires2SA ad -> AcctLogin2SA $ NeedsTwoSa creds+        | accountDataRequires2FA ad -> AcctLogin2FA $ NeedsTwoFa creds+        | otherwise -> AcctLoginOk $ AuthComplete creds ad+++  listTwoSaDevices (NeedsTwoSa creds) = do+    ask >>= fmap (TwoSaReady creds) . liftIO . listSetupDevices+++  beginTwoFa (ReadyForTwoFa creds td) TwoFaConfig{tfcPickPhone} = do+    mbPhone <- liftIO $ tfcPickPhone td+    case mbPhone of+      Nothing -> ask >>= liftIO . triggerTwoFaPush+      Just phone -> ask >>= liftIO . flip requestSmsCode phone+    pure $ TwoFaVerifying creds td mbPhone+++  verifyTwoFa (TwoFaVerifying creds td mbPhone) TwoFaConfig{tfcReadCode} = do+    api <- ask+    code <- liftIO $ tfcReadCode (scLength (tdSecurityCode td))+    ok <- liftIO $ case mbPhone of+      Nothing -> verifyTwoFaCode api code+      Just phone -> verifySmsCode api phone code+    if ok+      then pure $ TwoFaOk $ DoTrust creds+      else do+        freshTd <- liftIO $ fetchTrustData api+        let cs = tdSecurityCode freshTd+        pure $+          if scTooManyCodesValidated cs || scSecurityCodeLocked cs || scSecurityCodeCooldown cs+            then TwoFaLocked $ HaltTwoFaLocked creds+            else TwoFaRetry $ ReadyForTwoFa creds freshTd+++  doTrust (DoTrust creds) = do+    ask >>= liftIO . doTrustStep+    pure $ DoAccountLogin creds+++  beginTwoSa (ReadyForTwoSa creds devices) TwoSaConfig{tscPickDevice} = do+    api <- ask+    device <- liftIO $ tscPickDevice devices+    liftIO $ sendSetupVerification api device+    pure $ TwoSaVerifying creds device devices+++  verifyTwoSa (TwoSaVerifying creds device devices) TwoSaConfig{tscReadCode} = do+    api <- ask+    code <- liftIO tscReadCode+    ok <- liftIO $ validateSetupVerification api device code+    pure $+      if ok+        then TwoSaOk $ DoAccountLogin creds+        else TwoSaRetry $ ReadyForTwoSa creds devices+++parseAccountData :: Value -> IO AccountData+parseAccountData v =+  either (throwIO . UnexpectedResponse . Text.pack) pure $+    parseEither parseJSON v+++-- | Complete a pending 2FA (auth-endpoint) challenge+completeTwoFactor :: Api -> IO AuthState+completeTwoFactor = completeTwoFactorWith pleaseReadCode (\_ -> pure Nothing)+++-- | Like 'completeTwoFactor' with an injectable code prompt and phone selector, for testing+completeTwoFactorWith :: (Word8 -> IO AuthCode) -> (TrustData -> IO (Maybe TrustedPhone)) -> Api -> IO AuthState+completeTwoFactorWith readCode pickPhone api = do+  td <- fetchTrustData api+  let start = ReadyForTwoFa (sessionCreds (apiSession api)) td+      cfg = TwoFaConfig{tfcPickPhone = pickPhone, tfcReadCode = readCode}+  runReaderT (runLoginM (twoFaProcess start cfg)) api >>= \case+    CompletionAuthenticated (AuthComplete _ ad) -> pure $ Authenticated (apiSession api) ad+    CompletionNeedsTwoFa _ -> throwIO TwoFactorStillRequired+    CompletionNeedsTwoSa (TwoSaReady _ ds) -> pure $ Requires2SA (apiSession api) ds+    CompletionTwoFaLocked _ -> throwIO TwoFactorLocked+++-- | Used when already holding a 'Requires2SA' result from 'completeTwoFactor' or 'completeTwoFactorWith'+complete2SA :: Api -> NonEmpty Setup2SADevice -> IO AuthState+complete2SA = complete2SAWith selectSetupDevice (pleaseReadCode 6)+++-- | Like 'complete2SA' with injectable device selector and code prompt, for testing+complete2SAWith+  :: (NonEmpty Setup2SADevice -> IO Setup2SADevice)+  -> IO AuthCode+  -> Api+  -> NonEmpty Setup2SADevice+  -> IO AuthState+complete2SAWith pickDevice readCode api devices = do+  let start = ReadyForTwoSa (sessionCreds (apiSession api)) devices+      cfg = TwoSaConfig{tscPickDevice = pickDevice, tscReadCode = readCode}+  runReaderT (runLoginM (twoSaProcess start cfg)) api >>= \case+    CompletionAuthenticated (AuthComplete _ ad) -> pure $ Authenticated (apiSession api) ad+    CompletionNeedsTwoFa _ -> throwIO TwoFactorStillRequired+    CompletionNeedsTwoSa (TwoSaReady _ ds) -> pure $ Requires2SA (apiSession api) ds+    CompletionTwoFaLocked _ -> throwIO TwoFactorLocked
+ src/Network/HStratus/Internal/Http/Signin.hs view
@@ -0,0 +1,420 @@+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# OPTIONS_HADDOCK prune #-}++{- |+Module      : Network.HStratus.Internal.Http.Signin+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Implements the iCloud two-factor sign-in flow: fetching trust data, requesting and verifying SMS codes, and completing sign-in.+-}+module Network.HStratus.Internal.Http.Signin+  ( -- * Trust data+    fetchTrustData++    -- * SMS code+  , requestSmsCode+  , verifySmsCode++    -- * Session validation+  , validate++    -- * SRP sign-in+  , runSigninInit+  , runSigninComplete++    -- * Account login+  , accountLogin++    -- * 2FA helpers+  , triggerTwoFaPush+  , doTrustStep+  , verifyTwoFaCode++    -- * 2SA helpers+  , listSetupDevices+  , sendSetupVerification+  , validateSetupVerification+  )+where++import Control.Exception (IOException, catch, throwIO)+import Control.Monad (unless, void)+import Crypto.SRP+  ( FromClient (..)+  , FromServer (..)+  , Results (..)+  )+import Data.Aeson+  ( FromJSON (..)+  , Object+  , encode+  , withObject+  , (.:)+  )+import Data.Aeson.Types (Parser, Value (..))+import Data.ByteString (ByteString)+import qualified Data.ByteString.Base64 as B64+import Data.List.NonEmpty (NonEmpty)+import qualified Data.List.NonEmpty as NE+import Data.Maybe (fromMaybe)+import Data.String.Conv (toS)+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Data.Word (Word64)+import Network.HStratus.Http.Endpoints (Endpoints (..))+import Network.HStratus.Internal.Endpoints+  ( accountLoginBase+  , extendPath+  , homeHeaders+  , listDevices+  , sendVerification+  , signinCompleteBase+  , signinInitBase+  , toPut+  , twoFaOptionsBase+  , twoSvTrust+  , validateBase+  , validateVerification+  , verifySecurityCodeReq+  , withAcceptJson+  , withBody+  , withHeaders+  )+import Network.HStratus.Internal.Http+  ( KeyDeriver (..)+  , PasswordProtocol (..)+  , phoneCodeBody+  , phoneTriggerBody+  , validateSetupBody+  )+import Network.HStratus.Internal.Http.Api+  ( Api (..)+  , AuthCode+  , asObject+  , authHeaders+  , callApi+  , callHandlingResponse+  , callRequiredHeaders+  , extractOr'+  , maybeValue+  , mkJsonRequest+  , rawRequest+  , requiredHeaders+  , showStatusOf+  , withJsonRequestHeaders+  )+import Network.HStratus.Internal.HttpErrors+  ( ApiResponse+  , AuthError (..)+  )+import Network.HStratus.Internal.Session+  ( SavedHeaders (..)+  , loadSavedHeaders+  )+import Network.HStratus.Internal.Trust (TrustData (..), TrustedPhone)+import Network.HStratus.Session (Credentials (..), Session (..))+import Network.HStratus.Trust (Setup2SADevice (..))+import Network.HTTP.Client (Request (..), Response (..))+import Network.HTTP.Types (Status (..))+++-- | Fetch the 2FA options immediately after the 409 from signin/complete+fetchTrustData :: Api -> IO TrustData+fetchTrustData api = do+  savedHdrs <- loadSavedHeaders (apiSession api)+  let req = withHeaders (withAcceptJson $ authHeaders api savedHdrs) (twoFaOptionsBase (apiEndpoints api))+  callApi api req >>= extractOr'+++-- | POST to phone/securitycode to request an SMS code to the given phone+requestSmsCode :: Api -> TrustedPhone -> IO ()+requestSmsCode api@Api{apiEndpoints = ep} tp = do+  savedHdrs <- loadSavedHeaders (apiSession api)+  let req =+        withHeaders (authHeaders api savedHdrs) $+          withJsonRequestHeaders $+            withBody (encode $ phoneTriggerBody tp) $+              toPut (extendPath (epAuth ep) "/verify/phone")+  resp <- rawRequest api req+  unless (statusCode (responseStatus resp) < 400) $+    throwIO $+      UnexpectedResponse $+        showStatusOf resp+++-- | POST to phone/securitycode to verify an SMS code; returns True when accepted+verifySmsCode :: Api -> TrustedPhone -> AuthCode -> IO Bool+verifySmsCode api@Api{apiEndpoints = ep} tp code = do+  savedHdrs <- loadSavedHeaders (apiSession api)+  let req =+        withHeaders (authHeaders api savedHdrs) $+          withJsonRequestHeaders $+            withBody (encode $ phoneCodeBody tp code) $+              verifySecurityCodeReq "phone" ep+  resp <- rawRequest api req+  let c = statusCode (responseStatus resp)+  if+    | c < 400 -> pure True+    | c == 400 -> pure False+    | otherwise -> throwIO $ UnexpectedResponse $ showStatusOf resp+++data SigninInitReply = SigninInitReply+  { sirTag :: !Text+  , sirProtocol :: !PasswordProtocol+  , sirPublicBytes :: !ByteString+  , sirIterations :: !Word64+  , sirSalt :: !ByteString+  }+  deriving (Eq, Show)+++instance FromJSON SigninInitReply where+  parseJSON = withObject "SigninInitReply" parseSigninInitReply+++parseSigninInitReply :: Object -> Parser SigninInitReply+parseSigninInitReply o =+  let tag = o .: "c"+      iterations = o .: "iteration"+      protocol = o .: "protocol"+      publicBytes = o .: "b" >>= parseBase64Bytes+      salt = o .: "salt" >>= parseBase64Bytes+      parseBase64Bytes s = case B64.decode (encodeUtf8 s) of+        Left err -> fail err+        Right b -> pure b+   in SigninInitReply+        <$> tag+        <*> protocol+        <*> publicBytes+        <*> iterations+        <*> salt+++signinInit :: Api -> FromClient -> IO SigninInitReply+signinInit api other = do+  savedHdrs <- loadSavedHeaders (apiSession api)+  callHandlingResponse signinInitReq (withHeaders (authHeaders api savedHdrs)) api other+++-- | Execute the SRP init step, returning the server's public values and a 'KeyDeriver' for the completion step.+runSigninInit :: Api -> FromClient -> IO (FromServer, KeyDeriver)+runSigninInit api fc = do+  r <- signinInit api fc+  let fromServer =+        FromServer+          { fsPublicBytes = sirPublicBytes r+          , fsSalt = sirSalt r+          , fsPrimeGroup = apiGroup api+          , fsKnownAlgorithm = apiHashAlgorithm api+          }+      keyDeriver =+        KeyDeriver+          { kdTag = sirTag r+          , kdIterations = sirIterations r+          , kdWrappedF = apiWrappedPseudoRF api+          , kdProtocol = sirProtocol r+          }+  pure (fromServer, keyDeriver)+++signinInitReq :: Endpoints -> FromClient -> Request+signinInitReq = mkJsonRequest signinInitBase signinInitValue+++signinInitValue :: FromClient -> Value+signinInitValue fc =+  let a = decodeUtf8 $ B64.encode $ fcPublicBytes fc+   in asObject+        [ ("a", String a)+        , ("accountName", String (fcUser fc))+        , ("protocols", Array ["s2k", "s2k_fo"])+        ]+++data SigninCompletion = SigninCompletion+  { siTag :: !Text+  , siAccountName :: !Text+  , siSavedHeaders :: !SavedHeaders+  , siResults :: !Results+  }+++-- | Execute the SRP completion step, sending the client and server proofs to Apple.+runSigninComplete :: Api -> KeyDeriver -> Results -> IO ()+runSigninComplete api@Api{apiSession = session} kd results = do+  siSavedHeaders <- loadSavedHeaders session+  let siAccountName = credAccountName $ sessionCreds session+      completion =+        SigninCompletion+          { siTag = kdTag kd+          , siAccountName+          , siResults = results+          , siSavedHeaders+          }+  signinComplete api completion+++signinComplete :: Api -> SigninCompletion -> IO ()+signinComplete api sc = do+  savedHdrs <- loadSavedHeaders (apiSession api)+  let req = withHeaders (authHeaders api savedHdrs) $ signinCompleteReq (apiEndpoints api) sc+  resp <- callApi api req :: IO (Response (ApiResponse ()))+  handleSigninComplete resp+++signinCompleteReq :: Endpoints -> SigninCompletion -> Request+signinCompleteReq = mkJsonRequest signinCompleteBase signinCompleteValue+++signinCompleteValue :: SigninCompletion -> Value+signinCompleteValue sc =+  let Results{rClientProof, rServerProof} = siResults sc+      toBase64 = decodeUtf8 . B64.encode+      singleElem x = Array [String x]+      maybeArray = maybe (Array []) singleElem+   in asObject+        [ ("m1", String (toBase64 rClientProof))+        , ("m2", String (toBase64 rServerProof))+        , ("trustTokens", maybeArray (shTrustToken (siSavedHeaders sc)))+        , ("rememberMe", Bool True)+        , ("accountName", String (siAccountName sc))+        , ("c", String (siTag sc))+        ]+++handleSigninComplete :: Response (ApiResponse ()) -> IO ()+handleSigninComplete resp = do+  let code = statusCode $ responseStatus resp+  if+    | code == 401 -> throwIO InvalidCredentials+    | code == 403 -> throwIO AccountLocked+    | code == 412 -> throwIO PrivacyAgreementRequired+    | code == 409 -> pure () -- 2FA required; accountLogin will detect it+    | code >= 400 -> throwIO $ UnexpectedResponse $ showStatusOf resp+    | otherwise -> pure ()+++-- | Check whether the current session is still valid; returns @False@ on 401, throws on other errors.+validate :: Api -> IO Bool+validate api@Api{apiEndpoints} = do+  resp <- rawRequest api (validateReq apiEndpoints)+  let code = statusCode (responseStatus resp)+  if+    | code == 401 -> pure False+    | code >= 400 -> throwIO $ UnexpectedResponse $ showStatusOf resp+    | otherwise -> pure True+++validateReq :: Endpoints -> Request+validateReq = withJsonRequestHeaders . withBody (encode Null) . validateBase+++-- | POST to the account-login endpoint and return the raw JSON response (used to obtain 'AccountData').+accountLogin :: Api -> IO Value+accountLogin api@Api{apiEndpoints = ep} = do+  savedHdrs <- loadSavedHeaders $ apiSession api+  let hdrs = homeHeaders ep+  callHandlingResponse accountLoginReq (withHeaders hdrs) api savedHdrs+++accountLoginReq :: Endpoints -> SavedHeaders -> Request+accountLoginReq = mkJsonRequest accountLoginBase accountLoginValue+++-- | Trigger a 2FA push notification to the user's trusted device.+triggerTwoFaPush :: Api -> IO ()+triggerTwoFaPush api@Api{apiEndpoints = ep} = do+  savedHdrs <- loadSavedHeaders (apiSession api)+  let req =+        withHeaders+          (withAcceptJson $ authHeaders api savedHdrs)+          (toPut (extendPath (epAuth ep) "/verify/trusteddevice/securitycode"))+  void (rawRequest api req) `catch` \(e :: IOException) ->+    throwIO (UnexpectedResponse ("2FA push failed: " <> toS (show e)))+++-- | POST to the two-step-verification trust endpoint to mark this session as trusted.+doTrustStep :: Api -> IO ()+doTrustStep api@Api{apiEndpoints = ep} = do+  savedHdrs <- loadSavedHeaders (apiSession api)+  let req = withHeaders (withAcceptJson $ authHeaders api savedHdrs) (twoSvTrust ep)+  resp <- rawRequest api req+  unless (statusCode (responseStatus resp) < 400) $+    throwIO $+      UnexpectedResponse $+        showStatusOf resp+++-- | Submit a 2FA code entered by the user; returns @True@ when accepted, @False@ on a 400 rejection.+verifyTwoFaCode :: Api -> AuthCode -> IO Bool+verifyTwoFaCode api@Api{apiEndpoints = ep} code = do+  savedHdrs <- loadSavedHeaders (apiSession api)+  let body = encode $ Object [("securityCode", Object [("code", String code)])]+      req =+        withHeaders (authHeaders api savedHdrs) $+          withJsonRequestHeaders $+            withBody body $+              verifySecurityCodeReq "trusteddevice" ep+  resp <- rawRequest api req+  let c = statusCode (responseStatus resp)+  if+    | c < 400 -> pure True+    | c == 400 -> pure False+    | otherwise -> throwIO $ UnexpectedResponse $ showStatusOf resp+++accountLoginValue :: SavedHeaders -> Value+accountLoginValue hs =+  asObject+    [ ("accountCountryCode", maybeValue String (shCountry hs))+    , ("dsWebAuthToken", maybeValue String (shSessionToken hs))+    , ("trustToken", String $ fromMaybe "" $ shTrustToken hs)+    , ("extended_login", Bool True)+    ]+++newtype ListDevicesReply = ListDevicesReply {ldrDevices :: [Setup2SADevice]}+++instance FromJSON ListDevicesReply where+  parseJSON = withObject "ListDevicesReply" $ \o -> ListDevicesReply <$> o .: "devices"+++-- | Fetch the list of trusted devices available for legacy 2SA verification; throws when the server returns none.+listSetupDevices :: Api -> IO (NonEmpty Setup2SADevice)+listSetupDevices api@Api{apiEndpoints = ep} = do+  devices <- ldrDevices <$> callRequiredHeaders api (listDevices ep)+  maybe (throwIO (UnexpectedResponse "2SA: server returned no trusted devices")) pure (NE.nonEmpty devices)+++-- | Send a 2SA verification code to the given device.+sendSetupVerification :: Api -> Setup2SADevice -> IO ()+sendSetupVerification api@Api{apiSession = s, apiEndpoints = ep} device = do+  savedHdrs <- loadSavedHeaders s+  let req =+        withHeaders (requiredHeaders (epWidgetKey ep) savedHdrs) $+          withJsonRequestHeaders $+            withBody (encode device) $+              sendVerification ep+  resp <- rawRequest api req+  unless (statusCode (responseStatus resp) < 400) $ throwIO $ UnexpectedResponse $ showStatusOf resp+++-- | Submit a 2SA verification code for the given device; returns @True@ when accepted.+validateSetupVerification :: Api -> Setup2SADevice -> AuthCode -> IO Bool+validateSetupVerification api@Api{apiSession = s, apiEndpoints = ep} device code = do+  savedHdrs <- loadSavedHeaders s+  let req =+        withHeaders (requiredHeaders (epWidgetKey ep) savedHdrs) $+          withJsonRequestHeaders $+            withBody (encode $ validateSetupBody device code) $+              validateVerification ep+  resp <- rawRequest api req+  pure $ statusCode (responseStatus resp) < 400
+ src/Network/HStratus/Session.hs view
@@ -0,0 +1,90 @@+{-# OPTIONS_GHC -Wno-missing-home-modules #-}++{- |+Module      : Network.HStratus.Session+Copyright   : (c) 2025 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides the core types for an iCloud authentication session.++'Credentials' holds the account ID and password used to sign in. A 'Session'+combines those credentials with a filesystem working directory (where cookies,+session tokens, and account state are persisted between runs) and a per-client+identifier. 'AccountData' carries the account information returned by the+account-login endpoint, including the HSA version that determines which+two-factor challenge flow applies.++Use 'loadSession' to initialise a session from the local filesystem. The+credentials file is read from @$XDG_CONFIG_HOME\/hstratus\/credentials.json@;+other session state is created in the same directory on first use.++The session value is then passed to 'Network.HStratus.Http.mkApiWith' (or+'Network.HStratus.Http.mkApi' for the default configuration) to construct an+'Network.HStratus.Http.Api' handle for making authenticated requests.+-}+module Network.HStratus.Session+  ( -- * Credentials++    {- | The account ID and password used to sign in to iCloud.++    Expected to be read from+    @$XDG_CONFIG_HOME\/hstratus\/credentials.json@ with the fields+    @accountName@ and @password@.+    -}+    Credentials (..)++    -- * Session++    {- | Persistent data identifying a user and their local authentication state.++    Holds the credentials used to authenticate, the directory where session files+    are stored (cookies, saved headers, account data), and the per-client OAuth+    state identifier.+    -}+  , Session (..)++    -- * AccountData++    {- | Structured account information returned by the account-login endpoint.++    The 'adHsaVersion' field determines which two-factor flow applies:++    * @0@ — unknown (used as a sentinel when no account data is available)+    * @1@ — legacy two-step authentication (2SA); handled via the setup endpoint+    * @2@ — modern two-factor authentication (2FA); handled via the auth endpoint+    -}+  , Webservice (..)+  , AccountData (..)++    -- * Loading a session++    {- | Load a 'Session' from the local filesystem.++    Reads 'Credentials' from+    @$XDG_CONFIG_HOME\/hstratus\/credentials.json@ and initialises the+    session working directory (creating it if absent). A per-client ID is read+    from disk if one exists, or generated and saved for future runs.++    Throws an 'IOError' if the credentials file is absent or cannot be parsed.+    -}+  , loadSession++    -- * Saving credentials++    {- | Write 'Credentials' to @$XDG_CONFIG_HOME\/hstratus\/credentials.json@,+    creating the directory if it does not exist.+    -}+  , saveCredentials+  )+where++import Network.HStratus.Internal.Session+  ( AccountData (..)+  , Credentials (..)+  , Session (..)+  , Webservice (..)+  , loadSession+  , saveCredentials+  )+
+ src/Network/HStratus/Trust.hs view
@@ -0,0 +1,92 @@+{- |+Module      : Network.HStratus.Trust+Copyright   : (c) 2025 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Types and utilities for handling iCloud two-factor trust challenges.++After a successful SRP sign-in, iCloud may require an additional verification+step. 'TrustData' describes the challenge: which trusted phones or devices are+available to receive a code, and the current state of the security-code gate.++Two challenge flows exist:++* __2FA__ (modern, HSA version ≥ 2): the auth endpoint issues a 'TrustData'+  challenge; the user enters a code sent to a trusted phone or device.++* __2SA__ (legacy, HSA version 1): the setup endpoint lists registered+  'Setup2SADevice' values; the user selects one to receive a code.++'pleaseReadCode' and 'selectSetupDevice' are interactive prompts used as+defaults in 'Network.HStratus.Http.login' and 'Network.HStratus.Http.complete2SA'.+Supply your own alternatives via 'Network.HStratus.Http.loginWith' and+'Network.HStratus.Http.complete2SAWith' for testing or automation.+-}+module Network.HStratus.Trust+  ( -- * Two-factor challenge data++    {- | The two-factor challenge data returned by the auth endpoint after SRP sign-in.++    Describes which trusted contacts are available to receive a verification code+    ('tdList'), the current state of the security-code gate ('tdSecurityCode'),+    and whether any trusted devices are registered ('tdNoTrustedDevices').+    -}+    TrustData (..)+    -- | Information about a trusted phone number.+  , TrustedPhone (..)++    -- * Legacy two-step device++    {- | A 2SA device from the setup endpoint.++    Stored as the raw JSON object so the entire dict can be echoed back to+    @sendVerificationCode@ and augmented for @validateVerificationCode@.+    -}+  , Setup2SADevice (..)+    {- | Extract a human-readable label from a 'Setup2SADevice', preferring+    @phoneNumber@ then @name@.+    -}+  , setup2SADeviceLabel++    -- * Interactive prompts++    {- | Interactively prompt the user to enter the verification code sent to+    their trusted phone or device. The first argument is the expected code+    length, used to make the prompt more specific (e.g. @"6-digit"@).++    Used as the default code-reading action in 'Network.HStratus.Http.login'.+    Supply an alternative via 'Network.HStratus.Http.loginWith' for testing or+    automation.+    -}+  , pleaseReadCode+    {- | Interactively prompt the user to choose between device push and SMS+    for HSA2 2FA.++    If no trusted devices are registered ('tdNoTrustedDevices' is @True@), the+    first trusted phone is selected automatically. Otherwise, the user is+    prompted to press Enter for device push or enter a number to receive an SMS+    code.+    -}+  , selectTwoFaPhone+    {- | Interactively prompt the user to select a device from a list of 2SA+    setup devices.++    Used as the default device-selection action in+    'Network.HStratus.Http.complete2SA'. Supply an alternative via+    'Network.HStratus.Http.complete2SAWith' for testing or automation.+    -}+  , selectSetupDevice+  )+where++import Network.HStratus.Internal.Trust+  ( Setup2SADevice (..)+  , TrustData (..)+  , TrustedPhone (..)+  , pleaseReadCode+  , selectSetupDevice+  , selectTwoFaPhone+  , setup2SADeviceLabel+  )+
+ test/HStratus/ApiLoggerSpec.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE OverloadedStrings #-}++module HStratus.ApiLoggerSpec (spec) where++import Data.Aeson (Value, decode)+import qualified Data.ByteString.Char8 as BS8+import qualified Data.ByteString.Lazy.Char8 as LBS8+import Data.List (isPrefixOf)+import HStratus.Mock (defaultScenario, withMockApp)+import Network.HStratus.Http (ApiLogger, fileLogger, login, mkApiWith, redactingLogger, withLogger)+import Network.HStratus.Http.Endpoints (Endpoints (..))+import Network.HStratus.Session (Credentials (..), Session (..))+import Network.HTTP.Client (Request (..), defaultManagerSettings, defaultRequest, newManager)+import Network.HTTP.Types (methodPost)+import System.FilePath ((</>))+import System.IO (Handle, IOMode (..), withFile)+import System.IO.Temp (withSystemTempDirectory)+import Test.Hspec (Spec, describe, it, shouldContain, shouldNotBe, shouldNotContain, shouldSatisfy)+++spec :: Spec+spec = describe "Network.HStratus.Http.fileLogger" $ do+  it "writes a log entry for each HTTP request during login" $+    withSystemTempDirectory "icloud-auth-log" $ \tmpDir -> do+      let logPath = tmpDir </> "requests.log"+      withLoginLog tmpDir logPath $ \contents ->+        filter (== "---") (lines contents) `shouldSatisfy` (not . null)++  it "each entry contains the HTTP method, URI and response status" $+    withSystemTempDirectory "icloud-auth-log" $ \tmpDir -> do+      let logPath = tmpDir </> "requests.log"+      withLoginLog tmpDir logPath $ \contents -> do+        contents `shouldContain` "POST"+        contents `shouldContain` "signin/init"+        contents `shouldContain` "200"++  it "response bodies are valid JSON" $+    withSystemTempDirectory "icloud-auth-log" $ \tmpDir -> do+      let logPath = tmpDir </> "requests.log"+      withLoginLog tmpDir logPath $ \contents -> do+        let body = firstBody contents+        (decode (LBS8.pack body) :: Maybe Value) `shouldNotBe` Nothing++  describe "redactingLogger" $ do+    it "replaces sensitive header values with <redacted>" $+      withSystemTempDirectory "icloud-auth-redact" $ \tmpDir -> do+        let logPath = tmpDir </> "requests.log"+        withLoginLogUsing redactingLogger tmpDir logPath $ \contents ->+          contents `shouldContain` "<redacted>"+    it "preserves the method, URI and status line" $+      withSystemTempDirectory "icloud-auth-redact" $ \tmpDir -> do+        let logPath = tmpDir </> "requests.log"+        withLoginLogUsing redactingLogger tmpDir logPath $ \contents -> do+          contents `shouldContain` "POST"+          contents `shouldContain` "signin/init"+          contents `shouldContain` "200"+    it "does not write raw Set-Cookie values" $+      withSystemTempDirectory "icloud-auth-redact" $ \tmpDir -> do+        let logPath = tmpDir </> "requests.log"+        withLoginLogUsing fileLogger tmpDir logPath $ \verboseContents ->+          withLoginLogUsing redactingLogger tmpDir (logPath <> ".redacted") $ \redactedContents -> do+            let cookieLines = filter ("Set-Cookie:" `isPrefixOf`) (lines verboseContents)+            case cookieLines of+              [] -> pure ()+              (firstLine : _) -> redactedContents `shouldNotContain` drop (length ("Set-Cookie: " :: String)) firstLine+++{- | Extract the body of the first log entry.+Format: summary line, request headers, blank line, response headers, blank line, body, "---".+-}+firstBody :: String -> String+firstBody contents =+  let skipSection = drop 1 . dropWhile (not . null)+      bodyLines = takeWhile (/= "---") $ skipSection $ skipSection $ drop 1 (lines contents)+   in unlines bodyLines+++withLoginLog :: FilePath -> FilePath -> (String -> IO a) -> IO a+withLoginLog = withLoginLogUsing fileLogger+++withLoginLogUsing :: (Handle -> ApiLogger) -> FilePath -> FilePath -> (String -> IO a) -> IO a+withLoginLogUsing mkLogger tmpDir logPath action = do+  let sessionDir = tmpDir </> "session"+  withFile logPath WriteMode $ \logHandle ->+    withMockApp defaultScenario $ \serverPort -> do+      mgr <- newManager defaultManagerSettings+      api <-+        withLogger (mkLogger logHandle)+          <$> mkApiWith (testSession sessionDir) (testEndpoints serverPort) mgr+      _ <- login api+      pure ()+  contents <- readFile logPath+  action contents+++testSession :: FilePath -> Session+testSession topDir =+  Session+    { sessionCreds = Credentials "alice@example.com" "password123"+    , sessionTopDir = topDir+    , sessionClientId = "test-client-id"+    }+++testEndpoints :: Int -> Endpoints+testEndpoints serverPort =+  Endpoints+    { epHome = "http://127.0.0.1:" <> BS8.pack (show serverPort)+    , epAuth = mockReq "/appleauth/auth"+    , epSetup = mockReq "/setup/ws/1"+    , epWidgetKey = "test-widget-key"+    }+ where+  mockReq reqPath =+    defaultRequest+      { host = "127.0.0.1"+      , port = serverPort+      , secure = False+      , method = methodPost+      , path = reqPath+      }
+ test/HStratus/Examples.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.Examples+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Shared example data used across hstratus-auth test suites.+-}+module HStratus.Examples+  ( byteStrings+  , wordz+  , errorKeys+  , codeKeys+  )+where++import Data.ByteString (ByteString)+import Data.String.Conv (toS)+import Data.Text (Text)+++byteStrings :: [ByteString]+byteStrings =+  [ "Good"+  , "King"+  , "Wenceslas"+  , "looked"+  , "out"+  , "on"+  , "feast"+  , "of"+  , "stephen"+  , "when"+  , "snow"+  , "lay"+  , "round"+  , "about"+  , "bright"+  , "crisp"+  , "even"+  ]+++wordz :: [Text]+wordz = map toS byteStrings+++errorKeys :: [Text]+errorKeys = ["errorMessage", "reason", "errorReason", "error"]+++codeKeys :: [Text]+codeKeys = ["errorCode", "serverErrorCode"]
+ test/HStratus/Http/CliSpec.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.Http.CliSpec+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Tests for the CLI options parser in 'Network.HStratus.Http.Cli'.+-}+module HStratus.Http.CliSpec (spec) where++import Network.HStratus.Http.Cli (CommonOpts (..), commonOptsParser)+import Options.Applicative (ParserResult (..), defaultPrefs, execParserPure, fullDesc, info, renderFailure)+import Test.Hspec+import Test.Hspec.Benri (endsRight)+++defaultOpts :: CommonOpts+defaultOpts = CommonOpts False False Nothing False False+++spec :: Spec+spec = describe "Network.HStratus.Http.Cli.commonOptsParser" $ do+  it "defaults all flags to False with no log file" $+    pure (parseOpts []) `endsRight` defaultOpts++  it "sets optChina when --china is given" $+    fmap optChina (parseOpts ["--china"]) `shouldBe` Right True++  it "sets optLog when --log is given" $+    fmap optLog (parseOpts ["--log"]) `shouldBe` Right True++  it "sets optLogFile when --log-file is given" $+    fmap optLogFile (parseOpts ["--log-file", "/tmp/test.log"])+      `shouldBe` Right (Just "/tmp/test.log")++  it "sets optLogBodies when --log-bodies is given" $+    fmap optLogBodies (parseOpts ["--log-bodies"]) `shouldBe` Right True++  it "sets optRedact when --redact is given" $+    fmap optRedact (parseOpts ["--redact"]) `shouldBe` Right True++  it "accepts multiple flags together" $+    pure (parseOpts ["--log", "--redact", "--china"])+      `endsRight` defaultOpts{optChina = True, optLog = True, optRedact = True}+++parseOpts :: [String] -> Either String CommonOpts+parseOpts args =+  case execParserPure defaultPrefs (info commonOptsParser fullDesc) args of+    Success opts -> Right opts+    Failure failure -> Left (fst (renderFailure failure "test"))+    CompletionInvoked _ -> Left "completion invoked"
+ test/HStratus/Http/EndpointsSpec.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.Http.EndpointsSpec+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Tests for the iCloud endpoint builders in 'Network.HStratus.Internal.Endpoints'.+-}+module HStratus.Http.EndpointsSpec (spec) where++import qualified Data.Map.Strict as Map+import Network.HStratus.Internal.Endpoints (Realm (..), lookupWebservice, realmEndpoints, signinCompleteBase)+import Network.HStratus.Internal.HttpErrors (AuthError (..))+import Network.HStratus.Internal.Session (Webservice (..))+import Network.HTTP.Client (Request (..))+import Test.Hspec (Spec, describe, it, shouldBe, shouldThrow)+++spec :: Spec+spec = describe "Network.HStratus.Internal.Endpoints" $ do+  it "signinCompleteBase queryString has no leading ?" $+    queryString (signinCompleteBase (realmEndpoints Usual)) `shouldBe` "isRememberMeEnabled=true"++  describe "lookupWebservice" $ do+    it "resolves an active service to a Request" $ do+      let ws = Map.fromList [("findme", Webservice "https://p01-fmipweb.icloud.com" (Just "active"))]+      req <- lookupWebservice "findme" ws+      host req `shouldBe` "p01-fmipweb.icloud.com"++    it "resolves a service with no status to a Request" $ do+      let ws = Map.fromList [("findme", Webservice "https://p01-fmipweb.icloud.com" Nothing)]+      req <- lookupWebservice "findme" ws+      host req `shouldBe` "p01-fmipweb.icloud.com"++    it "throws WebserviceNotFound for an absent key" $+      lookupWebservice "missing" Map.empty+        `shouldThrow` (== WebserviceNotFound "missing")++    it "throws WebserviceNotFound for an inactive service" $ do+      let ws = Map.fromList [("findme", Webservice "https://p01-fmipweb.icloud.com" (Just "inactive"))]+      lookupWebservice "findme" ws+        `shouldThrow` (== WebserviceNotFound "findme")
+ test/HStratus/Http/ErrorsSpec.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module HStratus.Http.ErrorsSpec+  ( spec+  )+where++import Control.Exception (throwIO, try)+import Data.Aeson (Value (..), decode, encode, object)+import Data.Aeson.Key (fromText)+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.Maybe (catMaybes)+import Data.Text (Text)+import qualified HStratus.Examples as Examples+import Network.HStratus.Internal.HttpErrors+  ( ApiError (..)+  , ApiResponse (..)+  , AuthError (..)+  , extractOr+  )+import Test.Hspec (Spec, context, describe, it, shouldBe, shouldReturn)+import Test.Hspec.Benri (endsLeft, endsRight)+import Test.QuickCheck+  ( Gen+  , Property+  , elements+  , forAll+  , frequency+  )+++spec :: Spec+spec = describe "module Network.HStratus.Http.Error" $ do+  apiErrorSpec+  authErrorSpec+++apiErrorSpec :: Spec+apiErrorSpec = describe "ApiError" $ do+  context "parsing it from JSON" $ do+    it "should succeed" prop_parseJSONApiError+++prop_parseJSONApiError :: Property+prop_parseJSONApiError = forAll genApiErrorWithJsonEncoding $ \(encoded, ae) ->+  decode (BS.fromStrict encoded) == Just ae+++genApiErrorWithJsonEncoding :: Gen (ByteString, ApiError)+genApiErrorWithJsonEncoding = do+  reasonKV <- genKeyValue $ elements Examples.errorKeys+  mbCodeKV <- genKeyValueMb $ elements Examples.codeKeys+  let ae =+        ApiError+          { aeReason = snd reasonKV+          , aeCode = snd <$> mbCodeKV+          }+      asKV (x, y) = (fromText x, String y)+      objectParts = catMaybes [Just reasonKV, mbCodeKV]+      encoded = BS.toStrict $ encode $ object $ map asKV objectParts+  pure (encoded, ae)+++{- |+generate a value or Nothing as the value of field+when there is a value, generate the value of the key+-}+genKeyValueMb :: Gen Text -> Gen (Maybe (Text, Text))+genKeyValueMb keyGen = do+  valueMb <-+    frequency+      [ (1, Just <$> elements Examples.wordz)+      , (2, pure Nothing)+      ]+  case valueMb of+    Nothing -> pure Nothing+    Just x -> do+      key <- keyGen+      pure (Just (key, x))+++genKeyValue :: Gen Text -> Gen (Text, Text)+genKeyValue keyGen = do+  value <- elements Examples.wordz+  key <- keyGen+  pure (key, value)+++authErrorSpec :: Spec+authErrorSpec = describe "AuthError" $ do+  context "is catchable with try @AuthError" $ do+    it "catches InvalidCredentials" $+      catchAuthError InvalidCredentials `endsLeft` InvalidCredentials+    it "catches AccountLocked" $+      catchAuthError AccountLocked `endsLeft` AccountLocked+    it "catches PrivacyAgreementRequired" $+      catchAuthError PrivacyAgreementRequired `endsLeft` PrivacyAgreementRequired+    it "catches ServiceError" $+      catchAuthError (ServiceError "reason" (Just "code")) `endsLeft` ServiceError "reason" (Just "code")+    it "catches UnexpectedResponse" $+      catchAuthError (UnexpectedResponse "oops") `endsLeft` UnexpectedResponse "oops"++  context "extractOr on a Failed ApiResponse" $ do+    it "throws ServiceError with the ApiError reason and code" $+      catchAuthError' (extractOr (Failed (ApiError "bad" (Just "E1"))))+        `endsRight` ServiceError "bad" (Just "E1")++  context "extractOr on a Succeeded ApiResponse" $ do+    it "returns the wrapped value" $+      extractOr (Succeeded (42 :: Int)) `shouldReturn` 42++  context "ApiResponse FromJSON" $ do+    it "parses a success body as Succeeded" $+      (decode "{\"length\":6}" :: Maybe (ApiResponse Value))+        `shouldBe` Just (Succeeded (object [("length", Number 6)]))+    it "parses an error body as Failed" $+      (decode "{\"errorMessage\":\"oops\"}" :: Maybe (ApiResponse Value))+        `shouldBe` Just (Failed (ApiError "oops" Nothing))+++catchAuthError :: AuthError -> IO (Either AuthError AuthError)+catchAuthError e = try (throwIO e)+++catchAuthError' :: IO a -> IO (Either AuthError AuthError)+catchAuthError' action =+  try action >>= \case+    Left e -> pure (Right e)+    Right _ -> pure (Left (UnexpectedResponse "expected AuthError but got success"))
+ test/HStratus/Http/HeadersSpec.hs view
@@ -0,0 +1,273 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.Http.HeadersSpec+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Tests for HTTP header handling in the iCloud authentication flow.+-}+module HStratus.Http.HeadersSpec (spec) where++import Data.Aeson (decode, encodeFile)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BS8+import Data.IORef (readIORef)+import Data.List (find)+import Data.Maybe (fromJust)+import HStratus.Mock (Scenario (..), defaultScenario, withMockAppCapturing)+import Network.HStratus.Http (fetchTrustData, login, loginWith, mkApiWith, requestSmsCode, verifySmsCode)+import Network.HStratus.Http.Endpoints (Endpoints (..))+import Network.HStratus.Internal.Session (SavedHeaders (..), savedHeadersPath)+import Network.HStratus.Session (Credentials (..), Session (..))+import Network.HStratus.Trust (Setup2SADevice, TrustedPhone (..))+import Network.HTTP.Client (Request (..), defaultManagerSettings, defaultRequest, newManager)+import Network.HTTP.Types (RequestHeaders, hAccept, hContentType, methodPost)+import System.IO.Temp (withSystemTempDirectory)+import System.Posix.Files (setFileMode)+import Test.Hspec (Spec, describe, it, shouldSatisfy)+++spec :: Spec+spec = describe "Network.HStratus.Http request headers" $ do+  it "signin/init sends Content-Type and Accept: application/json" $+    withCapturedLogin (\_ -> pure ()) $ \captured ->+      headersFor "/appleauth/auth/signin/init" captured+        `shouldSatisfy` hasJsonContentHeaders++  it "signin/complete sends Content-Type and Accept: application/json" $+    withCapturedLogin (\_ -> pure ()) $ \captured ->+      headersFor "/appleauth/auth/signin/complete" captured+        `shouldSatisfy` hasJsonContentHeaders++  it "2sv/trust sends Accept: application/json" $+    withCapturedTwoFa $ \captured ->+      headersFor "/appleauth/auth/2sv/trust" captured+        `shouldSatisfy` hasJsonAccept++  it "validate sends Content-Type and Accept: application/json" $+    withCapturedLogin writeSavedHeaders $ \captured ->+      headersFor "/setup/ws/1/validate" captured+        `shouldSatisfy` hasJsonContentHeaders++  it "accountLogin sends Content-Type and Accept: application/json" $+    withCapturedLogin (\_ -> pure ()) $ \captured ->+      headersFor "/setup/ws/1/accountLogin" captured+        `shouldSatisfy` hasJsonContentHeaders++  it "accountLogin sends Origin and Referer" $+    withCapturedLogin (\_ -> pure ()) $ \captured ->+      headersFor "/setup/ws/1/accountLogin" captured+        `shouldSatisfy` hasOrigin++  it "signin/init sends X-Apple-Widget-Key" $+    withCapturedLogin (\_ -> pure ()) $ \captured ->+      headersFor "/appleauth/auth/signin/init" captured+        `shouldSatisfy` hasWidgetKey++  it "2sv/trust sends X-Apple-Widget-Key" $+    withCapturedTwoFa $ \captured ->+      headersFor "/appleauth/auth/2sv/trust" captured+        `shouldSatisfy` hasWidgetKey++  it "verify/trusteddevice/securitycode sends Content-Type, Accept: application/json, and X-Apple-Widget-Key" $+    withCapturedTwoFa $ \captured ->+      headersFor "/appleauth/auth/verify/trusteddevice/securitycode" captured+        `shouldSatisfy` (\hs -> hasJsonContentHeaders hs && hasWidgetKey hs)++  it "listDevices sends Accept: application/json and X-Apple-Widget-Key" $+    withCapturedTwoSa $ \captured ->+      headersFor "/setup/ws/1/listDevices" captured+        `shouldSatisfy` (\hs -> hasJsonAccept hs && hasWidgetKey hs)++  it "sendVerificationCode sends Content-Type, Accept: application/json, and X-Apple-Widget-Key" $+    withCapturedTwoSa $ \captured ->+      headersFor "/setup/ws/1/sendVerificationCode" captured+        `shouldSatisfy` (\hs -> hasJsonContentHeaders hs && hasWidgetKey hs)++  it "validateVerificationCode sends Content-Type, Accept: application/json, and X-Apple-Widget-Key" $+    withCapturedTwoSa $ \captured ->+      headersFor "/setup/ws/1/validateVerificationCode" captured+        `shouldSatisfy` (\hs -> hasJsonContentHeaders hs && hasWidgetKey hs)++  it "GET /appleauth/auth sends scnt, X-Apple-ID-Session-Id, and X-Apple-Widget-Key" $+    withCapturedFetchTrustData $ \captured ->+      headersFor "/appleauth/auth" captured+        `shouldSatisfy` (\hs -> hasWidgetKey hs && hasScnt hs && hasSessionId hs)++  it "requestSmsCode sends Content-Type, Accept, Widget-Key, scnt, and X-Apple-ID-Session-Id" $+    withCapturedRequestSms $ \captured ->+      headersFor "/appleauth/auth/verify/phone" captured+        `shouldSatisfy` (\hs -> hasJsonContentHeaders hs && hasWidgetKey hs && hasScnt hs && hasSessionId hs)++  it "verifySmsCode sends Content-Type, Accept, Widget-Key, scnt, and X-Apple-ID-Session-Id" $+    withCapturedVerifySms $ \captured ->+      headersFor "/verify/phone/securitycode" captured+        `shouldSatisfy` (\hs -> hasJsonContentHeaders hs && hasWidgetKey hs && hasScnt hs && hasSessionId hs)+++withCapturedLogin :: (FilePath -> IO ()) -> ([(ByteString, RequestHeaders)] -> IO ()) -> IO ()+withCapturedLogin setup action =+  withSystemTempDirectory "icloud-auth-headers" $ \tmpDir -> do+    setup tmpDir+    withMockAppCapturing defaultScenario $ \serverPort capturedRef -> do+      mgr <- newManager defaultManagerSettings+      api <- mkApiWith (testSession tmpDir) (testEndpoints serverPort) mgr+      _ <- login api+      captured <- readIORef capturedRef+      action captured+++withCapturedTwoFa :: ([(ByteString, RequestHeaders)] -> IO ()) -> IO ()+withCapturedTwoFa action =+  withSystemTempDirectory "icloud-auth-headers-2fa" $ \tmpDir ->+    withMockAppCapturing defaultScenario{snAccountLoginNeeds2FA = 1} $ \serverPort capturedRef -> do+      mgr <- newManager defaultManagerSettings+      api <- mkApiWith (testSession tmpDir) (testEndpoints serverPort) mgr+      _ <- loginWith (\_ -> pure "123456") (\_ -> pure Nothing) (\_ -> pure testDevice) api+      captured <- readIORef capturedRef+      action captured+++withCapturedTwoSa :: ([(ByteString, RequestHeaders)] -> IO ()) -> IO ()+withCapturedTwoSa action =+  withSystemTempDirectory "icloud-auth-headers-2sa" $ \tmpDir ->+    withMockAppCapturing defaultScenario{snAccountLoginNeeds2SA = True} $ \serverPort capturedRef -> do+      mgr <- newManager defaultManagerSettings+      api <- mkApiWith (testSession tmpDir) (testEndpoints serverPort) mgr+      _ <- loginWith (\_ -> pure "0") (\_ -> pure Nothing) (\_ -> pure testDevice) api+      captured <- readIORef capturedRef+      action captured+++withCapturedFetchTrustData :: ([(ByteString, RequestHeaders)] -> IO ()) -> IO ()+withCapturedFetchTrustData action =+  withSystemTempDirectory "icloud-auth-headers-trustdata" $ \tmpDir -> do+    writeSavedHeadersWithSession tmpDir+    withMockAppCapturing defaultScenario $ \serverPort capturedRef -> do+      mgr <- newManager defaultManagerSettings+      api <- mkApiWith (testSession tmpDir) (testEndpoints serverPort) mgr+      _ <- fetchTrustData api+      captured <- readIORef capturedRef+      action captured+++writeSavedHeaders :: FilePath -> IO ()+writeSavedHeaders tmpDir = do+  let creds = Credentials "alice@example.com" "password123"+      shPath = savedHeadersPath tmpDir creds+      hdrs = SavedHeaders Nothing Nothing (Just "test-token") Nothing Nothing+  encodeFile shPath hdrs+  setFileMode shPath 0o600+++writeSavedHeadersWithSession :: FilePath -> IO ()+writeSavedHeadersWithSession tmpDir = do+  let creds = Credentials "alice@example.com" "password123"+      shPath = savedHeadersPath tmpDir creds+      hdrs = SavedHeaders Nothing (Just "test-session-id") Nothing Nothing (Just "test-scnt")+  encodeFile shPath hdrs+  setFileMode shPath 0o600+++withCapturedRequestSms :: ([(ByteString, RequestHeaders)] -> IO ()) -> IO ()+withCapturedRequestSms action =+  withSystemTempDirectory "icloud-auth-headers-req-sms" $ \tmpDir -> do+    writeSavedHeadersWithSession tmpDir+    withMockAppCapturing defaultScenario $ \serverPort capturedRef -> do+      mgr <- newManager defaultManagerSettings+      api <- mkApiWith (testSession tmpDir) (testEndpoints serverPort) mgr+      requestSmsCode api testPhone+      captured <- readIORef capturedRef+      action captured+++withCapturedVerifySms :: ([(ByteString, RequestHeaders)] -> IO ()) -> IO ()+withCapturedVerifySms action =+  withSystemTempDirectory "icloud-auth-headers-verify-sms" $ \tmpDir -> do+    writeSavedHeadersWithSession tmpDir+    withMockAppCapturing defaultScenario $ \serverPort capturedRef -> do+      mgr <- newManager defaultManagerSettings+      api <- mkApiWith (testSession tmpDir) (testEndpoints serverPort) mgr+      _ <- verifySmsCode api testPhone "654321"+      captured <- readIORef capturedRef+      action captured+++testPhone :: TrustedPhone+testPhone = TrustedPhone 1 "+81 test" (Just "sms")+++headersFor :: ByteString -> [(ByteString, RequestHeaders)] -> Maybe RequestHeaders+headersFor pathFragment captured =+  fmap snd $ find (\(p, _) -> pathFragment `BS8.isSuffixOf` p) captured+++hasJsonAccept :: Maybe RequestHeaders -> Bool+hasJsonAccept Nothing = False+hasJsonAccept (Just hs) = any (\(n, v) -> n == hAccept && v == "application/json") hs+++hasJsonContentType :: Maybe RequestHeaders -> Bool+hasJsonContentType Nothing = False+hasJsonContentType (Just hs) = any (\(n, v) -> n == hContentType && v == "application/json") hs+++hasWidgetKey :: Maybe RequestHeaders -> Bool+hasWidgetKey Nothing = False+hasWidgetKey (Just hs) = any (\(n, _) -> n == "X-Apple-Widget-Key") hs+++hasOrigin :: Maybe RequestHeaders -> Bool+hasOrigin Nothing = False+hasOrigin (Just hs) = any (\(n, _) -> n == "Origin") hs+++hasJsonContentHeaders :: Maybe RequestHeaders -> Bool+hasJsonContentHeaders hs = hasJsonAccept hs && hasJsonContentType hs+++hasScnt :: Maybe RequestHeaders -> Bool+hasScnt Nothing = False+hasScnt (Just hs) = any (\(n, _) -> n == "scnt") hs+++hasSessionId :: Maybe RequestHeaders -> Bool+hasSessionId Nothing = False+hasSessionId (Just hs) = any (\(n, _) -> n == "X-Apple-ID-Session-Id") hs+++testSession :: FilePath -> Session+testSession topDir =+  Session+    { sessionCreds = Credentials "alice@example.com" "password123"+    , sessionTopDir = topDir+    , sessionClientId = "test-client-id"+    }+++testEndpoints :: Int -> Endpoints+testEndpoints serverPort =+  Endpoints+    { epHome = "http://127.0.0.1:" <> BS8.pack (show serverPort)+    , epAuth = mockReq "/appleauth/auth"+    , epSetup = mockReq "/setup/ws/1"+    , epWidgetKey = "test-widget-key"+    }+ where+  mockReq reqPath =+    defaultRequest+      { host = "127.0.0.1"+      , port = serverPort+      , secure = False+      , method = methodPost+      , path = reqPath+      }+++testDevice :: Setup2SADevice+testDevice =+  fromJust $+    decode+      "{\"deviceType\":\"SMS\",\"areaCode\":\"\",\"phoneNumber\":\"*******58\",\"deviceId\":\"1\"}"
+ test/HStratus/HttpMockSpec.hs view
@@ -0,0 +1,223 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.HttpMockSpec+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Tests for the iCloud HTTP client using a mock server.+-}+module HStratus.HttpMockSpec (spec) where++import Control.Exception (try)+import Data.Aeson (decode, encodeFile)+import Data.Bits ((.&.))+import qualified Data.ByteString.Char8 as BS8+import Data.IORef (newIORef, readIORef, writeIORef)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (fromJust)+import qualified Data.Text as Text+import HStratus.Mock (Scenario (..), SrpOutcome (..), defaultScenario, withMockApp, withMockAppCapturing)+import Network.HStratus.Http+  ( Api+  , AuthState (..)+  , complete2SAWith+  , login+  , loginWith+  , mkApiWith+  )+import Network.HStratus.Http.Endpoints (Endpoints (..))+import Network.HStratus.Internal.HttpErrors (AuthError (..))+import Network.HStratus.Internal.Session (SavedHeaders (..), cookiePath, savedHeadersPath)+import Network.HStratus.Session (Credentials (..), Session (..))+import Network.HStratus.Trust (Setup2SADevice, TrustedPhone (..))+import Network.HTTP.Client (Request (..), defaultManagerSettings, defaultRequest, newManager)+import Network.HTTP.Types (methodPost)+import System.FilePath ((</>))+import System.IO.Temp (withSystemTempDirectory)+import System.Posix.Files (fileMode, getFileStatus, setFileMode)+import Test.Hspec (Spec, describe, it, shouldBe, shouldReturn, shouldSatisfy)+++spec :: Spec+spec = describe "Network.HStratus.Http.login" $ do+  it "returns Authenticated on fresh login" $+    loginShouldAuthenticate defaultScenario++  it "creates the session directory when absent then returns Authenticated" $+    withSystemTempDirectory "icloud-auth-mock" $ \tmpDir ->+      withMockApi (tmpDir </> "session") defaultScenario $ \api -> do+        isAuthenticated <$> login api `shouldReturn` True++  it "returns Authenticated when saved headers are valid" $+    withSystemTempDirectory "icloud-auth-mock" $ \tmpDir -> do+      writeSavedHeaders tmpDir+      withMockApi tmpDir defaultScenario $ \api -> do+        isAuthenticated <$> login api `shouldReturn` True++  it "falls through to fresh login when validate returns 401" $+    withSystemTempDirectory "icloud-auth-mock" $ \tmpDir -> do+      writeSavedHeaders tmpDir+      withMockApi tmpDir defaultScenario{snValidate = False} $ \api -> do+        isAuthenticated <$> login api `shouldReturn` True++  it "completes 2FA automatically when accountLogin requires 2FA" $+    withFreshMockApi "icloud-auth-mock" defaultScenario{snAccountLoginNeeds2FA = 1} $ \api -> do+      isAuthenticated <$> loginWith (\_ -> pure "123456") (\_ -> pure Nothing) (\_ -> pure testDevice) api `shouldReturn` True++  it "complete2SA returns Authenticated after 2SA challenge" $+    withFreshMockApi "icloud-auth-2sa" defaultScenario $ \api -> do+      result <- complete2SAWith (\_ -> pure testDevice) (pure "0") api (testDevice :| [])+      isAuthenticated result `shouldBe` True++  it "completes 2SA automatically when account login signals 2SA required" $+    withFreshMockApi "icloud-auth-2sa-login" defaultScenario{snAccountLoginNeeds2SA = True} $ \api -> do+      isAuthenticated <$> loginWith (\_ -> pure "0") (\_ -> pure Nothing) (\_ -> pure testDevice) api `shouldReturn` True++  it "throws UnexpectedResponse with HTTP status when error response has no body" $+    withFreshMockApi "icloud-auth-empty-err" defaultScenario{snSrpCompleteEmptyError = True} $ \api -> do+      let throwsBadRequest (Left (UnexpectedResponse msg)) = "bad request" `Text.isPrefixOf` msg+          throwsBadRequest _otherwise = False+      result <- try (login api) :: IO (Either AuthError AuthState)+      result `shouldSatisfy` throwsBadRequest++  it "complete2SA retries when the first verification code is wrong" $ do+    codeRef <- newIORef ["wrongcode", "0"]+    let readCode = do+          codes <- readIORef codeRef+          case codes of+            [] -> fail "no more codes"+            (c : rest) -> writeIORef codeRef rest >> pure c+    withFreshMockApi "icloud-auth-2sa-retry" defaultScenario{snValidateCodeFails = True} $ \api -> do+      result <- complete2SAWith (\_ -> pure testDevice) readCode api (testDevice :| [])+      isAuthenticated result `shouldBe` True++  it "completes 2FA via SMS when phone selector returns a phone" $+    withFreshMockApi "icloud-auth-2fa-sms" defaultScenario{snAccountLoginNeeds2FA = 1} $ \api -> do+      isAuthenticated <$> loginWith (\_ -> pure "654321") (\_ -> pure (Just testPhone)) (\_ -> pure testDevice) api+        `shouldReturn` True++  it "calls GET /appleauth/auth when accountLogin requires 2FA" $+    withSystemTempDirectory "icloud-auth-fetches-trust" $ \tmpDir -> do+      let scenario = defaultScenario{snSrpOutcome = SrpNeeds2FA, snAccountLoginNeeds2FA = 1}+      withMockAppCapturing scenario $ \serverPort capturedRef -> do+        mgr <- newManager defaultManagerSettings+        api <- mkApiWith (testSession tmpDir) (testEndpoints serverPort) mgr+        _ <- loginWith (\_ -> pure "123456") (\_ -> pure Nothing) (\_ -> pure testDevice) api+        captured <- readIORef capturedRef+        map fst captured `shouldSatisfy` elem "/appleauth/auth"++  it "completes 2FA via device push after retrying when the first code is wrong" $ do+    codeRef <- newIORef ["wrongcode", "123456"]+    let readCode = do+          codes <- readIORef codeRef+          case codes of+            [] -> fail "no more codes"+            (c : rest) -> writeIORef codeRef rest >> pure c+    withFreshMockApi "icloud-auth-2fa-retry" defaultScenario{snAccountLoginNeeds2FA = 1, snVerifyDeviceCodeFails = True} $ \api ->+      isAuthenticated <$> loginWith (const readCode) (\_ -> pure Nothing) (\_ -> pure testDevice) api+        `shouldReturn` True++  it "retries signin/init and completes login when first response is 421" $+    loginShouldAuthenticate defaultScenario{snSrpInitReturnsRetryCode = Just 421}++  it "retries signin/init and completes login when first response is 450" $+    loginShouldAuthenticate defaultScenario{snSrpInitReturnsRetryCode = Just 450}++  it "retries signin/init and completes login when first response is 500" $+    loginShouldAuthenticate defaultScenario{snSrpInitReturnsRetryCode = Just 500}++  it "throws TwoFactorLocked when the server signals the account is locked" $+    withFreshMockApi "icloud-auth-2fa-locked" defaultScenario{snAccountLoginNeeds2FA = 1, snVerifyCodeLocks = True} $ \api -> do+      result <- try (loginWith (\_ -> pure "wrongcode") (\_ -> pure Nothing) (\_ -> pure testDevice) api) :: IO (Either AuthError AuthState)+      result `shouldSatisfy` (\case Left TwoFactorLocked -> True; _ -> False)++  it "sets cookie jar mode to 0o600 after login" $+    withSystemTempDirectory "icloud-auth-cookie-perms" $ \tmpDir ->+      withMockApi tmpDir defaultScenario $ \api -> do+        _ <- login api+        let creds = Credentials "alice@example.com" "password123"+            jarPath = cookiePath tmpDir creds+        shouldHaveMode600 jarPath+++shouldHaveMode600 :: FilePath -> IO ()+shouldHaveMode600 filePath = do+  mode <- fileMode <$> getFileStatus filePath+  (mode .&. 0o777) `shouldBe` 0o600+++withMockApi :: FilePath -> Scenario -> (Api -> IO a) -> IO a+withMockApi tmpDir scenario action =+  withMockApp scenario $ \serverPort -> do+    mgr <- newManager defaultManagerSettings+    api <- mkApiWith (testSession tmpDir) (testEndpoints serverPort) mgr+    action api+++withFreshMockApi :: String -> Scenario -> (Api -> IO a) -> IO a+withFreshMockApi prefix scenario action =+  withSystemTempDirectory prefix $ \tmpDir ->+    withMockApi tmpDir scenario action+++loginShouldAuthenticate :: Scenario -> IO ()+loginShouldAuthenticate scenario =+  withFreshMockApi "icloud-auth-mock" scenario $ \api -> do+    result <- login api+    isAuthenticated result `shouldBe` True+++testSession :: FilePath -> Session+testSession topDir =+  Session+    { sessionCreds = Credentials "alice@example.com" "password123"+    , sessionTopDir = topDir+    , sessionClientId = "test-client-id"+    }+++testEndpoints :: Int -> Endpoints+testEndpoints serverPort =+  Endpoints+    { epHome = "http://127.0.0.1:" <> BS8.pack (show serverPort)+    , epAuth = mockReq "/appleauth/auth"+    , epSetup = mockReq "/setup/ws/1"+    , epWidgetKey = "test-widget-key"+    }+ where+  mockReq reqPath =+    defaultRequest+      { host = "127.0.0.1"+      , port = serverPort+      , secure = False+      , method = methodPost+      , path = reqPath+      }+++writeSavedHeaders :: FilePath -> IO ()+writeSavedHeaders tmpDir = do+  let creds = Credentials "alice@example.com" "password123"+      hdrsPath = savedHeadersPath tmpDir creds+      hdrs = SavedHeaders Nothing Nothing (Just "test-token") Nothing Nothing+  encodeFile hdrsPath hdrs+  setFileMode hdrsPath 0o600+++isAuthenticated :: AuthState -> Bool+isAuthenticated (Authenticated _ _) = True+isAuthenticated _ = False+++testDevice :: Setup2SADevice+testDevice =+  fromJust $+    decode+      "{\"deviceType\":\"SMS\",\"areaCode\":\"\",\"phoneNumber\":\"*******58\",\"deviceId\":\"1\"}"+++testPhone :: TrustedPhone+testPhone = TrustedPhone 1 "+81 test" (Just "sms")
+ test/HStratus/HttpSpec.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.HttpSpec+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Tests for the iCloud HTTP client request and response handling.+-}+module HStratus.HttpSpec+  ( spec+  )+where++import Crypto.SRP.Hashing (KnownAlgorithm (SHA256), hashText)+import Data.Aeson (Value (..), decode, withObject, (.:))+import Data.Aeson.KeyMap (fromList)+import Data.Aeson.Types (parseMaybe)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Base16 as Base16+import Data.String.Conv (toS)+import Data.Text (Text)+import qualified HStratus.Examples as Examples+import Network.HStratus.Internal.Http+  ( PasswordProtocol (..)+  , hCounter+  , hCountry+  , hSessionId+  , hSessionToken+  , hTrustToken+  , needsRetry+  , phoneCodeBody+  , phoneTriggerBody+  , validateSetupBody+  )+import Network.HStratus.Internal.Session+  ( SavedHeaders (..)+  , pristine+  , updateSavedHeaders+  )+import Network.HStratus.Trust (Setup2SADevice (..), TrustedPhone (..))+import Network.HTTP.Types (HeaderName)+import Test.Hspec (Spec, context, describe, it, shouldBe)+import Test.QuickCheck+  ( Gen+  , Property+  , elements+  , forAll+  , forAllBlind+  , sublistOf+  , vectorOf+  )+++spec :: Spec+spec = describe "module Network.HStratus.Http" $ do+  updateSavedHeadersSpec+  needsRetrySpec+  passwordProtocolSpec+  validateSetupBodySpec+  phoneCodeBodySpec+  phoneTriggerBodySpec+++updateSavedHeadersSpec :: Spec+updateSavedHeadersSpec = describe "updateSavedHeaders" $ do+  context "using generated headers" $ do+    it "should generated the expected value" prop_updateSavedHeaders+++prop_updateSavedHeaders :: Property+prop_updateSavedHeaders = forAllBlind genHdrsAndExpectedSavedHeaders $ \(hdrs, f) ->+  f $ updateSavedHeaders hdrs pristine+++genHdrsAndExpectedSavedHeaders :: Gen ([(HeaderName, ByteString)], SavedHeaders -> Bool)+genHdrsAndExpectedSavedHeaders = do+  checks <- sublistOf sdChecks+  values <- vectorOf (length checks) (elements Examples.byteStrings)+  let headers = zip (map fst checks) values+      asPred getter want sd = Just (toS want) == getter sd+      preds = zipWith asPred (map snd checks) values+      combine xs sd = all ($ sd) xs+  pure (headers, combine preds)+++sdChecks :: [(HeaderName, SavedHeaders -> Maybe Text)]+sdChecks =+  [ (hCountry, shCountry)+  , (hSessionId, shSessionId)+  , (hSessionToken, shSessionToken)+  , (hTrustToken, shTrustToken)+  , (hCounter, shCounter)+  ]+++needsRetrySpec :: Spec+needsRetrySpec = describe "needsRetry" $ do+  it "is True for 421" $ needsRetry 421 `shouldBe` True+  it "is True for 450" $ needsRetry 450 `shouldBe` True+  it "is True for 500" $ needsRetry 500 `shouldBe` True+  it "is False for 200" $ needsRetry 200 `shouldBe` False+  it "is False for 400" $ needsRetry 400 `shouldBe` False+++passwordProtocolSpec :: Spec+passwordProtocolSpec = describe "PasswordProtocol" $ do+  context "parsing from JSON" $ do+    it "parses 's2k' as New" $+      (decode "\"s2k\"" :: Maybe PasswordProtocol) `shouldBe` Just New+    it "parses 's2k_fo' as Old" $+      (decode "\"s2k_fo\"" :: Maybe PasswordProtocol) `shouldBe` Just Old+    it "fails on unknown strings" $+      (decode "\"unknown\"" :: Maybe PasswordProtocol) `shouldBe` Nothing+  context "key derivation" $ do+    it "Old (Base16-encoded hash) always differs from New (raw hash)" $+      prop_oldNewHashesDiffer+++prop_oldNewHashesDiffer :: Property+prop_oldNewHashesDiffer = forAll (elements Examples.wordz) $ \pwd ->+  let hashed = hashText SHA256 pwd+   in Base16.encode hashed /= hashed+++validateSetupBodySpec :: Spec+validateSetupBodySpec = describe "validateSetupBody" $ do+  it "includes verificationCode from the code argument" $+    field "verificationCode" `shouldBe` Just (String "123456")+  it "includes trustBrowser set to True" $+    field "trustBrowser" `shouldBe` Just (Bool True)+  it "preserves original device fields" $+    field "deviceId" `shouldBe` Just (String "abc")+ where+  device = Setup2SADevice $ fromList [("deviceId", String "abc")]+  body = validateSetupBody device "123456"+  field k = parseMaybe (withObject "body" (.: k)) body+++phoneCodeBodySpec :: Spec+phoneCodeBodySpec = describe "phoneCodeBody" $ do+  it "sets phoneNumber.id to the TrustedPhone id" $+    phoneField verifyBody "id" `shouldBe` Just (Number 1)+  it "sets securityCode.code to the supplied code" $+    codeField verifyBody "code" `shouldBe` Just (String "654321")+  it "sets mode to sms" $+    field verifyBody "mode" `shouldBe` Just (String "sms")+  it "sets securityCode.code to empty string when requesting SMS" $+    codeField requestBody "code" `shouldBe` Just (String "")+  it "defaults mode to sms when tpnPushMode is Nothing" $+    field (phoneCodeBody noModePhone "123") "mode" `shouldBe` Just (String "sms")+ where+  phone = TrustedPhone 1 "+81 test" (Just "sms")+  noModePhone = TrustedPhone 1 "+81 test" Nothing+  verifyBody = phoneCodeBody phone "654321"+  requestBody = phoneCodeBody phone ""+  field b k = parseMaybe (withObject "body" (.: k)) b+  phoneField b k = field b "phoneNumber" >>= parseMaybe (withObject "phoneNumber" (.: k))+  codeField b k = field b "securityCode" >>= parseMaybe (withObject "securityCode" (.: k))+++phoneTriggerBodySpec :: Spec+phoneTriggerBodySpec = describe "phoneTriggerBody" $ do+  it "sets phoneNumber.id to the TrustedPhone id" $+    phoneField triggerBody "id" `shouldBe` Just (Number 1)+  it "sets mode to the tpnPushMode value" $+    field triggerBody "mode" `shouldBe` Just (String "sms")+  it "defaults mode to sms when tpnPushMode is Nothing" $+    field (phoneTriggerBody noModePhone) "mode" `shouldBe` Just (String "sms")+  it "does not include a securityCode field" $+    field triggerBody "securityCode" `shouldBe` (Nothing :: Maybe Value)+ where+  phone = TrustedPhone 1 "+81 test" (Just "sms")+  noModePhone = TrustedPhone 1 "+81 test" Nothing+  triggerBody = phoneTriggerBody phone+  field b k = parseMaybe (withObject "body" (.: k)) b+  phoneField b k = field b "phoneNumber" >>= parseMaybe (withObject "phoneNumber" (.: k))
+ test/HStratus/LoginFSMSpec.hs view
@@ -0,0 +1,294 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}++{- |+Module      : HStratus.LoginFSMSpec+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Tests for the iCloud login finite state machine in 'Network.HStratus.Internal.LoginFSM'.+-}+module HStratus.LoginFSMSpec+  ( spec+  )+where++import Data.List.NonEmpty (NonEmpty (..))+import Network.HStratus.Internal.LoginFSM+import Test.Hspec (Spec, describe, it, shouldBe)+++newtype TestState s = TestState ()+++data Script = Script+  { scriptCreds :: !Bool+  , scriptDir :: !Bool+  , scriptMkDir :: !Bool+  , scriptHasSavedSession :: !Bool+  , scriptSessionValid :: !Bool+  , scriptSrpInvalidKey :: !Bool+  , scriptAcct :: !Bool+  , scriptAcctTwoFa :: !Bool+  , scriptTwoFa :: ![Bool]+  , scriptTwoSa :: ![Bool]+  , scriptNoTrustedDevices :: !Bool+  , scriptTwoFaLocked :: !Bool+  }+++allTrue :: Script+allTrue =+  Script+    { scriptCreds = True+    , scriptDir = True+    , scriptMkDir = True+    , scriptHasSavedSession = False+    , scriptSessionValid = False+    , scriptSrpInvalidKey = False+    , scriptAcct = True+    , scriptAcctTwoFa = False+    , scriptTwoFa = [True]+    , scriptTwoSa = [True]+    , scriptNoTrustedDevices = False+    , scriptTwoFaLocked = False+    }+++popTwoFa :: TestM Bool+popTwoFa = TestM $ \s -> case scriptTwoFa s of+  (b : bs) -> (b, s{scriptTwoFa = bs})+  [] -> (True, s)+++popTwoSa :: TestM Bool+popTwoSa = TestM $ \s -> case scriptTwoSa s of+  (b : bs) -> (b, s{scriptTwoSa = bs})+  [] -> (True, s)+++instance LoginEvent TestM where+  type State TestM = TestState+++  initial = pure (TestState ())+++  ratifyCreds (TestState ()) = asksScript $ \s ->+    if scriptCreds s+      then GotCreds (TestState ())+      else NoCreds (TestState ())+++  ratifyArtifactDir (TestState ()) = asksScript $ \s ->+    if scriptDir s+      then DirPresent (TestState ())+      else DirAbsent (TestState ())+++  mkArtifactDir (TestState ()) = asksScript $ \s ->+    if scriptMkDir s+      then DirMade (TestState ())+      else NotMade (TestState ())+++  loadSession (TestState ()) = asksScript $ \s ->+    if scriptHasSavedSession s+      then HasPriorSession (TestState ())+      else HasClientId (TestState ())+++  validateSession (TestState ()) = asksScript $ \s ->+    if scriptSessionValid s+      then SessionStillValid (TestState ())+      else SessionStale (TestState ())+++  srpInit (TestState ()) = pure (TestState ())+++  srpComplete (TestState ()) = asksScript $ \s ->+    if scriptSrpInvalidKey s+      then SrpCompleteInvalidKey (TestState ())+      else SrpCompleteOk (TestState ())+++  acctLogin (TestState ()) = asksScript $ \s ->+    if scriptAcctTwoFa s+      then AcctLogin2FA (TestState ())+      else+        if scriptAcct s+          then AcctLoginOk (TestState ())+          else AcctLogin2SA (TestState ())+++  listTwoSaDevices (TestState ()) = pure (TestState ())+++  beginTwoFa (TestState ()) _cfg = pure (TestState ())+++  doTrust (TestState ()) = pure (TestState ())+++  verifyTwoFa (TestState ()) _cfg = do+    result <- popTwoFa+    if result+      then pure $ TwoFaOk (TestState ())+      else asksScript $ \s ->+        if scriptTwoFaLocked s+          then TwoFaLocked (TestState ())+          else TwoFaRetry (TestState ())+++  beginTwoSa (TestState ()) _cfg = pure (TestState ())+++  verifyTwoSa (TestState ()) _cfg = do+    result <- popTwoSa+    pure $ if result then TwoSaOk (TestState ()) else TwoSaRetry (TestState ())+++data Outcome+  = Authenticated+  | TwoFa+  | TwoSa+  | HaltCreds+  | HaltMkDir+  | HaltSrp+  | LockedByTwoFa+  deriving (Eq, Show)+++outcomeOf :: LoginOutcome TestState -> Outcome+outcomeOf = \case+  LoginAuthenticated _ -> Authenticated+  LoginNeedsTwoFa _ -> TwoFa+  LoginNeedsTwoSa _ -> TwoSa+  LoginHaltCreds _ -> HaltCreds+  LoginHaltDir _ -> HaltMkDir+  LoginHaltSrp _ -> HaltSrp+  LoginHaltTwoFaLocked _ -> LockedByTwoFa+++completionOutcomeOf :: CompletionOutcome TestState -> Outcome+completionOutcomeOf = \case+  CompletionAuthenticated _ -> Authenticated+  CompletionNeedsTwoFa _ -> TwoFa+  CompletionNeedsTwoSa _ -> TwoSa+  CompletionTwoFaLocked _ -> LockedByTwoFa+++runScript :: Script -> Outcome+runScript s = outcomeOf $ runTestM loginProcess s+++runTwoFaScript :: Script -> Outcome+runTwoFaScript s = completionOutcomeOf $ runTestM (twoFaProcess (TestState ()) dummyTwoFaConfig) s+++runTwoSaScript :: Script -> Outcome+runTwoSaScript s = completionOutcomeOf $ runTestM (twoSaProcess (TestState ()) dummyTwoSaConfig) s+++dummyTwoFaConfig :: TwoFaConfig+dummyTwoFaConfig = TwoFaConfig{tfcPickPhone = \_ -> pure Nothing, tfcReadCode = \_ -> pure ""}+++dummyTwoSaConfig :: TwoSaConfig+dummyTwoSaConfig =+  TwoSaConfig+    { tscPickDevice = \(d :| _) -> pure d+    , tscReadCode = pure ""+    }+++spec :: Spec+spec = do+  describe "LoginFSM.loginProcess" $ do+    it "halts when credentials are missing" $+      runScript (allTrue{scriptCreds = False}) `shouldBe` HaltCreds++    it "halts when the artifact directory cannot be created" $+      runScript (allTrue{scriptDir = False, scriptMkDir = False}) `shouldBe` HaltMkDir++    it "halts with invalid SRP key when the server public value is bad" $+      runScript (allTrue{scriptSrpInvalidKey = True}) `shouldBe` HaltSrp++    it "reaches Authenticated on the happy path" $+      runScript allTrue `shouldBe` Authenticated++    it "produces LoginNeedsTwoFa when account login signals 2FA required" $+      runScript (allTrue{scriptAcctTwoFa = True}) `shouldBe` TwoFa++    it "reaches Requires2SA when account login signals 2SA required" $+      runScript (allTrue{scriptAcct = False}) `shouldBe` TwoSa++    it "creates the artifact directory when absent then reaches Authenticated" $+      runScript (allTrue{scriptDir = False}) `shouldBe` Authenticated++    it "returns Authenticated immediately when the saved session is still valid" $+      runScript (allTrue{scriptHasSavedSession = True, scriptSessionValid = True}) `shouldBe` Authenticated++    it "falls through to SRP when the saved session is stale" $+      runScript (allTrue{scriptHasSavedSession = True, scriptSessionValid = False}) `shouldBe` Authenticated++  describe "LoginFSM.twoFaProcess" $ do+    it "reaches Authenticated when 2FA verification succeeds on the first attempt" $+      runTwoFaScript allTrue `shouldBe` Authenticated++    it "retries and reaches Authenticated after a failed 2FA verification" $+      runTwoFaScript (allTrue{scriptTwoFa = [False, True]}) `shouldBe` Authenticated++    it "reaches Requires2SA when account login signals 2SA required after 2FA" $+      runTwoFaScript (allTrue{scriptAcct = False}) `shouldBe` TwoSa++    it "still reaches Authenticated when noTrustedDevices is True" $+      runTwoFaScript (allTrue{scriptNoTrustedDevices = True}) `shouldBe` Authenticated++    it "halts with TwoFaLocked when the code is rejected and the server signals the account is locked" $+      runTwoFaScript (allTrue{scriptTwoFa = [False], scriptTwoFaLocked = True}) `shouldBe` LockedByTwoFa++  describe "LoginFSM.twoSaProcess" $ do+    it "reaches Authenticated when 2SA verification succeeds on the first attempt" $+      runTwoSaScript allTrue `shouldBe` Authenticated++    it "retries and reaches Authenticated after a failed 2SA verification" $+      runTwoSaScript (allTrue{scriptTwoSa = [False, True]}) `shouldBe` Authenticated++    it "reaches Requires2SA when account login signals 2SA required after 2SA" $+      runTwoSaScript (allTrue{scriptAcct = False}) `shouldBe` TwoSa+++newtype TestM a = TestM (Script -> (a, Script))+++instance Functor TestM where+  fmap f (TestM m) = TestM $ \s -> let (a, s') = m s in (f a, s')+++instance Applicative TestM where+  pure a = TestM (a,)+  TestM mf <*> TestM ma = TestM $ \s ->+    let (f, s') = mf s+        (a, s'') = ma s'+     in (f a, s'')+++instance Monad TestM where+  return = pure+  TestM ma >>= f = TestM $ \s ->+    let (a, s') = ma s+        TestM mb = f a+     in mb s'+++runTestM :: TestM a -> Script -> a+runTestM (TestM m) s = fst (m s)+++asksScript :: (Script -> a) -> TestM a+asksScript f = TestM $ \s -> (f s, s)
+ test/HStratus/Mock.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.Mock+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++A configurable mock iCloud server for hstratus-auth integration tests.+-}+module HStratus.Mock+  ( Scenario (..)+  , SrpOutcome (..)+  , defaultScenario+  , withMockApp+  , withMockAppCapturing+  )+where++import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as LBS+import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef)+import Network.HTTP.Types (HeaderName, RequestHeaders, hContentType, mkStatus, status200, status204, status400, status401, status404, status409)+import Network.Wai (Application, pathInfo, rawPathInfo, requestHeaders, requestMethod, responseLBS)+import Network.Wai.Handler.Warp (testWithApplication)+import Paths_hstratus_auth (getDataFileName)+++data SrpOutcome = SrpOk | SrpNeeds2FA+  deriving (Eq, Show)+++data Scenario = Scenario+  { snValidate :: Bool+  , snSrpOutcome :: SrpOutcome+  , snValidateCodeFails :: Bool+  -- ^ when True, the first validateVerificationCode call returns 400+  , snAccountLoginNeeds2SA :: Bool+  -- ^ when True, the first accountLogin call returns a 2SA-required response+  , snAccountLoginNeeds2FA :: Int+  -- ^ countdown: serve login_2fa_test.json while > 0, then loginWorking+  , snSrpCompleteEmptyError :: Bool+  -- ^ when True, signin/complete returns 401 with no body or Content-Type+  , snVerifyCodeLocks :: Bool+  -- ^ when True, POST verify/trusteddevice/securitycode returns 400 and GET /appleauth/auth returns locked trust data+  , snVerifyDeviceCodeFails :: Bool+  -- ^ when True, the first POST verify/trusteddevice/securitycode returns 400 (non-locking retry)+  , snSrpInitReturnsRetryCode :: Maybe Int+  -- ^ when Just n, signin/init returns status n on the first call, then succeeds+  }+++defaultScenario :: Scenario+defaultScenario =+  Scenario+    { snValidate = True+    , snSrpOutcome = SrpOk+    , snValidateCodeFails = False+    , snAccountLoginNeeds2SA = False+    , snAccountLoginNeeds2FA = 0+    , snSrpCompleteEmptyError = False+    , snVerifyCodeLocks = False+    , snVerifyDeviceCodeFails = False+    , snSrpInitReturnsRetryCode = Nothing+    }+++jsonHeaders :: [(HeaderName, ByteString)]+jsonHeaders = [(hContentType, "application/json"), ("X-Apple-Session-Token", "mock-session-token-value")]+++lockedTrustData :: LBS.ByteString+lockedTrustData =+  "{\"securityCode\":{\"length\":6,\"tooManyCodesSent\":false,\"tooManyCodesValidated\":false\+  \,\"securityCodeLocked\":true,\"securityCodeCooldown\":false}\+  \,\"trustedPhoneNumbers\":[{\"id\":1,\"numberWithDialCode\":\"+1 test\",\"pushMode\":null}]\+  \,\"noTrustedDevices\":false}"+++withMockApp :: Scenario -> (Int -> IO a) -> IO a+withMockApp scenario action =+  withMockAppCapturing scenario $ \port _ -> action port+++withMockAppCapturing :: Scenario -> (Int -> IORef [(ByteString, RequestHeaders)] -> IO a) -> IO a+withMockAppCapturing scenario action = do+  srpInit <- LBS.readFile =<< getDataFileName "testdata/srp_init_ok_test.json"+  login2fa <- LBS.readFile =<< getDataFileName "testdata/login_2fa_test.json"+  login2sa <- LBS.readFile =<< getDataFileName "testdata/login_2sa_test.json"+  loginWorking <- LBS.readFile =<< getDataFileName "testdata/login_working_test.json"+  listDevices <- LBS.readFile =<< getDataFileName "testdata/trusted_devices_test.json"+  trustData <- LBS.readFile =<< getDataFileName "testdata/trust_data_test.json"+  codeAttemptsRef <- newIORef (0 :: Int)+  deviceCodeAttemptsRef <- newIORef (0 :: Int)+  accountLoginRef <- newIORef (0 :: Int)+  srpInitRetryRef <- newIORef (0 :: Int)+  capturedRef <- newIORef []+  testWithApplication+    ( pure $+        mockApp+          scenario+          capturedRef+          srpInit+          login2fa+          login2sa+          loginWorking+          listDevices+          trustData+          codeAttemptsRef+          deviceCodeAttemptsRef+          accountLoginRef+          srpInitRetryRef+    )+    (\port -> action port capturedRef)+++mockApp+  :: Scenario+  -> IORef [(ByteString, RequestHeaders)]+  -> LBS.ByteString+  -> LBS.ByteString+  -> LBS.ByteString+  -> LBS.ByteString+  -> LBS.ByteString+  -> LBS.ByteString+  -> IORef Int+  -> IORef Int+  -> IORef Int+  -> IORef Int+  -> Application+mockApp+  scenario+  capturedRef+  srpInit+  login2fa+  login2sa+  loginWorking+  listDevices+  trustData+  codeAttemptsRef+  deviceCodeAttemptsRef+  accountLoginRef+  srpInitRetryRef+  req+  respond = do+    modifyIORef' capturedRef ((rawPathInfo req, requestHeaders req) :)+    let method = requestMethod req+        segs = pathInfo req+        json st body = responseLBS st jsonHeaders body+    resp <- case (method, segs) of+      ("GET", ["appleauth", "auth"]) ->+        pure $+          json status200 $+            if snVerifyCodeLocks scenario then lockedTrustData else trustData+      ("POST", ["appleauth", "auth", "signin", "init"]) -> do+        n <- readIORef srpInitRetryRef+        writeIORef srpInitRetryRef (n + 1)+        pure $ case snSrpInitReturnsRetryCode scenario of+          Just code | n == 0 -> responseLBS (mkStatus code "") [] ""+          _ -> json status200 srpInit+      ("POST", ["appleauth", "auth", "signin", "complete"]) ->+        pure $+          if snSrpCompleteEmptyError scenario+            then responseLBS status401 [] ""+            else case snSrpOutcome scenario of+              SrpOk -> json status200 "{}"+              SrpNeeds2FA -> responseLBS status409 jsonHeaders "{}"+      ("GET", ["appleauth", "auth", "2sv", "trust"]) ->+        pure $ json status200 "{}"+      ("PUT", ["appleauth", "auth", "verify", "trusteddevice", "securitycode"]) ->+        pure $ responseLBS status204 [] ""+      ("POST", ["appleauth", "auth", "verify", "trusteddevice", "securitycode"]) -> do+        n <- readIORef deviceCodeAttemptsRef+        writeIORef deviceCodeAttemptsRef (n + 1)+        pure $+          if snVerifyCodeLocks scenario+            then responseLBS status400 [] ""+            else+              if snVerifyDeviceCodeFails scenario && n == 0+                then responseLBS status400 [] ""+                else json status200 "{}"+      ("PUT", ["appleauth", "auth", "verify", "phone"]) ->+        pure $ json status200 "{}"+      ("POST", ["appleauth", "auth", "verify", "phone", "securitycode"]) ->+        pure $ json status200 "true"+      ("GET", ["setup", "ws", "1", "listDevices"]) ->+        pure $ json status200 listDevices+      ("POST", ["setup", "ws", "1", "sendVerificationCode"]) ->+        pure $ json status200 "{}"+      ("POST", ["setup", "ws", "1", "validateVerificationCode"]) -> do+        n <- readIORef codeAttemptsRef+        writeIORef codeAttemptsRef (n + 1)+        pure $+          if snValidateCodeFails scenario && n == 0+            then responseLBS status400 [] ""+            else json status200 "{}"+      ("POST", ["setup", "ws", "1", "validate"]) ->+        pure $+          if snValidate scenario+            then json status200 "{}"+            else responseLBS status401 [] ""+      ("POST", ["setup", "ws", "1", "accountLogin"]) -> do+        n <- readIORef accountLoginRef+        writeIORef accountLoginRef (n + 1)+        pure $+          if snAccountLoginNeeds2FA scenario > n+            then json status200 login2fa+            else+              if snAccountLoginNeeds2SA scenario && n == 0+                then json status200 login2sa+                else json status200 loginWorking+      _ -> pure $ responseLBS status404 [] "not found"+    respond resp
+ test/HStratus/PBKDF2Spec.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.PBKDF2Spec+Copyright   : (c) 2023 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3+-}+module HStratus.PBKDF2Spec (spec) where++import qualified Crypto.Hash.SHA1 as SHA1+import qualified Crypto.Hash.SHA256 as SHA256+import qualified Crypto.Hash.SHA512 as SHA512+import Data.Aeson+  ( FromJSON (..)+  , Object+  , Value+  , eitherDecodeFileStrict+  , withArray+  , withObject+  , (.:)+  )+import Data.Aeson.Types (Parser)+import Data.ByteString (ByteString)+import Data.ByteString.Base16 (decode)+import Data.Foldable (toList)+import Data.Text.Encoding (encodeUtf8)+import Data.Word (Word32, Word64, Word8)+import Network.HStratus.Internal.PBKDF2 (PseudoRandomF, deriveKey, wrap)+import Paths_hstratus_auth (getDataFileName)+import Test.Hspec (Spec, context, describe, it, runIO, shouldBe)+import Test.Hspec.Runner (SpecWith)+++spec :: Spec+spec = describe "module Network.HStratus.PBKDF2" $ do+  describe "deriveKey" $ do+    context "when using the wycheProof test cases" $ do+      specFrom "sha1" SHA1.hmac+      specFrom "sha256" SHA256.hmac+      specFrom "sha512" SHA512.hmac+++specFrom :: String -> PseudoRandomF -> Spec+specFrom shaName pseudo = do+  context ("with " ++ shaName ++ ".hmac as the pseudorandom function") $ do+    testData <- runIO $ namedDataPath shaName >>= loadTestData+    mapM_ (specWithFrom pseudo) testData+++specWithFrom :: PseudoRandomF -> TestDescription -> SpecWith ()+specWithFrom pseudo td = do+  let TestDescription{tdSalt, tdIterationCount = count, tdDerivedKey = key} = td+  it ("test " ++ show (tdId td) ++ " should succeed") $ do+    let pseudo' = wrap pseudo (tdDerivedLength td)+        calc x = deriveKey x (tdPassword td) tdSalt count+    calc <$> pseudo' `shouldBe` Right key+++loadTestData :: FilePath -> IO [TestDescription]+loadTestData src =+  let decodeDataFile aPath = fmap unTestFile <$> eitherDecodeFileStrict aPath+   in decodeDataFile src >>= either fail pure+++namedDataPath :: String -> IO FilePath+namedDataPath shaName =+  let path = "testdata/pbkdf2_hmac" ++ shaName ++ "_test.json"+   in getDataFileName path+++newtype TestFile = TestFile {unTestFile :: [TestDescription]}+++instance FromJSON TestFile where+  parseJSON = fmap TestFile . parseTestFile+++parseTestFile :: Value -> Parser [TestDescription]+parseTestFile =+  let oneTest = withObject "test" parseTestDescription+      manyTests = traverse oneTest . toList+      testsInTestGroup o = o .: "tests" >>= withArray "[test]" manyTests+      oneTestGroup = withObject "testsInTestGroup" testsInTestGroup+      manyTestGroups = traverse oneTestGroup . toList+      testGroupsAtTop o = o .: "testGroups" >>= withArray "[testGroups]" manyTestGroups+   in withObject "dataFile" (fmap concat . testGroupsAtTop)+++data TestDescription = TestDescription+  { tdId :: !Word8+  , tdPassword :: !ByteString+  , tdSalt :: !ByteString+  , tdIterationCount :: !Word64+  , tdDerivedLength :: !Word32+  , tdDerivedKey :: !ByteString+  }+  deriving (Eq, Show)+++parseTestDescription :: Object -> Parser TestDescription+parseTestDescription o = do+  let parseBase16Bytes = either fail pure . decode . encodeUtf8+  tdId <- o .: "tcId"+  tdPassword <- o .: "password" >>= parseBase16Bytes+  tdSalt <- o .: "salt" >>= parseBase16Bytes+  tdIterationCount <- o .: "iterationCount"+  tdDerivedLength <- o .: "dkLen"+  tdDerivedKey <- o .: "dk" >>= parseBase16Bytes+  pure+    TestDescription+      { tdId+      , tdPassword+      , tdSalt+      , tdIterationCount+      , tdDerivedKey+      , tdDerivedLength+      }
+ test/HStratus/SessionSpec.hs view
@@ -0,0 +1,530 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.SessionSpec+Copyright   : (c) 2023 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3+-}+module HStratus.SessionSpec (spec) where++import Control.Monad (when)+import Data.Aeson (decode, eitherDecodeFileStrict, encode, encodeFile, object, (.=))+import Data.Aeson.Types (parseJSON, parseMaybe)+import Data.Bits ((.&.))+import Data.Either (isLeft)+import Data.List (isInfixOf, sort)+import qualified Data.Map.Strict as Map+import Data.Maybe (fromMaybe)+import Data.String (IsString (..))+import Data.Text (Text)+import qualified Data.Text.IO as Text+import Data.Word (Word16)+import HStratus.TrustSpec (jsonKeysOf)+import Network.HStratus.Internal.Session+  ( SavedHeaders (..)+  , accountDataPath+  , accountDataRequires2FA+  , accountDataRequires2SA+  , appBase+  , checkSecureMode+  , clientIdPath+  , cookiePath+  , credentialsPath+  , encodeFileAtomic+  , loadAccountData+  , loadSavedHeaders+  , requireSecureFile+  , saveAccountData+  , saveCredentialsTo+  , savedHeadersPath+  , unknownAccountData+  , updateSessionSavedHeaders+  , (</>)+  )+import Network.HStratus.Session (AccountData (..), Credentials (..), Session (..), Webservice (..), loadSession)+import System.Directory (createDirectory, doesFileExist)+import System.Environment (setEnv)+import System.IO.Error (ioeGetErrorString)+import System.IO.Temp (withSystemTempDirectory)+import System.Posix.Files (fileMode, getFileStatus, setFileMode)+import Test.Hspec+  ( Spec+  , anyIOException+  , around+  , context+  , describe+  , it+  , shouldBe+  , shouldReturn+  , shouldSatisfy+  , shouldThrow+  )+import Test.QuickCheck+  ( Arbitrary (arbitrary)+  , Gen+  , Property+  , elements+  , frequency+  , listOf+  )+import Test.QuickCheck.Monadic (assert, monadicIO, pick, run)+++spec :: Spec+spec = do+  secureFileSpec+  secureWriteSpec+  checkSessionFilesSpec+  sessionSpec+  saveCredentialsSpec+  accountDataSpec+++secureFileSpec :: Spec+secureFileSpec = describe "module Network.HStratus.Internal.Session (file security)" $ do+  describe "checkSecureMode" $ do+    it "accepts mode 0o600" $+      checkSecureMode 0o600 "/tmp/test" `shouldBe` Right ()+    it "accepts mode 0o400 (read-only owner)" $+      checkSecureMode 0o400 "/tmp/test" `shouldBe` Right ()+    it "accepts mode 0o700 (owner-execute)" $+      checkSecureMode 0o700 "/tmp/test" `shouldBe` Right ()+    it "rejects mode 0o644 (world-readable)" $+      checkSecureMode 0o644 "/tmp/test" `shouldSatisfy` isLeft+    it "rejects mode 0o640 (group-readable)" $+      checkSecureMode 0o640 "/tmp/test" `shouldSatisfy` isLeft+    it "rejects mode 0o604 (world-readable, no group)" $+      checkSecureMode 0o604 "/tmp/test" `shouldSatisfy` isLeft+    it "includes the path in the error message" $+      case checkSecureMode 0o644 "/some/path" of+        Left msg -> "/some/path" `isInfixOf` msg `shouldBe` True+        Right () -> fail "expected Left"+    it "includes chmod hint in the error message" $+      case checkSecureMode 0o644 "/some/path" of+        Left msg -> "chmod 600" `isInfixOf` msg `shouldBe` True+        Right () -> fail "expected Left"+  describe "requireSecureFile" $ around withTmpDir $ do+    it "does nothing when the file is absent" $ \tmpDir -> do+      requireSecureFile (tmpDir </> "absent.txt")+    it "does nothing when the file has mode 0o600" $ \tmpDir -> do+      let path = tmpDir </> "secure.txt"+      writeFile path "content"+      setFileMode path 0o600+      requireSecureFile path+    it "throws when the file has mode 0o644" $ \tmpDir -> do+      let path = tmpDir </> "insecure.txt"+      writeFile path "content"+      setFileMode path 0o644+      requireSecureFile path `shouldThrow` anyIOException+    it "includes the path in the error for a bad-permission file" $ \tmpDir -> do+      let path = tmpDir </> "insecure.txt"+      writeFile path "content"+      setFileMode path 0o644+      requireSecureFile path+        `shouldThrow` (\e -> tmpDir `isInfixOf` ioeGetErrorString e)+++shouldHaveMode600 :: FilePath -> IO ()+shouldHaveMode600 path = do+  mode <- fileMode <$> getFileStatus path+  (mode .&. 0o777) `shouldBe` 0o600+++secureWriteSpec :: Spec+secureWriteSpec = describe "module Network.HStratus.Internal.Session (secure writes)" $ do+  context "saveCredentialsTo" $ around withTmpDir $ do+    it "writes credentials.json with mode 0o600" $ \tmpDir -> do+      saveCredentialsTo tmpDir exampleCred+      shouldHaveMode600 (credentialsPath tmpDir)+  context "updateSessionSavedHeaders" $ around useTmp $ do+    it "writes session headers with mode 0o600" $ \appRoot -> do+      saveCredentialsTo appRoot exampleCred+      s <- loadSession+      updateSessionSavedHeaders s id+      shouldHaveMode600 (savedHeadersPath appRoot (sessionCreds s))+  context "saveAccountData" $ around useTmp $ do+    it "writes account-data with mode 0o600" $ \appRoot -> do+      saveCredentialsTo appRoot exampleCred+      s <- loadSession+      saveAccountData s unknownAccountData+      shouldHaveMode600 (accountDataPath appRoot (sessionCreds s))+  context "loadClientId (new file)" $ around useTmp $ do+    it "creates client-id file with mode 0o600" $ \appRoot -> do+      saveCredentialsTo appRoot exampleCred+      s <- loadSession+      shouldHaveMode600 (clientIdPath appRoot (sessionCreds s))+++checkSessionFilesSpec :: Spec+checkSessionFilesSpec = describe "checkSessionFiles / loadSession" $ around useTmp $ do+  it "succeeds when credentials.json has mode 0o600" $ \appRoot -> do+    saveCredentialsTo appRoot exampleCred+    _ <- loadSession+    pure ()+  it "fails when credentials.json has mode 0o644" $ \appRoot -> do+    saveCredentialsTo appRoot exampleCred+    setFileMode (credentialsPath appRoot) 0o644+    loadSession `shouldThrow` anyIOException+  it "fails when session headers file has mode 0o644" $ \appRoot -> do+    saveCredentialsTo appRoot exampleCred+    s <- loadSession+    updateSessionSavedHeaders s id+    setFileMode (savedHeadersPath appRoot (sessionCreds s)) 0o644+    loadSession `shouldThrow` anyIOException+++-- save credential somewhere+-- confirm Session loads++-- save credentials+-- save a pre-existing clientId+-- loadSession; confirm loaded session as the clientId++-- save credentials+-- save generated SavedHeaders+-- loadSession; confirm saveHeaders are loaded++sessionSpec :: Spec+sessionSpec = describe "module Network.HStratus.Session" $ do+  context "Using an example Credential" $ do+    let topDir = "/tmp/icloud_authspec"+    context "cookiePath" $ do+      it "should be computed correctly" $ do+        let want = "/tmp/icloud_authspec/myaccountid-applecom.cookies.txt"+        cookiePath topDir exampleCred `shouldBe` want++    context "savedHeadersPath" $ do+      it "should be computed correctly" $ do+        let want = "/tmp/icloud_authspec/myaccountid-applecom.session.json"+        savedHeadersPath topDir exampleCred `shouldBe` want++    context "clientIdPath" $ do+      it "should be computed correctly" $ do+        let want = "/tmp/icloud_authspec/myaccountid-applecom.client-id.txt"+        clientIdPath topDir exampleCred `shouldBe` want+  savedHeadersFieldNamesSpec+  loadSessionSpec+  loadSavedHeadersSpec+  updateSessionSavedHeadersSpec+  encodeFileAtomicSpec+++savedHeadersFieldNamesSpec :: Spec+savedHeadersFieldNamesSpec = describe "SavedHeaders JSON field names" $ do+  it "uses the expected field names" $+    jsonKeysOf (SavedHeaders (Just "a") (Just "b") (Just "c") (Just "d") (Just "e"))+      `shouldBe` Just (sort ["country", "session_id", "session_token", "trust_token", "counter"])+++loadSessionSpec :: Spec+loadSessionSpec = describe "loadSession" $ around useTmp $ do+  context "with an invalid credentials file" $ do+    it "should fail to load" $ \appRoot ->+      failsOnBadCredentials appRoot `shouldThrow` anyIOException+  context "with only the credentials file" $ do+    it "should load a Session with a new clientId" prop_loadsSession+  context "with a clientId file available" $ do+    it "should load the saved clientId" prop_readsStoredCliendId+  context "with a saved headers file" $ do+    it "should load the saved headers" prop_readsStoredSavedHeaders+++loadSavedHeadersSpec :: Spec+loadSavedHeadersSpec = describe "loadSavedHeaders" $ around useTmp $ do+  context "with an invalid saved headers file" $ do+    it "should fail to load" $ \appRoot ->+      failsOnBadSavedHeaders appRoot `shouldThrow` anyIOException+    it "includes a re-login hint in the error message" $ \appRoot ->+      failsOnBadSavedHeaders appRoot+        `shouldThrow` (\e -> "hstratus auth login" `isInfixOf` ioeGetErrorString e)+++updateSessionSavedHeadersSpec :: Spec+updateSessionSavedHeadersSpec = describe "updateSessionSavedHeaders" $ around useTmp $ do+  context "when some SaveHeaders are already saved" $ do+    it "should update to the new headers" $ prop_updatesSavedHeaders True+  context "when No SaveHeaders have been saved" $ do+    it "should update to the new headers" $ prop_updatesSavedHeaders True+++useTmp :: (FilePath -> IO a) -> IO a+useTmp = withSystemTempDirectory "icloud-auth" . asConfigHome+++setupInvalid :: FilePath -> IO ()+setupInvalid path = Text.writeFile path "[}"+++failsOnBadCredentials :: FilePath -> IO Session+failsOnBadCredentials appRoot = do+  let path = credentialsPath appRoot+  setupInvalid path+  setFileMode path 0o600+  loadSession+++failsOnBadSavedHeaders :: FilePath -> IO SavedHeaders+failsOnBadSavedHeaders appRoot = do+  saveCredentialsTo appRoot exampleCred+  let shPath = savedHeadersPath appRoot exampleCred+  setupInvalid shPath+  setFileMode shPath 0o600+  s <- loadSession+  loadSavedHeaders s+++asConfigHome :: (FilePath -> IO a) -> FilePath -> IO a+asConfigHome action root = do+  setEnv "XDG_CONFIG_HOME" root+  let appRoot = root </> appBase+  createDirectory appRoot+  action appRoot+++prop_loadsSession :: FilePath -> Property+prop_loadsSession appRoot = monadicIO $ do+  preCreds <- pick genPreCredentials+  let creds = asCreds preCreds+  s <- run $ do+    saveCredentialsTo appRoot creds+    loadSession+  assert $ sessionClientId s /= "" && creds == sessionCreds s+++prop_readsStoredCliendId :: FilePath -> Property+prop_readsStoredCliendId appRoot = monadicIO $ do+  preCreds <- pick genPreCredentials+  fakeId <- pick $ genIndexedSuffix "client-id-"+  let creds = asCreds preCreds+  session <- run $ do+    saveCredentialsTo appRoot creds+    let cidPath = clientIdPath appRoot creds+    Text.writeFile cidPath fakeId+    setFileMode cidPath 0o600+    loadSession+  assert $ fakeId == sessionClientId session+++prop_readsStoredSavedHeaders :: FilePath -> Property+prop_readsStoredSavedHeaders appRoot = monadicIO $ do+  preCreds <- pick genPreCredentials+  savedHdrs <- pick genSaveHeaders+  let creds = asCreds preCreds+  savedHdrs' <- run $ do+    saveCredentialsTo appRoot creds+    s <- loadSession+    updateSessionSavedHeaders s (const savedHdrs)+    loadSavedHeaders s+  assert $ savedHdrs == savedHdrs'+++prop_updatesSavedHeaders :: Bool -> FilePath -> Property+prop_updatesSavedHeaders storeInitial appRoot = monadicIO $ do+  preCreds <- pick genPreCredentials+  savedHdrs <- pick genSaveHeaders+  newHdrs <- pick genSaveHeaders+  let creds = asCreds preCreds+  loadedHdrs <- run $ do+    saveCredentialsTo appRoot creds+    when storeInitial $ do+      let shPath = savedHeadersPath appRoot creds+      encodeFile shPath savedHdrs+      setFileMode shPath 0o600+    s <- loadSession+    updateSessionSavedHeaders s (const newHdrs)+    loadSavedHeaders s+  assert $ newHdrs == loadedHdrs+++saveCredentialsSpec :: Spec+saveCredentialsSpec = describe "saveCredentialsTo" $ do+  context "in a pre-existing directory" $ around withTmpDir $ do+    it "creates the credentials file" $ \tmpDir -> do+      saveCredentialsTo tmpDir exampleCred+      doesFileExist (credentialsPath tmpDir) `shouldReturn` True+    it "round-trips credentials through JSON" prop_saveLoadCredentials+    it "overwrites when called a second time" prop_overwritesCredentials+  context "when the target directory does not exist" $ do+    it "creates the directory and the file" $+      withSystemTempDirectory "icloud-auth-creds" $ \tmp -> do+        let target = tmp </> "new-subdir"+        saveCredentialsTo target exampleCred+        doesFileExist (credentialsPath target) `shouldReturn` True+++withTmpDir :: (FilePath -> IO a) -> IO a+withTmpDir = withSystemTempDirectory "icloud-auth-creds"+++prop_saveLoadCredentials :: FilePath -> Property+prop_saveLoadCredentials tmpDir = monadicIO $ do+  creds <- asCreds <$> pick genPreCredentials+  result <- run $ do+    saveCredentialsTo tmpDir creds+    eitherDecodeFileStrict (credentialsPath tmpDir)+  assert $ result == Right creds+++prop_overwritesCredentials :: FilePath -> Property+prop_overwritesCredentials tmpDir = monadicIO $ do+  creds1 <- asCreds <$> pick genPreCredentials+  creds2 <- asCreds <$> pick genPreCredentials+  result <- run $ do+    saveCredentialsTo tmpDir creds1+    saveCredentialsTo tmpDir creds2+    eitherDecodeFileStrict (credentialsPath tmpDir)+  assert $ result == Right creds2+++exampleCred :: Credentials+exampleCred =+  Credentials+    { credAccountName = "my-account-id@apple.com"+    , credPassword = "notasecret"+    }+++type PreCredentials = (Text, Text)+++asCreds :: PreCredentials -> Credentials+asCreds (credAccountName, credPassword) = Credentials{credAccountName, credPassword}+++genPreCredentials :: Gen PreCredentials+genPreCredentials =+  let mkId x = "account-" <> x <> "@apple.com"+   in (,) <$> genIndexedTemplate mkId <*> genIndexedSuffix "password-"+++genSaveHeaders :: Gen SavedHeaders+genSaveHeaders =+  let arb pre = frequency [(2, pure Nothing), (1, Just <$> genIndexedSuffix pre)]+   in SavedHeaders+        <$> arb "country-"+        <*> arb "session-id-"+        <*> arb "session-token-"+        <*> arb "trust-token-"+        <*> arb "counter="+++genWord16 :: Gen Word16+genWord16 = arbitrary+++genIndexedSuffix :: (Monoid a, IsString a) => a -> Gen a+genIndexedSuffix pre = genIndexedTemplate (pre <>)+++genIndexedTemplate :: (IsString a) => (a -> a) -> Gen a+genIndexedTemplate plate = plate . fromString . show <$> genWord16+++accountDataSpec :: Spec+accountDataSpec = describe "module Network.HStratus.Session (AccountData)" $ do+  context "AccountData" $ do+    it "round-trips through JSON encoding" prop_jsonRoundtripAccountData+  context "accountDataRequires2FA" $ do+    it "is True when hsaVersion == 2 and challenged" $+      accountDataRequires2FA (mkAccountData 2 True (Just False)) `shouldBe` True+    it "is True when hsaVersion == 2, not challenged, but browser explicitly untrusted" $+      accountDataRequires2FA (mkAccountData 2 False (Just False)) `shouldBe` True+    it "is False when hsaVersion == 2, not challenged, and browser trusted" $+      accountDataRequires2FA (mkAccountData 2 False (Just True)) `shouldBe` False+    it "is False when hsaVersion == 2, not challenged, and hsaTrustedBrowser absent" $+      accountDataRequires2FA (mkAccountData 2 False Nothing) `shouldBe` False+    it "is False when hsaVersion is 1" $+      accountDataRequires2FA (mkAccountData 1 True (Just False)) `shouldBe` False+    it "is False when hsaVersion is 3 (unknown version)" $+      accountDataRequires2FA (mkAccountData 3 True (Just False)) `shouldBe` False+  context "accountDataRequires2SA" $ do+    it "is True when hsaVersion is 1" $+      accountDataRequires2SA (mkAccountData 1 False (Just False)) `shouldBe` True+    it "is False when hsaVersion is 2" $+      accountDataRequires2SA (mkAccountData 2 False (Just False)) `shouldBe` False+    it "is False when hsaVersion is 0" $+      accountDataRequires2SA (mkAccountData 0 False (Just False)) `shouldBe` False+  context "AccountData JSON parsing" $ do+    it "fails to parse from null JSON" $+      (decode "null" :: Maybe AccountData) `shouldBe` Nothing+    it "fails to parse when dsInfo is absent" $+      (decode "{}" :: Maybe AccountData) `shouldBe` Nothing+  context "saveAccountData / loadAccountData" $ around useTmp $ do+    it "round-trips in a temp directory" prop_saveLoadAccountData+++prop_jsonRoundtripAccountData :: Property+prop_jsonRoundtripAccountData = monadicIO $ do+  ad <- pick genAccountData+  assert $ decode (encode ad) == Just ad+++prop_saveLoadAccountData :: FilePath -> Property+prop_saveLoadAccountData appRoot = monadicIO $ do+  preCreds <- pick genPreCredentials+  ad <- pick genAccountData+  let creds = asCreds preCreds+  loaded <- run $ do+    saveCredentialsTo appRoot creds+    s <- loadSession+    saveAccountData s ad+    loadAccountData s+  assert $ Just ad == loaded+++mkAccountData :: Int -> Bool -> Maybe Bool -> AccountData+mkAccountData ver challenged trusted =+  AccountData+    { adHsaVersion = ver+    , adHsaChallengeRequired = challenged+    , adHsaTrustedBrowser = trusted+    , adWebservices = Map.empty+    , adRaw = object []+    }+++genAccountData :: Gen AccountData+genAccountData = do+  adHsaVersion <- abs <$> (arbitrary :: Gen Int)+  adHsaChallengeRequired <- (arbitrary :: Gen Bool)+  adHsaTrustedBrowser <- elements [Nothing, Just True, Just False]+  adWebservices <- Map.fromList <$> listOf genWsPair+  let trustedField = maybe [] (\t -> ["hsaTrustedBrowser" .= t]) adHsaTrustedBrowser+      v =+        object $+          [ "dsInfo" .= object ["hsaVersion" .= adHsaVersion]+          , "hsaChallengeRequired" .= adHsaChallengeRequired+          , "webservices"+              .= fmap+                (\(Webservice url st) -> object $ ["url" .= url] <> maybe [] (\s -> ["status" .= s]) st)+                adWebservices+          ]+            <> trustedField+  pure $ fromMaybe unknownAccountData (parseMaybe parseJSON v)+ where+  genWsPair :: Gen (Text, Webservice)+  genWsPair = (,) <$> elements wsNames <*> genWebservice+  genWebservice :: Gen Webservice+  genWebservice = Webservice <$> genIndexedSuffix "https://example.com/" <*> elements [Nothing, Just "active", Just "inactive"]+  wsNames = ["findme", "contacts", "calendar", "mail"]+++encodeFileAtomicSpec :: Spec+encodeFileAtomicSpec = describe "encodeFileAtomic" $ around useTmp $ do+  it "round-trips a JSON value" $ \appRoot -> do+    let path = appRoot </> "test.json"+        value = object ["key" .= ("value" :: Text)]+    encodeFileAtomic path value+    result <- eitherDecodeFileStrict path+    result `shouldBe` Right value+  it "overwrites an existing file" $ \appRoot -> do+    let path = appRoot </> "test.json"+        old = object ["version" .= (1 :: Int)]+        new = object ["version" .= (2 :: Int)]+    encodeFileAtomic path old+    encodeFileAtomic path new+    result <- eitherDecodeFileStrict path+    result `shouldBe` Right new
+ test/HStratus/TrustSpec.hs view
@@ -0,0 +1,263 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : HStratus.TrustSpec+Copyright   : (c) 2023 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3+-}+module HStratus.TrustSpec (spec, encode, genTrustData, genTrustedList, jsonKeysOf) where++import Data.Aeson (Key, ToJSON (..), Value (..), decode, eitherDecodeFileStrict, encode)+import Data.Aeson.KeyMap (fromList)+import qualified Data.Aeson.KeyMap as KeyMap+import Data.List (sort)+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE+import Data.Maybe (catMaybes)+import Data.String.Conv (toS)+import Data.Text (Text)+import qualified HStratus.Examples as Examples+import Network.HStratus.Internal.Trust+import Paths_hstratus_auth (getDataFileName)+import System.IO.Silently (silence)+import Test.Hspec+  ( Spec+  , context+  , describe+  , it+  , shouldBe+  )+import Test.Hspec.Benri (endsJust, endsNothing, endsRight)+import Test.Main (withStdin)+import Test.QuickCheck+  ( Arbitrary (arbitrary)+  , Gen+  , Property+  , chooseInt+  , elements+  , forAll+  , frequency+  , listOf1+  , vectorOf+  )+import Test.QuickCheck.Monadic (assert, monadicIO, pick, run)+++spec :: Spec+spec = describe "module Network.HStratus.Trust" $ do+  describe "TrustData" $ do+    context "parsing generated examples to/from JSON" $ do+      it "should succeed" prop_jsonRoundtripTrustData+    context "parsing a hand-crafted Apple-shaped fixture" $ do+      it "should succeed" $ do+        fp <- getDataFileName "testdata/trust_data_test.json"+        eitherDecodeFileStrict fp `endsRight` expectedTrustData+  describe "CodeStatus JSON field names" $ do+    it "uses the server field names" $+      jsonKeysOf (CodeStatus 6 False False False False)+        `shouldBe` Just (sort ["length", "tooManyCodesSent", "tooManyCodesValidated", "securityCodeLocked", "securityCodeCooldown"])+  describe "CodeStatus JSON parsing" $ do+    it "defaults all boolean fields to False when absent" $+      decode "{\"length\":6}" `shouldBe` Just (CodeStatus 6 False False False False)+  describe "TrustedPhone JSON field names" $ do+    it "uses the server field names" $+      jsonKeysOf (TrustedPhone 1 "+81 test" (Just "sms"))+        `shouldBe` Just (sort ["id", "numberWithDialCode", "pushMode"])+  describe "TrustedDevice JSON field names" $ do+    it "uses the server field names" $+      jsonKeysOf (TrustedDevice "id1" "iPhone" "iPhone14")+        `shouldBe` Just (sort ["id", "name", "modelName"])+  describe "TrustedDevice JSON parsing" $ do+    it "defaults modelName to empty string when absent" $+      decode "{\"id\":\"1\",\"name\":\"iPhone\"}" `shouldBe` Just (TrustedDevice "1" "iPhone" "")++  describe "Setup2SADevice" $ do+    context "parsing generated examples to/from JSON" $ do+      it "should succeed" prop_jsonRoundtripSetup2SADevice++  describe "setup2SADeviceLabel" $ do+    it "returns phoneNumber when present" $+      setup2SADeviceLabel (mkDevice [("phoneNumber", String "+1234")]) `shouldBe` "+1234"+    it "returns name when phoneNumber is absent" $+      setup2SADeviceLabel (mkDevice [("name", String "iPhone")]) `shouldBe` "iPhone"+    it "prefers phoneNumber over name" $+      setup2SADeviceLabel (mkDevice [("phoneNumber", String "+1234"), ("name", String "iPhone")]) `shouldBe` "+1234"+    it "returns (unknown) when neither field is present" $+      setup2SADeviceLabel (mkDevice [("deviceId", String "abc")]) `shouldBe` "(unknown)"++  describe "selectPhone" $ do+    context "when the selected input" $ do+      context "is a number within the range" $ do+        it "should succeed" (prop_selectsWithNonMaxIndex selectPhone genTrustedPhone)+      context "is the maximum number" $ do+        it "should succeed" (prop_selectsWithMaxIndex selectPhone genTrustedPhone)++  describe "selectDevice" $ do+    context "when the selected input" $ do+      context "is a number within the range" $ do+        it "should succeed" (prop_selectsWithNonMaxIndex selectDevice genTrustedDevice)+      context "is the maximum number" $ do+        it "should succeed" (prop_selectsWithMaxIndex selectDevice genTrustedDevice)++  describe "selectSetupDevice" $ do+    context "when the selected input" $ do+      context "is a number within the range" $ do+        it "should succeed" (prop_selectsWithNonMaxIndex selectSetupDevice genSetup2SADevice)+      context "is the maximum number" $ do+        it "should succeed" (prop_selectsWithMaxIndex selectSetupDevice genSetup2SADevice)++  describe "selectTwoFaPhone" $ do+    context "when noTrustedDevices is True" $ do+      it "returns the first phone without prompting" $+        silence (selectTwoFaPhone (mkTrustData True [twoFaPhone1, twoFaPhone2]))+          `endsJust` twoFaPhone1+      it "returns Nothing when the phone list is empty" $+        endsNothing $+          silence (selectTwoFaPhone (mkTrustData True []))+    context "when noTrustedDevices is False" $ do+      context "and the phone list is empty" $ do+        it "returns Nothing without prompting" $+          endsNothing $+            silence (selectTwoFaPhone (mkTrustData False []))+      context "and the user presses Enter" $ do+        it "returns Nothing" $+          withStdin "\n" $+            endsNothing $+              silence (selectTwoFaPhone (mkTrustData False [twoFaPhone1]))+      context "and the user enters a valid index" $ do+        it "returns the selected phone" $+          withStdin "2" $+            silence (selectTwoFaPhone (mkTrustData False [twoFaPhone1, twoFaPhone2]))+              `endsJust` twoFaPhone2+      context "and the user first enters an invalid index" $ do+        it "retries and returns the selected phone" $+          withStdin (toS ("99\n1" :: String)) $+            silence (selectTwoFaPhone (mkTrustData False [twoFaPhone1, twoFaPhone2]))+              `endsJust` twoFaPhone1+++prop_jsonRoundtripTrustData :: Property+prop_jsonRoundtripTrustData = forAll genTrustData $ \td ->+  decode (encode td) == Just td+++prop_jsonRoundtripSetup2SADevice :: Property+prop_jsonRoundtripSetup2SADevice = forAll genSetup2SADevice $ \d ->+  decode (encode d) == Just d+++mkDevice :: [(Key, Value)] -> Setup2SADevice+mkDevice = Setup2SADevice . fromList+++genSetup2SADevice :: Gen Setup2SADevice+genSetup2SADevice = do+  phone <- genExWordMaybe+  devId <- genExWord+  let pairs =+        catMaybes+          [ Just ("deviceId", String devId)+          , fmap (\p -> ("phoneNumber", String p)) phone+          ]+  pure $ Setup2SADevice $ fromList pairs+++genCodeStatus :: Gen CodeStatus+genCodeStatus =+  CodeStatus+    <$> arbitrary+    <*> arbitrary+    <*> arbitrary+    <*> arbitrary+    <*> arbitrary+++genTrustedPhone :: Gen TrustedPhone+genTrustedPhone = TrustedPhone <$> arbitrary <*> genExWord <*> genExWordMaybe+++genTrustedDevice :: Gen TrustedDevice+genTrustedDevice = TrustedDevice <$> genExWord <*> genExWord <*> genExWord+++genTrustedList :: Gen TrustedList+genTrustedList =+  frequency+    [ (1, TrustedPhoneNumbers . NE.fromList <$> listOf1 genTrustedPhone)+    , (1, TrustedDevices . NE.fromList <$> listOf1 genTrustedDevice)+    ]+++twoFaPhone1 :: TrustedPhone+twoFaPhone1 = TrustedPhone 1 "+81 test-1" (Just "sms")+++twoFaPhone2 :: TrustedPhone+twoFaPhone2 = TrustedPhone 2 "+1 test-2" Nothing+++mkTrustData :: Bool -> [TrustedPhone] -> TrustData+mkTrustData noDevices phones =+  TrustData+    { tdList = case NE.nonEmpty phones of+        Just nep -> TrustedPhoneNumbers nep+        Nothing -> TrustedDevices (TrustedDevice "" "" "" :| [])+    , tdSecurityCode = CodeStatus 6 False False False False+    , tdNoTrustedDevices = noDevices+    }+++useIOSelector :: (Eq a) => (NonEmpty a -> IO a) -> (Int, a, NonEmpty a) -> IO Bool+useIOSelector selector (idx, want, xs) = do+  withStdin (toS $ show idx) $ do+    selected <- silence $ selector xs+    pure $ selected == want+++prop_selectsWithNonMaxIndex :: (Eq a, Show a) => (NonEmpty a -> IO a) -> Gen a -> Property+prop_selectsWithNonMaxIndex selector generator = monadicIO $ do+  pick (genWithNonMaxIndex generator) >>= run . useIOSelector selector >>= assert+++prop_selectsWithMaxIndex :: (Eq a, Show a) => (NonEmpty a -> IO a) -> Gen a -> Property+prop_selectsWithMaxIndex selector generator = monadicIO $ do+  let useMax (_ignoredIndex, _ignoredSelection, xs) = (NE.length xs, NE.last xs, xs)+      withNonMax = genWithNonMaxIndex generator+  pick (fmap useMax withNonMax) >>= run . useIOSelector selector >>= assert+++genWithNonMaxIndex :: Gen a -> Gen (Int, a, NonEmpty a)+genWithNonMaxIndex sourceGen = do+  low <- chooseInt (1, 5)+  high <- chooseInt (low, 10)+  xs <- NE.fromList <$> vectorOf high sourceGen -- vectorOf high always gives exactly high elements, high >= 1+  pure (low, NE.toList xs !! (low - 1), xs) -- low in [1..high], so in-bounds+++genTrustData :: Gen TrustData+genTrustData = TrustData <$> genTrustedList <*> genCodeStatus <*> arbitrary+++genExWord :: Gen Text+genExWord = elements Examples.wordz+++genExWordMaybe :: Gen (Maybe Text)+genExWordMaybe = frequency [(1, pure Nothing), (1, Just <$> genExWord)]+++jsonKeysOf :: (ToJSON a) => a -> Maybe [Key]+jsonKeysOf x = case toJSON x of+  Object o -> Just (sort $ KeyMap.keys o)+  _ -> Nothing+++expectedTrustData :: TrustData+expectedTrustData =+  TrustData+    { tdList = TrustedPhoneNumbers (TrustedPhone 1 "+81 \x2022\x2022 \x2022\x2022\x2022\x2022 \x2022\&34" (Just "sms") :| [])+    , tdSecurityCode = CodeStatus 6 False False False False+    , tdNoTrustedDevices = False+    }
+ test/Spec.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE ScopedTypeVariables #-}++{- |+Module      : Main+Copyright   : (c) 2026 Tim Emiola+Maintainer  : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++Test suite entry point for hstratus-auth.+-}+module Main where++import qualified HStratus.ApiLoggerSpec as ApiLogger+import qualified HStratus.Http.CliSpec as HttpCli+import qualified HStratus.Http.EndpointsSpec as HttpEndpoints+import qualified HStratus.Http.ErrorsSpec as HttpErrors+import qualified HStratus.Http.HeadersSpec as HttpHeaders+import qualified HStratus.HttpMockSpec as HttpMock+import qualified HStratus.HttpSpec as Http+import qualified HStratus.LoginFSMSpec as LoginFSM+import qualified HStratus.PBKDF2Spec as PBKDF2+import qualified HStratus.SessionSpec as Session+import qualified HStratus.TrustSpec as Trust+import System.IO+  ( BufferMode (..)+  , hSetBuffering+  , stderr+  , stdout+  )+import Test.Hspec+++main :: IO ()+main = do+  hSetBuffering stdout NoBuffering+  hSetBuffering stderr NoBuffering+  hspec $ do+    Session.spec+    Http.spec+    ApiLogger.spec+    HttpCli.spec+    HttpEndpoints.spec+    HttpErrors.spec+    HttpHeaders.spec+    HttpMock.spec+    PBKDF2.spec+    Trust.spec+    LoginFSM.spec
+ testdata/auth_ok_test.json view
@@ -0,0 +1,3 @@+{+  "authType": "hsa2"+}
+ testdata/login_2fa_test.json view
@@ -0,0 +1,244 @@+{+  "dsInfo": {+    "lastName": "TARANTINO",+    "iCDPEnabled": false,+    "tantorMigrated": true,+    "dsid": "quentintarantino",+    "hsaEnabled": true,+    "ironcadeMigrated": true,+    "locale": "fr-fr_FR",+    "brZoneConsolidated": false,+    "isManagedAppleID": false,+    "gilligan-invited": "true",+    "appleIdAliases": [+      "quentintarantino@me.com",+      "quentintarantino@icloud.com"+    ],+    "hsaVersion": 2,+    "isPaidDeveloper": false,+    "countryCode": "FRA",+    "notificationId": "12345678-1234-1234-1234-123456789012quentintarantino",+    "primaryEmailVerified": true,+    "aDsID": "123456-12-12345678-1234-1234-1234-123456789012quentintarantino",+    "locked": false,+    "hasICloudQualifyingDevice": true,+    "primaryEmail": "quentintarantino@hotmail.fr",+    "appleIdEntries": [+      {+        "isPrimary": true,+        "type": "EMAIL",+        "value": "quentintarantino@hotmail.fr"+      },+      {+        "type": "EMAIL",+        "value": "quentintarantino@me.com"+      },+      {+        "type": "EMAIL",+        "value": "quentintarantino@icloud.com"+      }+    ],+    "gilligan-enabled": "true",+    "fullName": "Quentin TARANTINO",+    "languageCode": "fr-fr",+    "appleId": "quentintarantino@hotmail.fr",+    "firstName": "Quentin",+    "iCloudAppleIdAlias": "quentintarantino@icloud.com",+    "notesMigrated": true,+    "hasPaymentInfo": true,+    "pcsDeleted": false,+    "appleIdAlias": "quentintarantino@me.com",+    "brMigrated": true,+    "statusCode": 2,+    "familyEligible": true+  },+  "hasMinimumDeviceForPhotosWeb": true,+  "iCDPEnabled": false,+  "webservices": {+    "reminders": {+      "url": "https://p31-remindersws.icloud.com:443",+      "status": "active"+    },+    "notes": {+      "url": "https://p38-notesws.icloud.com:443",+      "status": "active"+    },+    "mail": {+      "url": "https://p38-mailws.icloud.com:443",+      "status": "active"+    },+    "ckdatabasews": {+      "pcsRequired": true,+      "url": "https://p31-ckdatabasews.icloud.com:443",+      "status": "active"+    },+    "photosupload": {+      "pcsRequired": true,+      "url": "https://p31-uploadphotosws.icloud.com:443",+      "status": "active"+    },+    "photos": {+      "pcsRequired": true,+      "uploadUrl": "https://p31-uploadphotosws.icloud.com:443",+      "url": "https://p31-photosws.icloud.com:443",+      "status": "active"+    },+    "drivews": {+      "pcsRequired": true,+      "url": "https://p31-drivews.icloud.com:443",+      "status": "active"+    },+    "uploadimagews": {+      "url": "https://p31-uploadimagews.icloud.com:443",+      "status": "active"+    },+    "schoolwork": {},+    "cksharews": {+      "url": "https://p31-ckshare.icloud.com:443",+      "status": "active"+    },+    "findme": {+      "url": "https://p31-fmipweb.icloud.com:443",+      "status": "active"+    },+    "ckdeviceservice": {+      "url": "https://p31-ckdevice.icloud.com:443"+    },+    "iworkthumbnailws": {+      "url": "https://p31-iworkthumbnailws.icloud.com:443",+      "status": "active"+    },+    "calendar": {+      "url": "https://p31-calendarws.icloud.com:443",+      "status": "active"+    },+    "docws": {+      "pcsRequired": true,+      "url": "https://p31-docws.icloud.com:443",+      "status": "active"+    },+    "settings": {+      "url": "https://p31-settingsws.icloud.com:443",+      "status": "active"+    },+    "streams": {+      "url": "https://p31-streams.icloud.com:443",+      "status": "active"+    },+    "keyvalue": {+      "url": "https://p31-keyvalueservice.icloud.com:443",+      "status": "active"+    },+    "archivews": {+      "url": "https://p31-archivews.icloud.com:443",+      "status": "active"+    },+    "push": {+      "url": "https://p31-pushws.icloud.com:443",+      "status": "active"+    },+    "iwmb": {+      "url": "https://p31-iwmb.icloud.com:443",+      "status": "active"+    },+    "iworkexportws": {+      "url": "https://p31-iworkexportws.icloud.com:443",+      "status": "active"+    },+    "geows": {+      "url": "https://p31-geows.icloud.com:443",+      "status": "active"+    },+    "account": {+      "iCloudEnv": {+        "shortId": "p",+        "vipSuffix": "prod"+      },+      "url": "https://p31-setup.icloud.com:443",+      "status": "active"+    },+    "fmf": {+      "url": "https://p31-fmfweb.icloud.com:443",+      "status": "active"+    },+    "contacts": {+      "url": "https://p31-contactsws.icloud.com:443",+      "status": "active"+    }+  },+  "pcsEnabled": true,+  "configBag": {+    "urls": {+      "accountCreateUI": "https://appleid.apple.com/widget/account/?widgetKey=widget_keyquentintarantino#!create",+      "accountLoginUI": "https://idmsa.apple.com/appleauth/auth/signin?widgetKey=widget_keyquentintarantino",+      "accountLogin": "https://setup.icloud.com/setup/ws/1/accountLogin",+      "accountRepairUI": "https://appleid.apple.com/widget/account/?widgetKey=widget_keyquentintarantino#!repair",+      "downloadICloudTerms": "https://setup.icloud.com/setup/ws/1/downloadLiteTerms",+      "repairDone": "https://setup.icloud.com/setup/ws/1/repairDone",+      "accountAuthorizeUI": "https://idmsa.apple.com/appleauth/auth/authorize/signin?client_id=widget_keyquentintarantino",+      "vettingUrlForEmail": "https://id.apple.com/IDMSEmailVetting/vetShareEmail",+      "accountCreate": "https://setup.icloud.com/setup/ws/1/createLiteAccount",+      "getICloudTerms": "https://setup.icloud.com/setup/ws/1/getTerms",+      "vettingUrlForPhone": "https://id.apple.com/IDMSEmailVetting/vetSharePhone"+    },+    "accountCreateEnabled": "true"+  },+  "hsaTrustedBrowser": false,+  "appsOrder": [+    "mail",+    "contacts",+    "calendar",+    "photos",+    "iclouddrive",+    "notes3",+    "reminders",+    "pages",+    "numbers",+    "keynote",+    "newspublisher",+    "fmf",+    "find",+    "settings"+  ],+  "version": 2,+  "isExtendedLogin": true,+  "pcsServiceIdentitiesIncluded": false,+  "hsaChallengeRequired": true,+  "requestInfo": {+    "country": "FR",+    "timeZone": "GMT+1",+    "region": "IDF"+  },+  "pcsDeleted": false,+  "iCloudInfo": {+    "SafariBookmarksHasMigratedToCloudKit": true+  },+  "apps": {+    "calendar": {},+    "reminders": {},+    "keynote": {+      "isQualifiedForBeta": true+    },+    "settings": {+      "canLaunchWithOneFactor": true+    },+    "mail": {},+    "numbers": {+      "isQualifiedForBeta": true+    },+    "photos": {},+    "pages": {+      "isQualifiedForBeta": true+    },+    "notes3": {},+    "find": {+      "canLaunchWithOneFactor": true+    },+    "iclouddrive": {},+    "newspublisher": {+      "isHidden": true+    },+    "fmf": {},+    "contacts": {}+  }+}
+ testdata/login_2sa_test.json view
@@ -0,0 +1,6 @@+{+  "dsInfo": {"hsaVersion": 1},+  "hsaChallengeRequired": false,+  "hsaTrustedBrowser": false,+  "webservices": {}+}
+ testdata/login_working_test.json view
@@ -0,0 +1,244 @@+{+  "dsInfo": {+    "lastName": "TARANTINO",+    "iCDPEnabled": false,+    "tantorMigrated": true,+    "dsid": "quentintarantino",+    "hsaEnabled": true,+    "ironcadeMigrated": true,+    "locale": "fr-fr_FR",+    "brZoneConsolidated": false,+    "isManagedAppleID": false,+    "gilligan-invited": "true",+    "appleIdAliases": [+      "quentintarantino@me.com",+      "quentintarantino@icloud.com"+    ],+    "hsaVersion": 2,+    "isPaidDeveloper": false,+    "countryCode": "FRA",+    "notificationId": "12345678-1234-1234-1234-123456789012quentintarantino",+    "primaryEmailVerified": true,+    "aDsID": "123456-12-12345678-1234-1234-1234-123456789012quentintarantino",+    "locked": false,+    "hasICloudQualifyingDevice": true,+    "primaryEmail": "quentintarantino@hotmail.fr",+    "appleIdEntries": [+      {+        "isPrimary": true,+        "type": "EMAIL",+        "value": "quentintarantino@hotmail.fr"+      },+      {+        "type": "EMAIL",+        "value": "quentintarantino@me.com"+      },+      {+        "type": "EMAIL",+        "value": "quentintarantino@icloud.com"+      }+    ],+    "gilligan-enabled": "true",+    "fullName": "Quentin TARANTINO",+    "languageCode": "fr-fr",+    "appleId": "quentintarantino@hotmail.fr",+    "firstName": "Quentin",+    "iCloudAppleIdAlias": "quentintarantino@icloud.com",+    "notesMigrated": true,+    "hasPaymentInfo": false,+    "pcsDeleted": false,+    "appleIdAlias": "quentintarantino@me.com",+    "brMigrated": true,+    "statusCode": 2,+    "familyEligible": true+  },+  "hasMinimumDeviceForPhotosWeb": true,+  "iCDPEnabled": false,+  "webservices": {+    "reminders": {+      "url": "https://p31-remindersws.icloud.com:443",+      "status": "active"+    },+    "notes": {+      "url": "https://p38-notesws.icloud.com:443",+      "status": "active"+    },+    "mail": {+      "url": "https://p38-mailws.icloud.com:443",+      "status": "active"+    },+    "ckdatabasews": {+      "pcsRequired": true,+      "url": "https://p31-ckdatabasews.icloud.com:443",+      "status": "active"+    },+    "photosupload": {+      "pcsRequired": true,+      "url": "https://p31-uploadphotosws.icloud.com:443",+      "status": "active"+    },+    "photos": {+      "pcsRequired": true,+      "uploadUrl": "https://p31-uploadphotosws.icloud.com:443",+      "url": "https://p31-photosws.icloud.com:443",+      "status": "active"+    },+    "drivews": {+      "pcsRequired": true,+      "url": "https://p31-drivews.icloud.com:443",+      "status": "active"+    },+    "uploadimagews": {+      "url": "https://p31-uploadimagews.icloud.com:443",+      "status": "active"+    },+    "schoolwork": {},+    "cksharews": {+      "url": "https://p31-ckshare.icloud.com:443",+      "status": "active"+    },+    "findme": {+      "url": "https://p31-fmipweb.icloud.com:443",+      "status": "active"+    },+    "ckdeviceservice": {+      "url": "https://p31-ckdevice.icloud.com:443"+    },+    "iworkthumbnailws": {+      "url": "https://p31-iworkthumbnailws.icloud.com:443",+      "status": "active"+    },+    "calendar": {+      "url": "https://p31-calendarws.icloud.com:443",+      "status": "active"+    },+    "docws": {+      "pcsRequired": true,+      "url": "https://p31-docws.icloud.com:443",+      "status": "active"+    },+    "settings": {+      "url": "https://p31-settingsws.icloud.com:443",+      "status": "active"+    },+    "streams": {+      "url": "https://p31-streams.icloud.com:443",+      "status": "active"+    },+    "keyvalue": {+      "url": "https://p31-keyvalueservice.icloud.com:443",+      "status": "active"+    },+    "archivews": {+      "url": "https://p31-archivews.icloud.com:443",+      "status": "active"+    },+    "push": {+      "url": "https://p31-pushws.icloud.com:443",+      "status": "active"+    },+    "iwmb": {+      "url": "https://p31-iwmb.icloud.com:443",+      "status": "active"+    },+    "iworkexportws": {+      "url": "https://p31-iworkexportws.icloud.com:443",+      "status": "active"+    },+    "geows": {+      "url": "https://p31-geows.icloud.com:443",+      "status": "active"+    },+    "account": {+      "iCloudEnv": {+        "shortId": "p",+        "vipSuffix": "prod"+      },+      "url": "https://p31-setup.icloud.com:443",+      "status": "active"+    },+    "fmf": {+      "url": "https://p31-fmfweb.icloud.com:443",+      "status": "active"+    },+    "contacts": {+      "url": "https://p31-contactsws.icloud.com:443",+      "status": "active"+    }+  },+  "pcsEnabled": true,+  "configBag": {+    "urls": {+      "accountCreateUI": "https://appleid.apple.com/widget/account/?widgetKey=widget_keyquentintarantino#!create",+      "accountLoginUI": "https://idmsa.apple.com/appleauth/auth/signin?widgetKey=widget_keyquentintarantino",+      "accountLogin": "https://setup.icloud.com/setup/ws/1/accountLogin",+      "accountRepairUI": "https://appleid.apple.com/widget/account/?widgetKey=widget_keyquentintarantino#!repair",+      "downloadICloudTerms": "https://setup.icloud.com/setup/ws/1/downloadLiteTerms",+      "repairDone": "https://setup.icloud.com/setup/ws/1/repairDone",+      "accountAuthorizeUI": "https://idmsa.apple.com/appleauth/auth/authorize/signin?client_id=widget_keyquentintarantino",+      "vettingUrlForEmail": "https://id.apple.com/IDMSEmailVetting/vetShareEmail",+      "accountCreate": "https://setup.icloud.com/setup/ws/1/createLiteAccount",+      "getICloudTerms": "https://setup.icloud.com/setup/ws/1/getTerms",+      "vettingUrlForPhone": "https://id.apple.com/IDMSEmailVetting/vetSharePhone"+    },+    "accountCreateEnabled": "true"+  },+  "hsaTrustedBrowser": true,+  "appsOrder": [+    "mail",+    "contacts",+    "calendar",+    "photos",+    "iclouddrive",+    "notes3",+    "reminders",+    "pages",+    "numbers",+    "keynote",+    "newspublisher",+    "fmf",+    "find",+    "settings"+  ],+  "version": 2,+  "isExtendedLogin": true,+  "pcsServiceIdentitiesIncluded": true,+  "hsaChallengeRequired": false,+  "requestInfo": {+    "country": "FR",+    "timeZone": "GMT+1",+    "region": "IDF"+  },+  "pcsDeleted": false,+  "iCloudInfo": {+    "SafariBookmarksHasMigratedToCloudKit": true+  },+  "apps": {+    "calendar": {},+    "reminders": {},+    "keynote": {+      "isQualifiedForBeta": true+    },+    "settings": {+      "canLaunchWithOneFactor": true+    },+    "mail": {},+    "numbers": {+      "isQualifiedForBeta": true+    },+    "photos": {},+    "pages": {+      "isQualifiedForBeta": true+    },+    "notes3": {},+    "find": {+      "canLaunchWithOneFactor": true+    },+    "iclouddrive": {},+    "newspublisher": {+      "isHidden": true+    },+    "fmf": {},+    "contacts": {}+  }+}
+ testdata/pbkdf2_hmacsha1_test.json view
@@ -0,0 +1,937 @@+{+  "algorithm" : "PBKDF2-HMACSHA1",+  "schema" : "pbkdf_test_schema.json",+  "generatorVersion" : "0.9",+  "numberOfTests" : 64,+  "header" : [+    "Test vector of type PbkdfTest are for password based key derivations."+  ],+  "notes" : {+    "Ascii" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of ASCII characters."+    },+    "LargeIterationCount" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a high iteration count"+    },+    "NonUtf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is not a valid UTF-8 string."+    },+    "Printable" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of printable ASCII characters."+    },+    "Pseudorandom" : {+      "bugType" : "BASIC",+      "description" : "Pseudorandomly generated test vector"+    },+    "Rfc6070" : {+      "bugType" : "BASIC",+      "description" : "Known test vector from RFC 6070"+    },+    "Utf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is a valid UTF-8 string."+    }+  },+  "testGroups" : [+    {+      "type" : "PbkdfTest",+      "tests" : [+        {+          "tcId" : 1,+          "comment" : "RFC 6070",+          "flags" : [+            "Rfc6070",+            "Printable"+          ],+          "password" : "70617373776f7264",+          "salt" : "73616c74",+          "iterationCount" : 1,+          "dkLen" : 20,+          "dk" : "0c60c80f961f0e71f3a9b524af6012062fe037a6",+          "result" : "valid"+        },+        {+          "tcId" : 2,+          "comment" : "RFC 6070",+          "flags" : [+            "Rfc6070",+            "Printable"+          ],+          "password" : "70617373776f7264",+          "salt" : "73616c74",+          "iterationCount" : 2,+          "dkLen" : 20,+          "dk" : "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957",+          "result" : "valid"+        },+        {+          "tcId" : 3,+          "comment" : "RFC 6070",+          "flags" : [+            "Rfc6070",+            "Printable"+          ],+          "password" : "70617373776f7264",+          "salt" : "73616c74",+          "iterationCount" : 4096,+          "dkLen" : 20,+          "dk" : "4b007901b765489abead49d926f721d065a429c1",+          "result" : "valid"+        },+        {+          "tcId" : 4,+          "comment" : "RFC 6070",+          "flags" : [+            "Rfc6070",+            "LargeIterationCount",+            "Printable"+          ],+          "password" : "70617373776f7264",+          "salt" : "73616c74",+          "iterationCount" : 16777216,+          "dkLen" : 20,+          "dk" : "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984",+          "result" : "valid"+        },+        {+          "tcId" : 5,+          "comment" : "RFC 6070",+          "flags" : [+            "Rfc6070",+            "Printable"+          ],+          "password" : "70617373776f726450415353574f524470617373776f7264",+          "salt" : "73616c7453414c5473616c7453414c5473616c7453414c5473616c7453414c5473616c74",+          "iterationCount" : 4096,+          "dkLen" : 25,+          "dk" : "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038",+          "result" : "valid"+        },+        {+          "tcId" : 6,+          "comment" : "RFC 6070",+          "flags" : [+            "Rfc6070",+            "Ascii"+          ],+          "password" : "7061737300776f7264",+          "salt" : "7361006c74",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "56fa6aa75548099dcc37d7f03425e0c3",+          "result" : "valid"+        },+        {+          "tcId" : 7,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7130577430643470",+          "salt" : "798acc7c76739d75",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "9ea245e919e491118087fc369142e2ee",+          "result" : "valid"+        },+        {+          "tcId" : 8,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "5a30673349567272",+          "salt" : "84bbd18de5ec10ff",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "b874ff052f517d94eb6cb7edd2ede57a7d96c01eea5dbe4fafc47d6fce32ce28defb14f7980c5ed3ba10",+          "result" : "valid"+        },+        {+          "tcId" : 9,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7439315572766f47",+          "salt" : "5d76db9ca0f0bae2",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "56799b703ed17b58b9e3ee12c80f93b35b83baa769789c539f7a4cb3e825db5df8c7afed85bb276a01aae0dc78beb492de832b581c7d5b899b184516cf028e90f7",+          "result" : "valid"+        },+        {+          "tcId" : 10,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6b6f67583748447a",+          "salt" : "0a8fbd0232a3a7f6e60d6564c92ea35f",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "8e7c43f3a3baa0ac7e8e24db0b99ea45",+          "result" : "valid"+        },+        {+          "tcId" : 11,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "4939745242645168",+          "salt" : "0a3bb93c0de86a174ab005b8089706ab",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "ab713dcf744ded086b1b588f570ba0c1876e8737c488a852216add421c2dd70ff3b58e5390c80bbe9103",+          "result" : "valid"+        },+        {+          "tcId" : 12,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f4f4d37324f6330",+          "salt" : "3827c3b6b1e6de7aab096eb75e23610d",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "8cd3a480add2dfd8fbfee4f49c55b6b2980955635474c5a795d1d6cdca9e0934797cd5ea797a1cf8cfa7d975a63b3f66855ec6b4a97dde26e49bd149c6259f6f52",+          "result" : "valid"+        },+        {+          "tcId" : 13,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6761734354484546654d36526f61386349",+          "salt" : "cac9ae7ef670990e",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "5e71edeb8746e5215b0949e0f64a03aa",+          "result" : "valid"+        },+        {+          "tcId" : 14,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75375330684678774e33586f76707a6551",+          "salt" : "201b1e277f4ff955",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "349d048452b7fc74ae8e7b373925621321ff807968e3ff4d5a31b815ace5b95d07cd868f20f958161c68",+          "result" : "valid"+        },+        {+          "tcId" : 15,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "51524b6a6d6d6b48524462507178696868",+          "salt" : "e71d6a702d2d54bc",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "5c480790477441c254bc89611d52e5cb83fb0e6eb9fe1000344c638dfd134d67d4c633f6fdbb3316dc57115f34b0e9d4186b96023d6d0c6b90da34720731bdad6b",+          "result" : "valid"+        },+        {+          "tcId" : 16,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "3732317a52704b79306b4c6d3862317279",+          "salt" : "8b9314972ef99ac98b69b6c2ac4247c7",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "9edef2939c154b91d359876d41772d79",+          "result" : "valid"+        },+        {+          "tcId" : 17,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75507078436d7536577a68535030553947",+          "salt" : "874d7cc9369ff9988642ffe05a7f2153",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "69cc4a0b7374717be2312a49cc4768a8e40a360f8637eb475dc125f253a54747edfcb64132ce8097a9ed",+          "result" : "valid"+        },+        {+          "tcId" : 18,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f39505350597a445349783365374d7063",+          "salt" : "1569909588ec25a3afa147cb7fff9fb1",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "744a5c9831b81db08874abdc9cd0afd7bc7f11250da19ad6aa61ed02626b1d52dcbf9bbb34dd44c65a8a7973f34244e0e16f27e31ee45c37d84ad1f9465445ddc9",+          "result" : "valid"+        },+        {+          "tcId" : 19,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "4b2d6c600e66535c7f3d6d6c",+          "salt" : "7a4c02db8bd8fcc2",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "8739361a2fa899f6d5cceba27d4142da",+          "result" : "valid"+        },+        {+          "tcId" : 20,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "287e74200e6b69285f5f7e00",+          "salt" : "dddf018487fa4b95",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "68a1ad4082c5f45e339ac85e44fbe4368fd604d73030bab4d459e0f7e79a709293a349305bc91ac86de4",+          "result" : "valid"+        },+        {+          "tcId" : 21,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0f3b5e220e1c000c2a703a4d",+          "salt" : "7cf71b19c0daf499",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "b58ad2cb772a9eb1bae46afcfc7738a043bcc3ff602071850d3019b8935270dc20a2ee90c5cb8f6c323b49f7477797352b1ba0b7973f6a2652cd2bb319f092f371",+          "result" : "valid"+        },+        {+          "tcId" : 22,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "1f4b7b354903545f2b07720b",+          "salt" : "62f91ea01f3a18aa2f122bd15c4a615b",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "deea05da06df69c5401517d132678f90",+          "result" : "valid"+        },+        {+          "tcId" : 23,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5d7e1f70174b2f616c0d6722",+          "salt" : "4fb602173327228431dd8d088154c7ec",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "05489b534681b9eb74d4bac4caeaa9e7fa5977dca35314b0118f57662b539b016b38c6393c8c75aa419d",+          "result" : "valid"+        },+        {+          "tcId" : 24,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2657324e694c6b242b174f1a",+          "salt" : "3cad67ef1eaf3aaafb5ee372d6b1ecb4",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "a175e5aa360bd47b5e0e7d4d7bb29d0e4d5eef89f80e30e5570cb2b21aabdffb37996aee3dfefef1d79d1ec9fa0198276db9ed763770f1127d4d3e0c047037ab0a",+          "result" : "valid"+        },+        {+          "tcId" : 25,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "15296b0933473119180f3c0645202d5221633803",+          "salt" : "d7412e4137fc4410",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "6033619ba0f6a1c4d2877ab466cc8bca",+          "result" : "valid"+        },+        {+          "tcId" : 26,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2a1124361d53464245580130076d6b54001e3d01",+          "salt" : "7ecaaed1eab03c3f",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "2e481954bbb8ac4e330a1d28ea8f736d63a1f760bb58d97a70855ffe8ff9480972c46b645095470a8cda",+          "result" : "valid"+        },+        {+          "tcId" : 27,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "666252024e18233308430820127e753e6b041f56",+          "salt" : "756fafa7fac1f019",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "95273cd50ba4a4e419b2e3a59d4587291188710912bcab5bf7640deb5f34c9c046f410f4e6b83192a6d36e96b5b8f11a898fb69d1ea6df38614138165c5438ee86",+          "result" : "valid"+        },+        {+          "tcId" : 28,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "7a106c2773542c791d2f0a02632b69644f666278",+          "salt" : "a36350db68186d747254bd9835219487",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "d206f75207b50786c7571d9a4b21a2a0",+          "result" : "valid"+        },+        {+          "tcId" : 29,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0b3c6f070149615b5b461c7f5c3a366a6d375402",+          "salt" : "42be6c8fcd7858458fb97cece0069524",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "6a0ffe5218cde4ec89dad3bc6ff32133c31b86baf45740fdd1313b8b2c525871fce7be5b312999bde78d",+          "result" : "valid"+        },+        {+          "tcId" : 30,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5177161d237c1b64426353434113015665344a6c",+          "salt" : "596dbf5830aa8b00682429883e0b1ed0",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "52ec2859141885b77108eade00564a7564c41aa1747af8ef2fee384ada8e82c51743a5005d3ca4f855339837e8ff2e8fe78dcb17904a192591f508c801fc4c8abc",+          "result" : "valid"+        },+        {+          "tcId" : 31,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0a3d1bed38acc83",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "672b655566b8ce79b5e5244b2bb59472",+          "result" : "valid"+        },+        {+          "tcId" : 32,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0bcccab64cf9a",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "b753a37266ffa863239bf199abf0a8eef11c7a40cfc058ea4d46f58353d08a7bffda005327f98db8fd1d",+          "result" : "valid"+        },+        {+          "tcId" : 33,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c4bddd8f4ad69d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "28bffd8c6c6a6231336c3948c5a00510f433be50c1a36ebc2f180ace80feb4914fa35204386c2b6eb41e634951702f6de924bcffaaf4d976c01794c64c6af3d08e",+          "result" : "valid"+        },+        {+          "tcId" : 34,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c299d292cd92dcb8",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "5be67e596d953ede56531b9a66bbe566",+          "result" : "valid"+        },+        {+          "tcId" : 35,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d086d0bed193d9b6",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "f73504788a1c491bc856cb3ab9b873f07cf1dba837c100997029a206f19169583c8a2c4efe1e2bbca6aa",+          "result" : "valid"+        },+        {+          "tcId" : 36,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da8ddcacde86d3b0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "8ad0fc0024268cc557baad39644cbbe03fbc251262a51fb20f5dd2f1b4e14e73f4f53f6912e3a75f69f6ec6a42273411069df812899e4877323351b6056d8f21a2",+          "result" : "valid"+        },+        {+          "tcId" : 37,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "cfbec3b9d7acc7a9d585d1a5c28fcca119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "4a5fdfd0cafdbb8793806299275372dd",+          "result" : "valid"+        },+        {+          "tcId" : 38,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d29cd099d197d1b0da9aca8ec2a3daa5c38a",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "0479f3777ab64d13f2b514526b149686347688d824be30e077b2bcd4c05cbca845e3091b00d3b049e721",+          "result" : "valid"+        },+        {+          "tcId" : 39,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de89d193d18c75c2b8c6bcd190d198c3a2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "7374cbb82049ccfb9271ee9df3dcdcf5f64870f356816e5077f8a5fb0cad52e14df3609814b771bf7d151de23d91584b973bc7fd34ee955a893f904fcfdc9e0215",+          "result" : "valid"+        },+        {+          "tcId" : 40,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da9dd096cb93db88dbbed495ceaacf853c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "f90c5c5cab441e656c21a259fb83547e",+          "result" : "valid"+        },+        {+          "tcId" : 41,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de81d0a6cea9c7b3cbabce9bda90d29f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "bf873cb5210319562901a01c9fc8521a41b4d0397f774853f2499ec6b6b37490c4a8487523868af2ee3c",+          "result" : "valid"+        },+        {+          "tcId" : 42,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d281da97c8a3d8b7ceb9db80dc8dd4a0c3be",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "dcd906b27d8cffe87013fbb1557877e21c2bb58fcdec5f94d9a18beb523902c60fc4d63bb78f58cff300309f4d3411e8f524dadeadd956d94c551e3370b55c60b1",+          "result" : "valid"+        },+        {+          "tcId" : 43,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "8423ec7ea4ca4b03",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "5409681bde39075bda761ad33cfd06f8",+          "result" : "valid"+        },+        {+          "tcId" : 44,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "2c3c932bf0648bda",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "357f1e696f62b232b242d6a64b680fb73ccda4cdde09cc7ade62d5a9f828f4ac2f728a687c79a079c4ff",+          "result" : "valid"+        },+        {+          "tcId" : 45,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "393d474fd84a259d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "e04e777c27b3ab84cd7647751f710fa37f50019f2b1536ae52b3f7893dd9bcd6345fa2e1459501c01dfe65da07dd83ff6e88e211418c88fec36410e6efaaf59240",+          "result" : "valid"+        },+        {+          "tcId" : 46,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "48997492c3528738",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "1436967f0de829ae653a31c45168525c",+          "result" : "valid"+        },+        {+          "tcId" : 47,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "74064c3ebc53d676",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "6b7433a14d6225b5222fb7d5f5dfe674154c075ad83d13dbb274ccf6880f75c7545d35bc6e7bfcfa814b",+          "result" : "valid"+        },+        {+          "tcId" : 48,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "668d4f2c8f86f4f0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "23c9045a0a037f60d0ef0e4058c8a3633714c67e4d3f4637499de4d9c949248bfce2bc7cd6bac4cfe74e9c28a221401d3ea7d99e0b33ca23bcd66ff239d1e8c40d",+          "result" : "valid"+        },+        {+          "tcId" : 49,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "e3feb0f96dec61e9dd451465f88f132119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "16ef25c3c98e34147b33a8d33813b13f",+          "result" : "valid"+        },+        {+          "tcId" : 50,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "5c9ca419dc576470269a828e40a3aea5ca",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "fb711258128c94ffbe6c2ecffa8ae50823ec51c71ad512a2c3df0e56ad82403a87cb1b24e4049f56c065",+          "result" : "valid"+        },+        {+          "tcId" : 51,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "bf898453344c6875f0b8b9bc2c50fc58e2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "25917d740b0882ba22158b8008ce8c074462c3751413b85e44e86366147c06aa11fa07dd3ee95424d4c8db162cbd164d795e9ddfb2b369b3475c1152c2d3f01b19",+          "result" : "valid"+        },+        {+          "tcId" : 52,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "9e9d7c166ad3bec856fe5d15d3aacbc53c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "72a9fed7bf44ed9f3d09a812e14ada47",+          "result" : "valid"+        },+        {+          "tcId" : 53,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "17812c26bba941f3c2ebdb9bb6904c9f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "65a48055639d90cb775b68926e1ad0bfa164d80c010711da0d1ff6838da9d4a7d1aaf28f3b1994f8c50f",+          "result" : "valid"+        },+        {+          "tcId" : 54,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "6c816697ba2366375bb986c0af0d7d20fe",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "17d8c8f2974fbcb8c75109d1660e72fc808aaa96fbd8542a0f51bab2135333e42a5e97c5a96005550aafdba80f78da789d2759e79135a75315baa99f27e8f5f898",+          "result" : "valid"+        },+        {+          "tcId" : 55,+          "comment" : "empty password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "",+          "salt" : "1a71e2118c9fbcc9",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "93f5d4cf0da5cd8c1f2c71a48a8efebfbda76763c29ca535c5a09e627af5c410",+          "result" : "valid"+        },+        {+          "tcId" : 56,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "523249584467597a5a4271363970667a4a714e744b7761545a4544494676766b6a6253417167566e456a6b456b454557504e69383653626a6e376b725764394d67",+          "salt" : "d26b99043c8ba3a4",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "de22ce1289402528225f5e52e00dad82b394f1e593d2fa0fa83185137bef5ed2",+          "result" : "valid"+        },+        {+          "tcId" : 57,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "63727a466d396430795463456a6468545758693877674e516f544e6d486e61686f6956317071613133655471477933497531354b4f52516339494c53646756527a45524e6b4463723565676a62584a78426572536a74726b6b674341616a63356243354434706e66743836663754626663666370595a30767354454d4930524178",+          "salt" : "9266da5b8c102b27",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "c80288d20baa94340a79735e9257867b92709568e71355209911d7c4ad484621",+          "result" : "valid"+        },+        {+          "tcId" : 58,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "32647a56696e6f45774464656c656257797058314d6f4f685975585a463172514b7a32535a6c307578576377796f3561416e6f42524e7a5044763072513662693742345a34324f50695258534c6859684441643262746f647633744d54424430744b46316e4b655942656565547a70413145434150713942687a4a4c555a67737636754e4b664450333558414d684a486c736a6f5a796b677130624d506265556941796d6f324371586b64524752633876544176684e5a5838536f564d33704e74594a4a7258766975337547583233736a353847723061614a4b45763765796c373248636e6167713474766e533737626d6376676c79536d347370707a65673869",+          "salt" : "6a06903b78dae6de",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "90767f81981d82c053146a0f69f04d3035a675ffc0cbca5f5debdccfbe59aa69",+          "result" : "valid"+        },+        {+          "tcId" : 59,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ff",+          "salt" : "32140a66b88e1683",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "33d50f1918fd8cc91969b41cf0ac6cb7",+          "result" : "valid"+        },+        {+          "tcId" : 60,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ffffffffffffffff",+          "salt" : "8a359634423ed028",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "ced7945a9c40448206dc738a462cabe5",+          "result" : "valid"+        },+        {+          "tcId" : 61,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "c0c0c0c0c0c0c0c0",+          "salt" : "d6f596f170ed2414",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "c6d3d7d40940ce6ea88ec034df470722",+          "result" : "valid"+        },+        {+          "tcId" : 62,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "eeeeeeeeeeeeeeee",+          "salt" : "6b2269425e288d03",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "d1dd8d5b7f5e74d628bc656b2c4a7e08",+          "result" : "valid"+        },+        {+          "tcId" : 63,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "f0f0f0f0f0f0f0f0",+          "salt" : "ebf0b04633711248",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "1cf137fbce42e38f531a4f36ff3717e4",+          "result" : "valid"+        },+        {+          "tcId" : 64,+          "comment" : "special case password",+          "flags" : [+            "Ascii"+          ],+          "password" : "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",+          "salt" : "9de9b71eeb9d9a34",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "a7b2fbb97f52f3b5f9006e0f1acf0903",+          "result" : "valid"+        }+      ]+    }+  ]+}
+ testdata/pbkdf2_hmacsha224_test.json view
@@ -0,0 +1,844 @@+{+  "algorithm" : "PBKDF2-HMACSHA224",+  "schema" : "pbkdf_test_schema.json",+  "generatorVersion" : "0.9",+  "numberOfTests" : 58,+  "header" : [+    "Test vector of type PbkdfTest are for password based key derivations."+  ],+  "notes" : {+    "Ascii" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of ASCII characters."+    },+    "NonUtf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is not a valid UTF-8 string."+    },+    "Printable" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of printable ASCII characters."+    },+    "Pseudorandom" : {+      "bugType" : "BASIC",+      "description" : "Pseudorandomly generated test vector"+    },+    "Utf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is a valid UTF-8 string."+    }+  },+  "testGroups" : [+    {+      "type" : "PbkdfTest",+      "tests" : [+        {+          "tcId" : 1,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7130577430643470",+          "salt" : "798acc7c76739d75",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "8ee143d436330d9978192aadd0b94620",+          "result" : "valid"+        },+        {+          "tcId" : 2,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "5a30673349567272",+          "salt" : "84bbd18de5ec10ff",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "9a1f601c9cbdab8d856ca134226400b7bb76e05f832e6dbed7fbbbb8588f350df805442b5978f5640f52",+          "result" : "valid"+        },+        {+          "tcId" : 3,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7439315572766f47",+          "salt" : "5d76db9ca0f0bae2",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "a2f0f558845aa8fd8c5f7c203a59ddd0d58f1887150c2591c2909233f742748728c1cd68444c8f21d109557ed43ce6e9a1d98334069a6cedda77836fef55ad9ebd",+          "result" : "valid"+        },+        {+          "tcId" : 4,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6b6f67583748447a",+          "salt" : "0a8fbd0232a3a7f6e60d6564c92ea35f",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "bfd1d1ebc924499214c89c76006da228",+          "result" : "valid"+        },+        {+          "tcId" : 5,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "4939745242645168",+          "salt" : "0a3bb93c0de86a174ab005b8089706ab",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "1fc1f840c5b1810f6f01faefe26c895ae6b9f7207dc7cf1add3aed572eaf12bbbaddfe80db5422d50218",+          "result" : "valid"+        },+        {+          "tcId" : 6,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f4f4d37324f6330",+          "salt" : "3827c3b6b1e6de7aab096eb75e23610d",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "7444ff45b60c82af7f14f218361734e42b854fb7c1997d1266240392b6bf297f5a99906aa3c5c0156d03ffae52615f85a21ff45bdcb3ae7e5159db15d257524468",+          "result" : "valid"+        },+        {+          "tcId" : 7,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6761734354484546654d36526f61386349",+          "salt" : "cac9ae7ef670990e",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "c9448067dc15c12a1550c0b0a0e2a358",+          "result" : "valid"+        },+        {+          "tcId" : 8,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75375330684678774e33586f76707a6551",+          "salt" : "201b1e277f4ff955",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "2f4159d45782f53fb33dd1369d3aea4e2374ca4527232b6a9556e710850d5029379bf3f304d7deb46a16",+          "result" : "valid"+        },+        {+          "tcId" : 9,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "51524b6a6d6d6b48524462507178696868",+          "salt" : "e71d6a702d2d54bc",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "4d7e0c01e9b1ea0f6a50759994199d9f0044ed9cfc10808ba8412ef2014ef3d2aed990e7a8822e51adca3874fa65e99c7bf73a6f42f75c463d9f865facca832ce3",+          "result" : "valid"+        },+        {+          "tcId" : 10,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "3732317a52704b79306b4c6d3862317279",+          "salt" : "8b9314972ef99ac98b69b6c2ac4247c7",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "1852ac8b5be4deaec864b3067568662a",+          "result" : "valid"+        },+        {+          "tcId" : 11,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75507078436d7536577a68535030553947",+          "salt" : "874d7cc9369ff9988642ffe05a7f2153",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "8b2f2b1e5fcf522011021c828cf26fb17bdbbe158e8af6998c0decddad9be0b9afb5ed20c66f6bc291aa",+          "result" : "valid"+        },+        {+          "tcId" : 12,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f39505350597a445349783365374d7063",+          "salt" : "1569909588ec25a3afa147cb7fff9fb1",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "aefe089d5e9ee63c3523e11bbe71eb450a4018aa4e254ed2b2ed87d05a1b3004d384647b7409c6c757c642377b2dd00d9d08056bf593c39397df746bc6f4d16f9e",+          "result" : "valid"+        },+        {+          "tcId" : 13,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "4b2d6c600e66535c7f3d6d6c",+          "salt" : "7a4c02db8bd8fcc2",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "c49bf50d62342de453882f039bb24f9c",+          "result" : "valid"+        },+        {+          "tcId" : 14,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "287e74200e6b69285f5f7e00",+          "salt" : "dddf018487fa4b95",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "b40505833bd98dfbf63887c2a3a01b2b6eaf74f2d84eb2c61dca3e7d71518838a8be7d6a4e523ef5b560",+          "result" : "valid"+        },+        {+          "tcId" : 15,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0f3b5e220e1c000c2a703a4d",+          "salt" : "7cf71b19c0daf499",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "98c020d02eeaec4b22a02f868ddf3605b71b5cc6f7100290e52d34a9130b0fa216c642bac79880b6cd1a994c4174ec5eb49b006e9af04d2f287e08947c39a1146c",+          "result" : "valid"+        },+        {+          "tcId" : 16,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "1f4b7b354903545f2b07720b",+          "salt" : "62f91ea01f3a18aa2f122bd15c4a615b",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "e3d468c8fdd6dc804807a5d95c582fe9",+          "result" : "valid"+        },+        {+          "tcId" : 17,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5d7e1f70174b2f616c0d6722",+          "salt" : "4fb602173327228431dd8d088154c7ec",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "8bd0fea27f1cfbfe25ac2984555f8df381368988d9f2dbffc22131dd965753afd1da4a53410557c23506",+          "result" : "valid"+        },+        {+          "tcId" : 18,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2657324e694c6b242b174f1a",+          "salt" : "3cad67ef1eaf3aaafb5ee372d6b1ecb4",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "aed6d39875469a31c7ad938fd8851332a63a3dacff2b61884cea25f9e81c5610ca9d1c4f1a1afeb05d55bd9109a12ee0a71219aa40525b8cbe6af98dfd9e2f01d7",+          "result" : "valid"+        },+        {+          "tcId" : 19,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "15296b0933473119180f3c0645202d5221633803",+          "salt" : "d7412e4137fc4410",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "8d1b5237ed7520cb0027882bd0467f93",+          "result" : "valid"+        },+        {+          "tcId" : 20,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2a1124361d53464245580130076d6b54001e3d01",+          "salt" : "7ecaaed1eab03c3f",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "b95ebbc29ead629df25d6533554fbf46647222984c903ff37119b8dacd7aaba8882920de15de74497ed2",+          "result" : "valid"+        },+        {+          "tcId" : 21,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "666252024e18233308430820127e753e6b041f56",+          "salt" : "756fafa7fac1f019",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "fe93a237dd6d3b8f0b2b7eacfbe7ded7380459b8178442789a1ebedd07ee2a0e19be10cda8408874c74836f50fe91f40d53313a66af23ed3430a6ecddf1385c100",+          "result" : "valid"+        },+        {+          "tcId" : 22,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "7a106c2773542c791d2f0a02632b69644f666278",+          "salt" : "a36350db68186d747254bd9835219487",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "2ed3182fe79f06c9a596fd490b1c017d",+          "result" : "valid"+        },+        {+          "tcId" : 23,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0b3c6f070149615b5b461c7f5c3a366a6d375402",+          "salt" : "42be6c8fcd7858458fb97cece0069524",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "94414d27ac8d70b9bd2f1bd2b118d2e3e1357d2f1b0bd6362860cf4bb147ccacb7fc3368ec510956bf0c",+          "result" : "valid"+        },+        {+          "tcId" : 24,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5177161d237c1b64426353434113015665344a6c",+          "salt" : "596dbf5830aa8b00682429883e0b1ed0",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "e7e75e35802873aaab1f30780c20f94e78b170d67b2e4a176021965e625e4627cb3426160fa1d2061c6811f3f72c501fdafe661b694bcb121b51c921574ea2db58",+          "result" : "valid"+        },+        {+          "tcId" : 25,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0a3d1bed38acc83",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "cb040e12e0eea63ad10d001e89919ac6",+          "result" : "valid"+        },+        {+          "tcId" : 26,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0bcccab64cf9a",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "6fce6b09ea9b732ac9c8cde746cde750afd085727c5b6e682de5a7d37e675b00b04793f07b06f42c8b71",+          "result" : "valid"+        },+        {+          "tcId" : 27,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c4bddd8f4ad69d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "8b58d6b6e4713dfab03329c62062a9853773b28c1841f8d30fdb8c6ba784a4670c1597f3e7271dcfdbd92a1ebf568670d4089080ecd5ae2ec9764d9d2ae7a8c9ed",+          "result" : "valid"+        },+        {+          "tcId" : 28,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c299d292cd92dcb8",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "3c7b934589ba433d0da6e397f132616f",+          "result" : "valid"+        },+        {+          "tcId" : 29,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d086d0bed193d9b6",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "7814fd5d064e12b86cd9e6b61cbb2dca414b1b12e665acc8f6ae7922a5c60113d3fc181457b6733c61da",+          "result" : "valid"+        },+        {+          "tcId" : 30,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da8ddcacde86d3b0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "2d631f110b6bc4ea1ee462d6039af2f3ac651c4739f9a246eff02bcbccab7a72a854c1a1f6429c4ee1c0d62e0cf56ee5906f989e8f6ded492a33977f233d5d0a6e",+          "result" : "valid"+        },+        {+          "tcId" : 31,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "cfbec3b9d7acc7a9d585d1a5c28fcca119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "9999bb75024ef114c3e0787500c72aaa",+          "result" : "valid"+        },+        {+          "tcId" : 32,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d29cd099d197d1b0da9aca8ec2a3daa5c38a",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "c74a4ace9c8dec9490adccb6bad6f38a7b36ea30553bf032823c42bb9970a986f409e0c6314f4a82406b",+          "result" : "valid"+        },+        {+          "tcId" : 33,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de89d193d18c75c2b8c6bcd190d198c3a2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "b4baf5e438c7465049535ea5f183ccc01bf412f515e52b5259a6dbde5627193e94d632411ed16d271e119ea80ebf6c19834c1d956cab22aec7337f11662564d026",+          "result" : "valid"+        },+        {+          "tcId" : 34,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da9dd096cb93db88dbbed495ceaacf853c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "59dd1197e7721f9f3be4cbdd309e4dfd",+          "result" : "valid"+        },+        {+          "tcId" : 35,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de81d0a6cea9c7b3cbabce9bda90d29f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "2699a8ac4728c9535b2c094be58ed578c425f7c479e40946728be29c6ab14d52cc03b602b91199a213fe",+          "result" : "valid"+        },+        {+          "tcId" : 36,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d281da97c8a3d8b7ceb9db80dc8dd4a0c3be",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "33083fc03d8634cd65c9a5839d7cfdcd6b09cf8c003a90ceeb5e3e58ff719a2e1d8f6cec64506c01d159079cb3335197534830a6725efc30b8667aee805afa1d07",+          "result" : "valid"+        },+        {+          "tcId" : 37,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "8423ec7ea4ca4b03",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "3b84b80b42f43bca914e80164355e852",+          "result" : "valid"+        },+        {+          "tcId" : 38,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "2c3c932bf0648bda",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "7ce297a6d0588e7c9f6d2d75f44b0a8833f074139048aa0f4e4ab384b6776b60ef1a146eeafd2ab57c3e",+          "result" : "valid"+        },+        {+          "tcId" : 39,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "393d474fd84a259d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "a2320e47599baea5be1c4d5974fbff28d613e7aee0524aac6894e220ae5e168a3d9b9f8998ae4e13342192f682c08a7f3b9461f5c4792c288534765d6aaefae2c7",+          "result" : "valid"+        },+        {+          "tcId" : 40,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "48997492c3528738",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "a37a0077cd05aa668e786dd304981f9d",+          "result" : "valid"+        },+        {+          "tcId" : 41,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "74064c3ebc53d676",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "b35e1def1113955c4f667c6e2eedf363fbdfa88486f8035223b0e1e3eef0399b543a779cf57dc84a0341",+          "result" : "valid"+        },+        {+          "tcId" : 42,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "668d4f2c8f86f4f0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "6c8d13d7925f29cbf6987213e4842a7cde9f03ae8802bad7d493864e5f26144a80347e5a54c3ab2884cacd9aa6d41151afdbf31c0335b5b108c12c8b469386f069",+          "result" : "valid"+        },+        {+          "tcId" : 43,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "e3feb0f96dec61e9dd451465f88f132119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "a4078a7899f2adbab5f3451fcb89340a",+          "result" : "valid"+        },+        {+          "tcId" : 44,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "5c9ca419dc576470269a828e40a3aea5ca",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "350c195ddd613de101173c0601ce650d41ee8e09232567536b66abed5f752b72e07a69dea3c01bf71f47",+          "result" : "valid"+        },+        {+          "tcId" : 45,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "bf898453344c6875f0b8b9bc2c50fc58e2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "3b9b133ec921e8ea9dc432e688a15bab4217a02b34535f6aeecc3ab53b255e7bf8ec55a21ccc075fdc9b46c6ebefb1d638a8aa7a59f23d397ced4c8bf9946fa31a",+          "result" : "valid"+        },+        {+          "tcId" : 46,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "9e9d7c166ad3bec856fe5d15d3aacbc53c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "28ffe8e4266995d04219ae8d8ed8ccfc",+          "result" : "valid"+        },+        {+          "tcId" : 47,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "17812c26bba941f3c2ebdb9bb6904c9f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "ae47a0c8b42de24c2198ac5835c40197b7ca16039e35d696757ff9e5c726f3438ea6643bf9b305ec6d9c",+          "result" : "valid"+        },+        {+          "tcId" : 48,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "6c816697ba2366375bb986c0af0d7d20fe",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "55177ff79ba5c91d166676c501298e04697c3aa1d74a6d5d0083885836235b3d1de8b92cfe45a7565aae340389f6fe2954ff8bd2c6385feb07ca66fbdb3e6c60e6",+          "result" : "valid"+        },+        {+          "tcId" : 49,+          "comment" : "empty password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "",+          "salt" : "1a71e2118c9fbcc9",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "07ac722bd16e1c4172d8603276a9e0fa7cf2e571afa7ff758ab00a078716e92a",+          "result" : "valid"+        },+        {+          "tcId" : 50,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "523249584467597a5a4271363970667a4a714e744b7761545a4544494676766b6a6253417167566e456a6b456b454557504e69383653626a6e376b725764394d67",+          "salt" : "d26b99043c8ba3a4",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "b6222d1ed0a2de7cf974f5781107f2945efce5bae3593df6e4fefd56dbd0708a",+          "result" : "valid"+        },+        {+          "tcId" : 51,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "63727a466d396430795463456a6468545758693877674e516f544e6d486e61686f6956317071613133655471477933497531354b4f52516339494c53646756527a45524e6b4463723565676a62584a78426572536a74726b6b674341616a63356243354434706e66743836663754626663666370595a30767354454d4930524178",+          "salt" : "9266da5b8c102b27",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "784299cb3c35493271eb7d454afccc6badb739df38ddc7063a5d7b89711e4557",+          "result" : "valid"+        },+        {+          "tcId" : 52,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "32647a56696e6f45774464656c656257797058314d6f4f685975585a463172514b7a32535a6c307578576377796f3561416e6f42524e7a5044763072513662693742345a34324f50695258534c6859684441643262746f647633744d54424430744b46316e4b655942656565547a70413145434150713942687a4a4c555a67737636754e4b664450333558414d684a486c736a6f5a796b677130624d506265556941796d6f324371586b64524752633876544176684e5a5838536f564d33704e74594a4a7258766975337547583233736a353847723061614a4b45763765796c373248636e6167713474766e533737626d6376676c79536d347370707a65673869",+          "salt" : "6a06903b78dae6de",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "b7bfe6935e410bf32d784c62286a27c1a2cf2f62aaf24d8c3cd37e530975208f",+          "result" : "valid"+        },+        {+          "tcId" : 53,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ff",+          "salt" : "32140a66b88e1683",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "faeec67a6a2412e6358bbcf94fd38ded",+          "result" : "valid"+        },+        {+          "tcId" : 54,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ffffffffffffffff",+          "salt" : "8a359634423ed028",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "fb035bae860781afee4d914470ea4850",+          "result" : "valid"+        },+        {+          "tcId" : 55,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "c0c0c0c0c0c0c0c0",+          "salt" : "d6f596f170ed2414",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "fd42b834d6972af4605a86838471ad82",+          "result" : "valid"+        },+        {+          "tcId" : 56,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "eeeeeeeeeeeeeeee",+          "salt" : "6b2269425e288d03",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "3f57172c4cc4da1ccbd022f84c7b82a8",+          "result" : "valid"+        },+        {+          "tcId" : 57,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "f0f0f0f0f0f0f0f0",+          "salt" : "ebf0b04633711248",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "645e34ea3e1a8dc4aaf2ffb1f970c3ee",+          "result" : "valid"+        },+        {+          "tcId" : 58,+          "comment" : "special case password",+          "flags" : [+            "Ascii"+          ],+          "password" : "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",+          "salt" : "9de9b71eeb9d9a34",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "54fdb38a5a46c597866bdb22b0bdd7b0",+          "result" : "valid"+        }+      ]+    }+  ]+}
+ testdata/pbkdf2_hmacsha256_test.json view
@@ -0,0 +1,876 @@+{+  "algorithm" : "PBKDF2-HMACSHA256",+  "schema" : "pbkdf_test_schema.json",+  "generatorVersion" : "0.9",+  "numberOfTests" : 60,+  "header" : [+    "Test vector of type PbkdfTest are for password based key derivations."+  ],+  "notes" : {+    "Ascii" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of ASCII characters."+    },+    "NonUtf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is not a valid UTF-8 string."+    },+    "Printable" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of printable ASCII characters."+    },+    "Pseudorandom" : {+      "bugType" : "BASIC",+      "description" : "Pseudorandomly generated test vector"+    },+    "Rfc7914" : {+      "bugType" : "BASIC",+      "description" : "Known test vector from RFC 7914"+    },+    "Utf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is a valid UTF-8 string."+    }+  },+  "testGroups" : [+    {+      "type" : "PbkdfTest",+      "tests" : [+        {+          "tcId" : 1,+          "comment" : "RFC 7914",+          "flags" : [+            "Rfc7914",+            "Printable"+          ],+          "password" : "706173737764",+          "salt" : "73616c74",+          "iterationCount" : 1,+          "dkLen" : 64,+          "dk" : "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783",+          "result" : "valid"+        },+        {+          "tcId" : 2,+          "comment" : "RFC 7914",+          "flags" : [+            "Rfc7914",+            "Printable"+          ],+          "password" : "50617373776f7264",+          "salt" : "4e61436c",+          "iterationCount" : 80000,+          "dkLen" : 64,+          "dk" : "4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d",+          "result" : "valid"+        },+        {+          "tcId" : 3,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7130577430643470",+          "salt" : "798acc7c76739d75",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "0501c73cb9f40b6769513e70e525051e",+          "result" : "valid"+        },+        {+          "tcId" : 4,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "5a30673349567272",+          "salt" : "84bbd18de5ec10ff",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "05fd57d1cc373fa9f37e1857ac1c0af8fbf635e139a42f9dd25a4e4b4698ea13e943f42220384d32a272",+          "result" : "valid"+        },+        {+          "tcId" : 5,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7439315572766f47",+          "salt" : "5d76db9ca0f0bae2",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "2a3974a8071f005997c00d33812d8cf52e6af76a7ac558bf5dedeb296464bccf696ad753a79eb7b1b21738584a58d03d2b6c2b7bda08788f844655f6a90b0e3444",+          "result" : "valid"+        },+        {+          "tcId" : 6,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6b6f67583748447a",+          "salt" : "0a8fbd0232a3a7f6e60d6564c92ea35f",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "c81676dbeb95582c66c3fc2636f1cb7f",+          "result" : "valid"+        },+        {+          "tcId" : 7,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "4939745242645168",+          "salt" : "0a3bb93c0de86a174ab005b8089706ab",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "b7a44e0e93f5f7ec0e177292db0b4a1354b62709e0dfe02167e19586fa99a70d777b39f953fb4fa4c342",+          "result" : "valid"+        },+        {+          "tcId" : 8,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f4f4d37324f6330",+          "salt" : "3827c3b6b1e6de7aab096eb75e23610d",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "907bc6a107df5940fb4c986b3f1ed888bcb0c41462df94b58de682c5627c1125492fd3091b06d83ce09794edad3776adb107994f671efe7b3ccd1d3a8fdcb444cc",+          "result" : "valid"+        },+        {+          "tcId" : 9,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6761734354484546654d36526f61386349",+          "salt" : "cac9ae7ef670990e",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "2a9fc926e220c7915ccd8cde2916ae87",+          "result" : "valid"+        },+        {+          "tcId" : 10,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75375330684678774e33586f76707a6551",+          "salt" : "201b1e277f4ff955",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "0dc0835f4b34eb966f4cf0d662c020eb09b733c7baa78abe2be41a01a1ccad60aad4969ac3fbcb643511",+          "result" : "valid"+        },+        {+          "tcId" : 11,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "51524b6a6d6d6b48524462507178696868",+          "salt" : "e71d6a702d2d54bc",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "2cdbfdc4f06e4d0e743a49ca459eff921b2eaf70e5d69b3c913239c56376c78c47ab66927c322612ad97cb1c824ae425aaefc6647db405dfab3a89cfd2055a0f4c",+          "result" : "valid"+        },+        {+          "tcId" : 12,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "3732317a52704b79306b4c6d3862317279",+          "salt" : "8b9314972ef99ac98b69b6c2ac4247c7",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "0da1ed7920894303780ee9a34fb6fa2f",+          "result" : "valid"+        },+        {+          "tcId" : 13,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75507078436d7536577a68535030553947",+          "salt" : "874d7cc9369ff9988642ffe05a7f2153",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "35367e9ce58daa37e51883e9828204d438ede1065acf9933dfee1aae0ac7b24b46ae0c2c16885b809e6b",+          "result" : "valid"+        },+        {+          "tcId" : 14,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f39505350597a445349783365374d7063",+          "salt" : "1569909588ec25a3afa147cb7fff9fb1",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "7afa4394324fdfe23b879a8fb932c908217e4368ee5ee8dccac2fe2d2f62c12e9f48620c50b7bdb05448c64987b10df54ff5012e9a5064e9f92c51cc7b635d270d",+          "result" : "valid"+        },+        {+          "tcId" : 15,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "4b2d6c600e66535c7f3d6d6c",+          "salt" : "7a4c02db8bd8fcc2",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "b05992b03b7f25b4a24a27e98cdfff3a",+          "result" : "valid"+        },+        {+          "tcId" : 16,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "287e74200e6b69285f5f7e00",+          "salt" : "dddf018487fa4b95",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "89d809be903d632abd1b29130229b3a837bb4943aafb86a520498fa1814cd3f82f0b6c7db8d09d19c439",+          "result" : "valid"+        },+        {+          "tcId" : 17,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0f3b5e220e1c000c2a703a4d",+          "salt" : "7cf71b19c0daf499",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "5f3110ce1e653e086b289eb972ddd496b44884b7eab0361d0e9a39426b1d977582bd32b84ae1a204a51dbd458757489e01928dbcced2397109a1470d8383804099",+          "result" : "valid"+        },+        {+          "tcId" : 18,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "1f4b7b354903545f2b07720b",+          "salt" : "62f91ea01f3a18aa2f122bd15c4a615b",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "d4bbf153661b4516d740535b37780aaf",+          "result" : "valid"+        },+        {+          "tcId" : 19,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5d7e1f70174b2f616c0d6722",+          "salt" : "4fb602173327228431dd8d088154c7ec",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "8216132b4952f8be46586e22ce961b597671f9172027e36f2c4cbd189dcaf719981a9c03186d5faadd80",+          "result" : "valid"+        },+        {+          "tcId" : 20,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2657324e694c6b242b174f1a",+          "salt" : "3cad67ef1eaf3aaafb5ee372d6b1ecb4",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "7103c69766b64bae721776ac272db586c7ba3c4775e33e1f37a24fad51e599d56d344e4566d48928f3c1c52b91642b10c86a29d3d2490eee2889c2899802e38c82",+          "result" : "valid"+        },+        {+          "tcId" : 21,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "15296b0933473119180f3c0645202d5221633803",+          "salt" : "d7412e4137fc4410",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "1c0b23dfd1a3ea727c5c596ca840b393",+          "result" : "valid"+        },+        {+          "tcId" : 22,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2a1124361d53464245580130076d6b54001e3d01",+          "salt" : "7ecaaed1eab03c3f",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "c0f82935e509c96ad0edf9aa2db243cb81cec0c047d5141915a58ce114c119290228f318a9c5dcd518f6",+          "result" : "valid"+        },+        {+          "tcId" : 23,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "666252024e18233308430820127e753e6b041f56",+          "salt" : "756fafa7fac1f019",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "d571b3006053ce3060ddfdbcf3c782d834e49701d4c745f3a282618cacdff4c73cadb7e719de641860ab0ffc860518d0bf02cec16ca83c26f4df993050ee67f2f7",+          "result" : "valid"+        },+        {+          "tcId" : 24,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "7a106c2773542c791d2f0a02632b69644f666278",+          "salt" : "a36350db68186d747254bd9835219487",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "5f0af11697779ae0530fab32806f2184",+          "result" : "valid"+        },+        {+          "tcId" : 25,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0b3c6f070149615b5b461c7f5c3a366a6d375402",+          "salt" : "42be6c8fcd7858458fb97cece0069524",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "88d3744fae380954ae5875eca20b998e0d0c991716a8ef90cc1e547d489415ce6f8310742defcf4ca6cd",+          "result" : "valid"+        },+        {+          "tcId" : 26,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5177161d237c1b64426353434113015665344a6c",+          "salt" : "596dbf5830aa8b00682429883e0b1ed0",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "d8e5a55f92ba0d6da87765c653c82f080472260640e59ebe7d003baca444bdbe553156e5c365be28fc0acb7927e5c9789cdf6b481aa1c39c444a0cfc8b6e475182",+          "result" : "valid"+        },+        {+          "tcId" : 27,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0a3d1bed38acc83",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "c06bc1f9a63de10fe5f6936991477029",+          "result" : "valid"+        },+        {+          "tcId" : 28,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0bcccab64cf9a",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "6a6287564b66b1f08bfabdfe2907b870b8c6ad8071efd78a7302c9472d7b80fb50c662bf609827006831",+          "result" : "valid"+        },+        {+          "tcId" : 29,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c4bddd8f4ad69d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "7e52e8694056266a7da6527dac089f921298dfc497020eecee58d73fd123deb97cf3c2f532505468afc17e6e283ba6b68afcd5e6881be17e1647d3ab491a5589b1",+          "result" : "valid"+        },+        {+          "tcId" : 30,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c299d292cd92dcb8",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "fcfa00ce7c0249542e6f3a40ef0a86d0",+          "result" : "valid"+        },+        {+          "tcId" : 31,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d086d0bed193d9b6",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "46abf02c99b9f6d86fbf2e11bf0642b658a82a9e034ddea40df78a84bbc4b1e1e9f5bfbc55b12fba4545",+          "result" : "valid"+        },+        {+          "tcId" : 32,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da8ddcacde86d3b0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "4438d49f0cd0ff7026fa6283b25bac570e1c845eaccdd80060e896f5420ee54533eb649a6fd9d99d29ceb7dcb18d059d422840aef7d4452d888f59bdd760b5db73",+          "result" : "valid"+        },+        {+          "tcId" : 33,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "cfbec3b9d7acc7a9d585d1a5c28fcca119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "a9a7c8ade1041532e085ff65c35f28e8",+          "result" : "valid"+        },+        {+          "tcId" : 34,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d29cd099d197d1b0da9aca8ec2a3daa5c38a",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "e34b5cf037bec44307bb8a60bbdc7d5f19badd15825150ec453adb490b947c3c1bc4132ec2217be47f60",+          "result" : "valid"+        },+        {+          "tcId" : 35,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de89d193d18c75c2b8c6bcd190d198c3a2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "d9ac15032443c7ed2678a07849c65897a2625f367fee614895c6f4f655094721024342ad24f0fa654bbd1abc7d01993ab21120da4e6f5b36a67885462d14442732",+          "result" : "valid"+        },+        {+          "tcId" : 36,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da9dd096cb93db88dbbed495ceaacf853c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "230bf1d40a4ccfe9a1b5241a8266cef9",+          "result" : "valid"+        },+        {+          "tcId" : 37,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de81d0a6cea9c7b3cbabce9bda90d29f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "978fd90caf8646ef894d8c9ffe19d20f0f0dfe80cf5bfe8464385bc0552e226a82b1fa764e8198ffda9f",+          "result" : "valid"+        },+        {+          "tcId" : 38,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d281da97c8a3d8b7ceb9db80dc8dd4a0c3be",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "92b10b7d996d6e3f5d8e7e2c1075cc89c837b85cb72aff0092449b256254591178a6bf7fdfe3742f5fe5e402788058f4f90efc9cc9e9e7547b27bd4d34da333314",+          "result" : "valid"+        },+        {+          "tcId" : 39,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "8423ec7ea4ca4b03",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "5425a8ac3db02f9675331c0560a7b641",+          "result" : "valid"+        },+        {+          "tcId" : 40,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "2c3c932bf0648bda",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "9eb4049ccee01488f15af3b988ef6d4817fce166686c17a7ea1cdc8bc548883ed1d53df6439fecda324b",+          "result" : "valid"+        },+        {+          "tcId" : 41,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "393d474fd84a259d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "a6d3a1636fe688f9a0aef5050e749c9f641669e6b953958263752382cb324de125315c4f340464c935e15e60dfa61fc8e8cd15386b2ed9894fad8b327338dc91f7",+          "result" : "valid"+        },+        {+          "tcId" : 42,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "48997492c3528738",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "2b7d2346fbb52fbff8881db235132319",+          "result" : "valid"+        },+        {+          "tcId" : 43,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "74064c3ebc53d676",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "a97c2fb6bd6a91470d78af24ee8f0f9e783517ba0396db031a46640c8436368ba9b5d0e048952bb2a6f0",+          "result" : "valid"+        },+        {+          "tcId" : 44,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "668d4f2c8f86f4f0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "8a4399e9487079701b079b6ad3587ff62593ff75c36629268e0cc2f88089203ac27a81ef9b4fa9ab6db5e220fca8f3d46c2b8c5c4d1149d461d1cd2c0884496608",+          "result" : "valid"+        },+        {+          "tcId" : 45,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "e3feb0f96dec61e9dd451465f88f132119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "a2641a7a323435e0443cfd8dd60a536e",+          "result" : "valid"+        },+        {+          "tcId" : 46,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "5c9ca419dc576470269a828e40a3aea5ca",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "df3c139e07076d5e37d9dd0b59164903830d8fd1c4cb0b3189a5f97cd73b66f8066c31630bb44e705d9f",+          "result" : "valid"+        },+        {+          "tcId" : 47,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "bf898453344c6875f0b8b9bc2c50fc58e2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "bb14e909b953a6576fdd607ccb8ee2d69fc1f9458ae597010c08a17094d64beb156f3db86203d441b7a2a192d75fb8f8ce78baf6f059f3b982a1e1ea32a6f1a958",+          "result" : "valid"+        },+        {+          "tcId" : 48,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "9e9d7c166ad3bec856fe5d15d3aacbc53c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "38a92f260cc84a8eb5943df1f19f64a1",+          "result" : "valid"+        },+        {+          "tcId" : 49,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "17812c26bba941f3c2ebdb9bb6904c9f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "a410ebcad1e884660353e7324d04f9f0e68ea3bb925fefe32b86c8f590029d63f9abc5c34c9a7b07845f",+          "result" : "valid"+        },+        {+          "tcId" : 50,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "6c816697ba2366375bb986c0af0d7d20fe",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "2577e802465d3e309dc76be377bbabdc8e59838d9f4f0a23ccd67f118094a851edd9a6ab4d332d8927bb0b77e71da28d6f5db78b2196758340ebf25e74cf681a1f",+          "result" : "valid"+        },+        {+          "tcId" : 51,+          "comment" : "empty password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "",+          "salt" : "1a71e2118c9fbcc9",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "3e513d89ea5ad303f17cbf7cbdea54a940f0f5811844dfa875a55a8241d2f8df",+          "result" : "valid"+        },+        {+          "tcId" : 52,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "523249584467597a5a4271363970667a4a714e744b7761545a4544494676766b6a6253417167566e456a6b456b454557504e69383653626a6e376b725764394d67",+          "salt" : "d26b99043c8ba3a4",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "c8595fa30dc95fb839bebfcc230f06844b2f75a393570b22d6c14d647837b87a",+          "result" : "valid"+        },+        {+          "tcId" : 53,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "63727a466d396430795463456a6468545758693877674e516f544e6d486e61686f6956317071613133655471477933497531354b4f52516339494c53646756527a45524e6b4463723565676a62584a78426572536a74726b6b674341616a63356243354434706e66743836663754626663666370595a30767354454d4930524178",+          "salt" : "9266da5b8c102b27",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "24a86f12235e0232bc80a84635a43934b2d37ae1120b4aa1728a3ead93868980",+          "result" : "valid"+        },+        {+          "tcId" : 54,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "32647a56696e6f45774464656c656257797058314d6f4f685975585a463172514b7a32535a6c307578576377796f3561416e6f42524e7a5044763072513662693742345a34324f50695258534c6859684441643262746f647633744d54424430744b46316e4b655942656565547a70413145434150713942687a4a4c555a67737636754e4b664450333558414d684a486c736a6f5a796b677130624d506265556941796d6f324371586b64524752633876544176684e5a5838536f564d33704e74594a4a7258766975337547583233736a353847723061614a4b45763765796c373248636e6167713474766e533737626d6376676c79536d347370707a65673869",+          "salt" : "6a06903b78dae6de",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "a50be9c16f6bf68808436aa3bc6eec36d3c5653c9c7510c1a4a641755b8325fb",+          "result" : "valid"+        },+        {+          "tcId" : 55,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ff",+          "salt" : "32140a66b88e1683",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "49bc8d940c8f67ae5ee0764f59dea94e",+          "result" : "valid"+        },+        {+          "tcId" : 56,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ffffffffffffffff",+          "salt" : "8a359634423ed028",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "00ef53593c10c8986f36838017194c2c",+          "result" : "valid"+        },+        {+          "tcId" : 57,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "c0c0c0c0c0c0c0c0",+          "salt" : "d6f596f170ed2414",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "b81cd442c5aa1b23aee69225a501cc70",+          "result" : "valid"+        },+        {+          "tcId" : 58,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "eeeeeeeeeeeeeeee",+          "salt" : "6b2269425e288d03",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "a80f332f10dfc0e4380b2fac8449fe77",+          "result" : "valid"+        },+        {+          "tcId" : 59,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "f0f0f0f0f0f0f0f0",+          "salt" : "ebf0b04633711248",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "a516a9d9e5d310227dc19ef15357b4b1",+          "result" : "valid"+        },+        {+          "tcId" : 60,+          "comment" : "special case password",+          "flags" : [+            "Ascii"+          ],+          "password" : "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",+          "salt" : "9de9b71eeb9d9a34",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "5869f35bb108f1c45605ca8109e6661d",+          "result" : "valid"+        }+      ]+    }+  ]+}
+ testdata/pbkdf2_hmacsha384_test.json view
@@ -0,0 +1,844 @@+{+  "algorithm" : "PBKDF2-HMACSHA384",+  "schema" : "pbkdf_test_schema.json",+  "generatorVersion" : "0.9",+  "numberOfTests" : 58,+  "header" : [+    "Test vector of type PbkdfTest are for password based key derivations."+  ],+  "notes" : {+    "Ascii" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of ASCII characters."+    },+    "NonUtf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is not a valid UTF-8 string."+    },+    "Printable" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of printable ASCII characters."+    },+    "Pseudorandom" : {+      "bugType" : "BASIC",+      "description" : "Pseudorandomly generated test vector"+    },+    "Utf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is a valid UTF-8 string."+    }+  },+  "testGroups" : [+    {+      "type" : "PbkdfTest",+      "tests" : [+        {+          "tcId" : 1,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7130577430643470",+          "salt" : "798acc7c76739d75",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "c5a4f853b1a3960134e572c8e29a5be9",+          "result" : "valid"+        },+        {+          "tcId" : 2,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "5a30673349567272",+          "salt" : "84bbd18de5ec10ff",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "dfe1dea643acbae0d7d3154717f9a620937d9bf32218b27db99c0c34a0a6dfa189b31292020b727cc147",+          "result" : "valid"+        },+        {+          "tcId" : 3,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7439315572766f47",+          "salt" : "5d76db9ca0f0bae2",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "17c6ba7e45f8a26a13b4d5f72ca3a2f97147e5f60c3108829b5b51633ab8afd9888b0465b22995f072ee2c8383e091afb808bf48b0e786da661ff95142a6229f1f",+          "result" : "valid"+        },+        {+          "tcId" : 4,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6b6f67583748447a",+          "salt" : "0a8fbd0232a3a7f6e60d6564c92ea35f",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "7b3772edd5ddd2fefed54e124f813d15",+          "result" : "valid"+        },+        {+          "tcId" : 5,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "4939745242645168",+          "salt" : "0a3bb93c0de86a174ab005b8089706ab",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "314c435ffba5e7f8dc3af254ac164c51398c839f3c789a91fb7927aa28dc2424bc589e29fd715fe74bba",+          "result" : "valid"+        },+        {+          "tcId" : 6,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f4f4d37324f6330",+          "salt" : "3827c3b6b1e6de7aab096eb75e23610d",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "409b051d5d6f77149633e65dfd0ebbadebda7f82ffcc09d2efb81c6d94a425537c5bb1bea65a9fb40a92a4466bfb448d7b3505894dccdb6a365d44d4eba54ef11c",+          "result" : "valid"+        },+        {+          "tcId" : 7,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6761734354484546654d36526f61386349",+          "salt" : "cac9ae7ef670990e",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "9df584f636d3853cf75116538f240315",+          "result" : "valid"+        },+        {+          "tcId" : 8,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75375330684678774e33586f76707a6551",+          "salt" : "201b1e277f4ff955",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "74d040278a45fe589c66833a34cc71bc7249b4dc86e3dc70117afb94cf7cb60bca51c1d3359a06998c95",+          "result" : "valid"+        },+        {+          "tcId" : 9,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "51524b6a6d6d6b48524462507178696868",+          "salt" : "e71d6a702d2d54bc",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "421a7d2aa3a0b31f51b2971af45327ffbcb023a6fd71a51739e57b335c9fe386f8b3083991bc012474161341ebb71f7b9f495d012904264aacebc00a6392b7e81c",+          "result" : "valid"+        },+        {+          "tcId" : 10,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "3732317a52704b79306b4c6d3862317279",+          "salt" : "8b9314972ef99ac98b69b6c2ac4247c7",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "5873f6050f94ffbd5db5913711a1e779",+          "result" : "valid"+        },+        {+          "tcId" : 11,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75507078436d7536577a68535030553947",+          "salt" : "874d7cc9369ff9988642ffe05a7f2153",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "38c6c68ab0ea2aab6e8ec42b6d08203e7f3c2a51c7771626b9df6ac6ac0e7c7b9e690ea53916e7a4ef0a",+          "result" : "valid"+        },+        {+          "tcId" : 12,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f39505350597a445349783365374d7063",+          "salt" : "1569909588ec25a3afa147cb7fff9fb1",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "8b3c1f69e3e992203f59b4aee9c0ed79d98bf7a1f261036d58e2289dcde27504129d357f8c21919f1f152e7d4a5c011e04f1adf2a3d0fdcb64cad396810332d291",+          "result" : "valid"+        },+        {+          "tcId" : 13,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "4b2d6c600e66535c7f3d6d6c",+          "salt" : "7a4c02db8bd8fcc2",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "60f6a456e9a4f1d8dcfb7a73c58ab3be",+          "result" : "valid"+        },+        {+          "tcId" : 14,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "287e74200e6b69285f5f7e00",+          "salt" : "dddf018487fa4b95",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "745ff4750e9371a28424220ea555138fed621e4b32c58503720dd1e2243803848f9fe9b74ed09afe759f",+          "result" : "valid"+        },+        {+          "tcId" : 15,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0f3b5e220e1c000c2a703a4d",+          "salt" : "7cf71b19c0daf499",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "9531ee2380908f10dd0806782a90dd01a513880e6d848ce8370e5a6dff7a061d6e20a2b1ecccadfed5feaae62fa64e0cc6fe98bd87f6311fbc624453344e694a94",+          "result" : "valid"+        },+        {+          "tcId" : 16,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "1f4b7b354903545f2b07720b",+          "salt" : "62f91ea01f3a18aa2f122bd15c4a615b",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "ea4658ca124f8a8332e1241da381e22e",+          "result" : "valid"+        },+        {+          "tcId" : 17,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5d7e1f70174b2f616c0d6722",+          "salt" : "4fb602173327228431dd8d088154c7ec",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "7b89d1bd20dec87b8ecf9bae12fa0e5d9c0f1c010078249baeefded4f23e354a5fe9efa50d2d569ee12d",+          "result" : "valid"+        },+        {+          "tcId" : 18,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2657324e694c6b242b174f1a",+          "salt" : "3cad67ef1eaf3aaafb5ee372d6b1ecb4",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "08968b88460e0df143e1f52d0b1640e77266204bb552d3e9a260dfc782aef06707f7eb386a3037dc343792b6c41452549d812eca04be8bf01d436e1557bec6438d",+          "result" : "valid"+        },+        {+          "tcId" : 19,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "15296b0933473119180f3c0645202d5221633803",+          "salt" : "d7412e4137fc4410",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "23055ea847137568a2ea8f84ae13d220",+          "result" : "valid"+        },+        {+          "tcId" : 20,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2a1124361d53464245580130076d6b54001e3d01",+          "salt" : "7ecaaed1eab03c3f",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "819a1fd3f182702d3629b0453352e7224048ed964e57b252a9a039c1d73292f2f668429a8aa530732edc",+          "result" : "valid"+        },+        {+          "tcId" : 21,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "666252024e18233308430820127e753e6b041f56",+          "salt" : "756fafa7fac1f019",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "ba344a8df0cbedced95d6f5f8ea8f4d3e1aca52f751a2c1b529067df28d0e3d08ea42ffb6b8f8537dab32ce89ec0dd4a84c7683b5960d987ab7f5f744c7d8d7f84",+          "result" : "valid"+        },+        {+          "tcId" : 22,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "7a106c2773542c791d2f0a02632b69644f666278",+          "salt" : "a36350db68186d747254bd9835219487",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "d5ce40d19ffa0ea0a0f9ede719053fcd",+          "result" : "valid"+        },+        {+          "tcId" : 23,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0b3c6f070149615b5b461c7f5c3a366a6d375402",+          "salt" : "42be6c8fcd7858458fb97cece0069524",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "840512f3702d1387ab28785750cca03d0452dcd3abbf39f56fa38c04dfe6e1e17570463a8cc074850070",+          "result" : "valid"+        },+        {+          "tcId" : 24,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5177161d237c1b64426353434113015665344a6c",+          "salt" : "596dbf5830aa8b00682429883e0b1ed0",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "f9e10e6fc595e3ae155910f4c019245b559bbec1a944de00bbbe1fe202265db4ccf28b9f10b519c1856e3386a15ba9005e05cf5e050718d4a92c89dc309f167a31",+          "result" : "valid"+        },+        {+          "tcId" : 25,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0a3d1bed38acc83",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "ae75e7ae9848e3550cf3ddfca4ed5e74",+          "result" : "valid"+        },+        {+          "tcId" : 26,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0bcccab64cf9a",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "9b909246d2fb6e76a5375bcd608820f855e43b7782fb7aecfedc766fbd7296a99862c4ee9ffe52d2305d",+          "result" : "valid"+        },+        {+          "tcId" : 27,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c4bddd8f4ad69d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "555ab346c29248f24748dd7cb9ca4d7793894cf0ffdf337c01c6badf5ea8821eac516b7a93a74628be32468ecad35bc28bf422bcee9046ed9d5512765d644edf05",+          "result" : "valid"+        },+        {+          "tcId" : 28,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c299d292cd92dcb8",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "84d0773faa5dad4dbbd7d7f461272807",+          "result" : "valid"+        },+        {+          "tcId" : 29,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d086d0bed193d9b6",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "58b8fd680c076f23f4e65bbb58a4af6a7160c4a8d81c99fb05f0543dd01923dcaef67a8ac6f14e6e1019",+          "result" : "valid"+        },+        {+          "tcId" : 30,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da8ddcacde86d3b0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "fca2e866aa3e4f42abe27feb90a55286bbd868c93bdd67029db96c77bf943eb094c9e31c55f108bd848824e0ef0bc69e763cbd7c3c6ddc008703a40bcbc8e22667",+          "result" : "valid"+        },+        {+          "tcId" : 31,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "cfbec3b9d7acc7a9d585d1a5c28fcca119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "7612061da9d4dd1ee3638ab60eae5605",+          "result" : "valid"+        },+        {+          "tcId" : 32,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d29cd099d197d1b0da9aca8ec2a3daa5c38a",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "845d37414eb3bba79503226c55112a62f787d88aae3f027c0028a06f63d7307dcf9c1c7cd8f255755562",+          "result" : "valid"+        },+        {+          "tcId" : 33,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de89d193d18c75c2b8c6bcd190d198c3a2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "94fa0a0e7150e8553aef53c3d695f8ef65b3e54c056ed1d0a0c46a064cb5891f4bdff7ee37f21136fb482dcb13421fdce7408b2cd18b9831dc11143aacfca500f0",+          "result" : "valid"+        },+        {+          "tcId" : 34,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da9dd096cb93db88dbbed495ceaacf853c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "33f720ca3aa8e6784f93d9d04983f88a",+          "result" : "valid"+        },+        {+          "tcId" : 35,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de81d0a6cea9c7b3cbabce9bda90d29f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "5b043b06c042f0ed61f00d15b4d058425ecb822505d141757ad2b428f1ef71dbdb2bca191b7d7b5704b9",+          "result" : "valid"+        },+        {+          "tcId" : 36,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d281da97c8a3d8b7ceb9db80dc8dd4a0c3be",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "67aee62bffe2d7742a2058bb146ba629f532e2c0576555f0fb385ae8ea7b34e2f8d8b24dd31ea3a14ec6f39138337771d86518d3d1742eb3a5c197d4732012a338",+          "result" : "valid"+        },+        {+          "tcId" : 37,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "8423ec7ea4ca4b03",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "de49fbeb68b9e092e952fb2f752b642b",+          "result" : "valid"+        },+        {+          "tcId" : 38,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "2c3c932bf0648bda",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "49b18e7cf5f8803bc129c0f433de931f7e2c3ac1f3305789495ec3548787cee83c1c973f73f4bac9e80b",+          "result" : "valid"+        },+        {+          "tcId" : 39,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "393d474fd84a259d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "f3a909587aacd3fd03e7fb4a48ece960d90a7a65b36f5b8f982f14f0997a4630e01cf092573a9af728f46e86b35099fe68ccab07a111b832718bd224af8a130194",+          "result" : "valid"+        },+        {+          "tcId" : 40,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "48997492c3528738",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "c26c4d099817ab48baeb2ed94a6e0e66",+          "result" : "valid"+        },+        {+          "tcId" : 41,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "74064c3ebc53d676",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "2790b6ca89a36f6702f3fe2a031fbf5349613dd756b8e23e545180286b7f94746eb8df4ee00536a902e1",+          "result" : "valid"+        },+        {+          "tcId" : 42,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "668d4f2c8f86f4f0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "44fbc1a0422401ed1b4f7a3cccf4c56ed6dd61bbca31ffc542e2930ea62b5c6deeff8cc9c709b2e96a5bcc3ef5b47fec89a0fad774ef5a406fcd319a374dcbf7db",+          "result" : "valid"+        },+        {+          "tcId" : 43,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "e3feb0f96dec61e9dd451465f88f132119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "81c0ff3ddfee0fbb1c285ea772b21a93",+          "result" : "valid"+        },+        {+          "tcId" : 44,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "5c9ca419dc576470269a828e40a3aea5ca",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "a54129741f4d41c198e76bc688de03d221bee45538cdb9f265859af918a4b336f6fc9b9f9a0b44894107",+          "result" : "valid"+        },+        {+          "tcId" : 45,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "bf898453344c6875f0b8b9bc2c50fc58e2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "d88a85b941a802f48a4fa80ebe4ca10e659d684a6310975dff862819930b60f0d776b728a716ef3b7abaea89c9b0fdcbb76c1b9fac44ad28d468800bbb20c7a76c",+          "result" : "valid"+        },+        {+          "tcId" : 46,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "9e9d7c166ad3bec856fe5d15d3aacbc53c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "1a7db5ce7b3fa690dda6aed94b42a07d",+          "result" : "valid"+        },+        {+          "tcId" : 47,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "17812c26bba941f3c2ebdb9bb6904c9f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "41f0fdc44e8e51be0bf913af19524a6d041d6160194226fc90c17da715b4f7b6859060d72d8973bfa66d",+          "result" : "valid"+        },+        {+          "tcId" : 48,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "6c816697ba2366375bb986c0af0d7d20fe",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "81fb39ec6f23c4fd32fc923cea3fbfa42f5b10620f3f0c0716e71301d0886a1392f50ee0e7805964f44bc08abce926002a36fc7806828e7da93cf8ddb910a21e60",+          "result" : "valid"+        },+        {+          "tcId" : 49,+          "comment" : "empty password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "",+          "salt" : "1a71e2118c9fbcc9",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "221029235aaa16453fc00ce61bb81d2c0fe24e8a038dbf845578f7d3aa1006d9",+          "result" : "valid"+        },+        {+          "tcId" : 50,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "523249584467597a5a4271363970667a4a714e744b7761545a4544494676766b6a6253417167566e456a6b456b454557504e69383653626a6e376b725764394d67",+          "salt" : "d26b99043c8ba3a4",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "6aff25d08e9acf0bc81366c88c2939b2206a5f3e96a6ae1bb7754286edd72fb5",+          "result" : "valid"+        },+        {+          "tcId" : 51,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "63727a466d396430795463456a6468545758693877674e516f544e6d486e61686f6956317071613133655471477933497531354b4f52516339494c53646756527a45524e6b4463723565676a62584a78426572536a74726b6b674341616a63356243354434706e66743836663754626663666370595a30767354454d4930524178",+          "salt" : "9266da5b8c102b27",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "b967e5eb79c1ec4a3ad0cb09c18915793bd87b51911a5e9ca43cbe49eb328b83",+          "result" : "valid"+        },+        {+          "tcId" : 52,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "32647a56696e6f45774464656c656257797058314d6f4f685975585a463172514b7a32535a6c307578576377796f3561416e6f42524e7a5044763072513662693742345a34324f50695258534c6859684441643262746f647633744d54424430744b46316e4b655942656565547a70413145434150713942687a4a4c555a67737636754e4b664450333558414d684a486c736a6f5a796b677130624d506265556941796d6f324371586b64524752633876544176684e5a5838536f564d33704e74594a4a7258766975337547583233736a353847723061614a4b45763765796c373248636e6167713474766e533737626d6376676c79536d347370707a65673869",+          "salt" : "6a06903b78dae6de",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "b94a84f5e52d232868f5fe8f4c568ad582e7bd623eab7b7a4f4081f45ab2c7ea",+          "result" : "valid"+        },+        {+          "tcId" : 53,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ff",+          "salt" : "32140a66b88e1683",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "bb50b900e301b23a4801bba57e163b47",+          "result" : "valid"+        },+        {+          "tcId" : 54,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ffffffffffffffff",+          "salt" : "8a359634423ed028",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "08f89be37ee7d29f9c24c67a354effcb",+          "result" : "valid"+        },+        {+          "tcId" : 55,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "c0c0c0c0c0c0c0c0",+          "salt" : "d6f596f170ed2414",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "956f188642dfc807cb47658c203093e5",+          "result" : "valid"+        },+        {+          "tcId" : 56,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "eeeeeeeeeeeeeeee",+          "salt" : "6b2269425e288d03",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "c8b50d274f16cd66fce052a0e52f6840",+          "result" : "valid"+        },+        {+          "tcId" : 57,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "f0f0f0f0f0f0f0f0",+          "salt" : "ebf0b04633711248",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "5f4d98d86b3de83fe6a554c139256037",+          "result" : "valid"+        },+        {+          "tcId" : 58,+          "comment" : "special case password",+          "flags" : [+            "Ascii"+          ],+          "password" : "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",+          "salt" : "9de9b71eeb9d9a34",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "8458ae1b33e367c9724db9259bc06208",+          "result" : "valid"+        }+      ]+    }+  ]+}
+ testdata/pbkdf2_hmacsha512_test.json view
@@ -0,0 +1,844 @@+{+  "algorithm" : "PBKDF2-HMACSHA512",+  "schema" : "pbkdf_test_schema.json",+  "generatorVersion" : "0.9",+  "numberOfTests" : 58,+  "header" : [+    "Test vector of type PbkdfTest are for password based key derivations."+  ],+  "notes" : {+    "Ascii" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of ASCII characters."+    },+    "NonUtf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is not a valid UTF-8 string."+    },+    "Printable" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password consisting of printable ASCII characters."+    },+    "Pseudorandom" : {+      "bugType" : "BASIC",+      "description" : "Pseudorandomly generated test vector"+    },+    "Utf8" : {+      "bugType" : "FUNCTIONALITY",+      "description" : "The test vector contains a password that is a valid UTF-8 string."+    }+  },+  "testGroups" : [+    {+      "type" : "PbkdfTest",+      "tests" : [+        {+          "tcId" : 1,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7130577430643470",+          "salt" : "798acc7c76739d75",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "4935390897319c3efc15d19304109c79",+          "result" : "valid"+        },+        {+          "tcId" : 2,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "5a30673349567272",+          "salt" : "84bbd18de5ec10ff",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "d1b8e64e3b67a548c8bda3118eab1bc3f81b4cc9ead842eda18f6685533c2c7f4f49b8c7a78fea13b776",+          "result" : "valid"+        },+        {+          "tcId" : 3,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "7439315572766f47",+          "salt" : "5d76db9ca0f0bae2",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "a5d7f0fe4adc54e2ac5edc54e005827a90cbd46c00b72be68f8fbd1da98c079b98622a69b1ea44c0d94cdae03c339b742d047ac63cac0d9af59786baee4a158080",+          "result" : "valid"+        },+        {+          "tcId" : 4,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6b6f67583748447a",+          "salt" : "0a8fbd0232a3a7f6e60d6564c92ea35f",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "23cb042249cf5d03f0cbfe8726df1bee",+          "result" : "valid"+        },+        {+          "tcId" : 5,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "4939745242645168",+          "salt" : "0a3bb93c0de86a174ab005b8089706ab",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "8c9a9843f6c108ad5b95d872bd93f1afd8001871e24ae03100ab33a47b3bf923de1e0bb4a95985563a87",+          "result" : "valid"+        },+        {+          "tcId" : 6,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f4f4d37324f6330",+          "salt" : "3827c3b6b1e6de7aab096eb75e23610d",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "0159b91e667079e448ae9707f9d538fcc4f297165f6fc85b2966f951669600ee7553f2b80e72f7c2688f2f4cf0f9f7eaf070333987aa7f7d45843076e1708e1229",+          "result" : "valid"+        },+        {+          "tcId" : 7,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6761734354484546654d36526f61386349",+          "salt" : "cac9ae7ef670990e",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "4f423de7d3ce6dde25a9c88058f175d4",+          "result" : "valid"+        },+        {+          "tcId" : 8,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75375330684678774e33586f76707a6551",+          "salt" : "201b1e277f4ff955",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "f5161add0381cd85fd3816902a56f1e5f84349362d9780eca06c201d1aa976106f5c55146c06d33d131d",+          "result" : "valid"+        },+        {+          "tcId" : 9,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "51524b6a6d6d6b48524462507178696868",+          "salt" : "e71d6a702d2d54bc",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "8d4c0251d0344406a8e31261046fb4f2bdd7ef402afa5c4eb5020afc2d516ce2ceaf84a2fb904737234fdfe1b2226c5f64ec5a106ef18e9571a53b0db6a9136f43",+          "result" : "valid"+        },+        {+          "tcId" : 10,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "3732317a52704b79306b4c6d3862317279",+          "salt" : "8b9314972ef99ac98b69b6c2ac4247c7",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "68b052397d9d0009393f939d8a41030a",+          "result" : "valid"+        },+        {+          "tcId" : 11,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "75507078436d7536577a68535030553947",+          "salt" : "874d7cc9369ff9988642ffe05a7f2153",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "f6db4aec63695dfa52aa449120b118ef399ed5efa73c3f59c7aa08cd3cd0aa4401634a7bbf61d753752c",+          "result" : "valid"+        },+        {+          "tcId" : 12,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "6f39505350597a445349783365374d7063",+          "salt" : "1569909588ec25a3afa147cb7fff9fb1",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "b8b2e382821089b02c972c1ca1495bc8d021fabe77733335555c6b27c4a6e73098e8385055891e8a55c05be3d20279bb168b8011f60705928dd05d1451775b62f1",+          "result" : "valid"+        },+        {+          "tcId" : 13,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "4b2d6c600e66535c7f3d6d6c",+          "salt" : "7a4c02db8bd8fcc2",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "d3aca4f6df6e06b97c44fd295ed07965",+          "result" : "valid"+        },+        {+          "tcId" : 14,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "287e74200e6b69285f5f7e00",+          "salt" : "dddf018487fa4b95",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "1fe411d3bde75e596197040e192dd0aa56ba282125db6e91090caace7bef3b07bbd4d3170b3ca8548587",+          "result" : "valid"+        },+        {+          "tcId" : 15,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0f3b5e220e1c000c2a703a4d",+          "salt" : "7cf71b19c0daf499",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "bcc6ca2f82450f4bff09009af28215e68b01ccfcaabdf176a7eceb61974cdd9a9ce3bd10b1d7f781c4d82612fb42bf3db424912389a0727515fef785d2cc28275c",+          "result" : "valid"+        },+        {+          "tcId" : 16,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "1f4b7b354903545f2b07720b",+          "salt" : "62f91ea01f3a18aa2f122bd15c4a615b",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "26bfec4392af75d5e912d47c7d911fc5",+          "result" : "valid"+        },+        {+          "tcId" : 17,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5d7e1f70174b2f616c0d6722",+          "salt" : "4fb602173327228431dd8d088154c7ec",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "21f15760b44d879949320ecfa6fb1023c15f6284127c985ce36d83de2fa6e25fe5f98e0e0bcb89acda3f",+          "result" : "valid"+        },+        {+          "tcId" : 18,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2657324e694c6b242b174f1a",+          "salt" : "3cad67ef1eaf3aaafb5ee372d6b1ecb4",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "26c4a80bfa48325beefc3a32c543b1ed9dcb67458dba5c41b9ec1ea87ba82f52a98beaf2578f1e1d6f781676bc4664e9b629250bf0ad8440f8ceb05c51101ae8c7",+          "result" : "valid"+        },+        {+          "tcId" : 19,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "15296b0933473119180f3c0645202d5221633803",+          "salt" : "d7412e4137fc4410",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "49fa49e9d9d4b8a230b888550fa95d4e",+          "result" : "valid"+        },+        {+          "tcId" : 20,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "2a1124361d53464245580130076d6b54001e3d01",+          "salt" : "7ecaaed1eab03c3f",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "d80c7b2a6c4bfeede1926ad9fac65400078b865bc02d145fc0e045fe37cbbde4cafad121e4935f0b3052",+          "result" : "valid"+        },+        {+          "tcId" : 21,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "666252024e18233308430820127e753e6b041f56",+          "salt" : "756fafa7fac1f019",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "ca4168cd34fd8bf311c180bcf00d7c50482c89f9bd639f8e3217ebcefc99f7cc4bf7fce90a9faed87912293163d5bdb55a61566bccc7366195d72b76f3b26ad332",+          "result" : "valid"+        },+        {+          "tcId" : 22,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "7a106c2773542c791d2f0a02632b69644f666278",+          "salt" : "a36350db68186d747254bd9835219487",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "878ede7d7b80d4dfb2d7bffba76b288d",+          "result" : "valid"+        },+        {+          "tcId" : 23,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "0b3c6f070149615b5b461c7f5c3a366a6d375402",+          "salt" : "42be6c8fcd7858458fb97cece0069524",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "4d631b173437810b9dc991a9b81d2e8a4981cfed6fd6ae508db3f6f6bb4d267388741ead7d3272fd5075",+          "result" : "valid"+        },+        {+          "tcId" : 24,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Ascii"+          ],+          "password" : "5177161d237c1b64426353434113015665344a6c",+          "salt" : "596dbf5830aa8b00682429883e0b1ed0",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "a4647786a04fa4129f5c4c43ff7ca48362b9e25bc962345355f01036c5df2962fab2906218267bd04b11a1834abb965e861e2fa3d721e4dded92ed7eefed058609",+          "result" : "valid"+        },+        {+          "tcId" : 25,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0a3d1bed38acc83",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "6b62ce6976fe2c9bd0a8a7acba7b83b5",+          "result" : "valid"+        },+        {+          "tcId" : 26,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d0bcccab64cf9a",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "3b80d72751093ad48c08b70fd13308834078b52c3c14e47523e189a475b6f6678dafb991bcb5c0b73f2c",+          "result" : "valid"+        },+        {+          "tcId" : 27,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c4bddd8f4ad69d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "4bb5ebbe99297ac9a3e3b14ad1a5b52307fbdb991e9bae74561a93e7165086f4a198ad515be73ccda118168150ece8627199ff267c3132e3a556ba3d8d812d8392",+          "result" : "valid"+        },+        {+          "tcId" : 28,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "c299d292cd92dcb8",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "876ae3968ee80397415e7137d9bddf76",+          "result" : "valid"+        },+        {+          "tcId" : 29,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d086d0bed193d9b6",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "a0d55ac130b1ef52e4dc05203cb78304cc49f56dd165eafc51f009d44cddf6e4befa59d3231c2393e9ab",+          "result" : "valid"+        },+        {+          "tcId" : 30,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da8ddcacde86d3b0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "1837beaf1c4a9efe10e8f8f6dafea3e494708a2512d7b64a396ed69b419b8c7e65f6ba2809c8555b62be2640d0ae297a68136f7a3fe8e226b87467e67c6254a6a2",+          "result" : "valid"+        },+        {+          "tcId" : 31,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "cfbec3b9d7acc7a9d585d1a5c28fcca119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "1b3587d675b3170ab20fdb14f8c88e42",+          "result" : "valid"+        },+        {+          "tcId" : 32,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d29cd099d197d1b0da9aca8ec2a3daa5c38a",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "93b0fc1b0313bab41929332682d9d2a5b947fbb5bbdb37c06d897dfc4a309eff5f1b1806a6a3b6f5fedf",+          "result" : "valid"+        },+        {+          "tcId" : 33,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de89d193d18c75c2b8c6bcd190d198c3a2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "e44c3d06dc5128edd230cd9bfcbd204fde2369fb39d6370cbb0021ce1610003d7d1789e83f3fa9ed5842f1a5afde4609b3b09ef6595b7f8647c26c6ae87f4b776f",+          "result" : "valid"+        },+        {+          "tcId" : 34,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "da9dd096cb93db88dbbed495ceaacf853c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "6a294cfc91bd87da2700085a289a3f10",+          "result" : "valid"+        },+        {+          "tcId" : 35,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "de81d0a6cea9c7b3cbabce9bda90d29f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "18d49eaf798f4fab75aaadb42f8bcff45b83da5fad7932705cc829897692cedb161268aa04659ef2a1d8",+          "result" : "valid"+        },+        {+          "tcId" : 36,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "Utf8"+          ],+          "password" : "d281da97c8a3d8b7ceb9db80dc8dd4a0c3be",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "ab933595222171a6bf0ef7288f4761d4e6dd29e68e566fde1b3bb6a5ae380b591334e680ed1fc70f181c54f3a6ea17388cf25c7f18555b1f67a6837b3590578161",+          "result" : "valid"+        },+        {+          "tcId" : 37,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "8423ec7ea4ca4b03",+          "salt" : "8dfae85c9f2072ae",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "196cb574f499f6e20cd00a4eb0f70d4a",+          "result" : "valid"+        },+        {+          "tcId" : 38,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "2c3c932bf0648bda",+          "salt" : "15187b0393d8a441",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "28ea911364150649f9c5831e5d3ab76b4c4009416ba68143388d00e294437ade367fd24fbc79a5173544",+          "result" : "valid"+        },+        {+          "tcId" : 39,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "393d474fd84a259d",+          "salt" : "775bde4bd6e40ddd",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "38688a90efa49043340f480ba134e4d34f2f444c049864b918fbae5e324c7d11e5c790c4d6c4741bbedccc55f02a40a19edb117339e21fff27fd3b3b56b348403f",+          "result" : "valid"+        },+        {+          "tcId" : 40,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "48997492c3528738",+          "salt" : "99c55e182238c8e0c385447685e9ba85",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "f3d1fa6c1cb2bc8f8fae275b69746f03",+          "result" : "valid"+        },+        {+          "tcId" : 41,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "74064c3ebc53d676",+          "salt" : "42a7aca6a7664f87b405b49d62a074db",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "abb8c155cde2637d33261bd5c1db245ceb1e59e8cf223927697375bbc42273bf4f291d582885f9a5b634",+          "result" : "valid"+        },+        {+          "tcId" : 42,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "668d4f2c8f86f4f0",+          "salt" : "f3eb3938f338fe8639813beacd8100a5",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "51960fa5ea92577833bd8ef87911c7e0ace4a94fd4f76afb6ec145455257593953389c7cbe497eae1d787f95b8472b0c251406492c64c25db19ace47d24d8f5a75",+          "result" : "valid"+        },+        {+          "tcId" : 43,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "e3feb0f96dec61e9dd451465f88f132119",+          "salt" : "e9c55717a1259a29",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "2008be3283e9f6049829a4157c1c3088",+          "result" : "valid"+        },+        {+          "tcId" : 44,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "5c9ca419dc576470269a828e40a3aea5ca",+          "salt" : "5acc2d76a9f4444c",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "81a8a54d860990b96d5aa9debadc95c6164ac85294a2350b20b893e73ab7421845bc69ede73805bfcff7",+          "result" : "valid"+        },+        {+          "tcId" : 45,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "bf898453344c6875f0b8b9bc2c50fc58e2",+          "salt" : "c69c3b58917e0975",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "0452a418242f9539f8849d65466c96c984e021ddfde942a6a7e0b6dd1584dc8a536b1722684021444bf60fd958f391a5ba92fa6a44450d089faec32179f01f04c6",+          "result" : "valid"+        },+        {+          "tcId" : 46,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "9e9d7c166ad3bec856fe5d15d3aacbc53c",+          "salt" : "200bba668b2010b1968b82091848937c",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "6320a1e2cc157e09607d4fc9a4ae3269",+          "result" : "valid"+        },+        {+          "tcId" : 47,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "17812c26bba941f3c2ebdb9bb6904c9f73",+          "salt" : "36586fdefbe916369412c9f3e6337ddf",+          "iterationCount" : 4096,+          "dkLen" : 42,+          "dk" : "d42a8d13e4901d902c6ebf070fa3b253af9a0783cf5260daca193090337ad8ab783a2e46eb8022996c18",+          "result" : "valid"+        },+        {+          "tcId" : 48,+          "comment" : "",+          "flags" : [+            "Pseudorandom",+            "NonUtf8"+          ],+          "password" : "6c816697ba2366375bb986c0af0d7d20fe",+          "salt" : "ab7c17e3b78fe71e373b0ccb0fc3ccda",+          "iterationCount" : 4096,+          "dkLen" : 65,+          "dk" : "634fc75709b5d0e585269c6f31280bdc79c1aaaf72fd5a13e9848c5fc1be7caeff06fbda3fd83ec67aee6286e4411da66f1c9da5414734d5388f9f95c25a13c99a",+          "result" : "valid"+        },+        {+          "tcId" : 49,+          "comment" : "empty password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "",+          "salt" : "1a71e2118c9fbcc9",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "580ad63c3ade95c330d57e32af17fd342457fb0289b5d74c2d01ee109293bbdb",+          "result" : "valid"+        },+        {+          "tcId" : 50,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "523249584467597a5a4271363970667a4a714e744b7761545a4544494676766b6a6253417167566e456a6b456b454557504e69383653626a6e376b725764394d67",+          "salt" : "d26b99043c8ba3a4",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "983adc3df73cffc0649a9c9682498c6bacbe91980e809d0cf002200d913b2b73",+          "result" : "valid"+        },+        {+          "tcId" : 51,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "63727a466d396430795463456a6468545758693877674e516f544e6d486e61686f6956317071613133655471477933497531354b4f52516339494c53646756527a45524e6b4463723565676a62584a78426572536a74726b6b674341616a63356243354434706e66743836663754626663666370595a30767354454d4930524178",+          "salt" : "9266da5b8c102b27",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "9a3a9c839c05c455f1e83959f486b23b15f6e91bdf71b3da11bb0dd71ec98d49",+          "result" : "valid"+        },+        {+          "tcId" : 52,+          "comment" : "long password",+          "flags" : [+            "Pseudorandom",+            "Printable"+          ],+          "password" : "32647a56696e6f45774464656c656257797058314d6f4f685975585a463172514b7a32535a6c307578576377796f3561416e6f42524e7a5044763072513662693742345a34324f50695258534c6859684441643262746f647633744d54424430744b46316e4b655942656565547a70413145434150713942687a4a4c555a67737636754e4b664450333558414d684a486c736a6f5a796b677130624d506265556941796d6f324371586b64524752633876544176684e5a5838536f564d33704e74594a4a7258766975337547583233736a353847723061614a4b45763765796c373248636e6167713474766e533737626d6376676c79536d347370707a65673869",+          "salt" : "6a06903b78dae6de",+          "iterationCount" : 4096,+          "dkLen" : 32,+          "dk" : "c27d89fdfb870fe02f4e3843025d33e91fa2eff7f8a18eefe7113818d1765126",+          "result" : "valid"+        },+        {+          "tcId" : 53,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ff",+          "salt" : "32140a66b88e1683",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "73eb7888fbad86cdb299455454d26429",+          "result" : "valid"+        },+        {+          "tcId" : 54,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "ffffffffffffffff",+          "salt" : "8a359634423ed028",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "f0bdbf5507d74ab49ee220500107d69f",+          "result" : "valid"+        },+        {+          "tcId" : 55,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "c0c0c0c0c0c0c0c0",+          "salt" : "d6f596f170ed2414",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "51eaac2999bab0ba22134d21f95ea277",+          "result" : "valid"+        },+        {+          "tcId" : 56,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "eeeeeeeeeeeeeeee",+          "salt" : "6b2269425e288d03",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "f4e6be1916b7d1e21e8c4ea77c75b54e",+          "result" : "valid"+        },+        {+          "tcId" : 57,+          "comment" : "special case password",+          "flags" : [+            "NonUtf8"+          ],+          "password" : "f0f0f0f0f0f0f0f0",+          "salt" : "ebf0b04633711248",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "51baf8f1ecab753e30fd1ff995c29355",+          "result" : "valid"+        },+        {+          "tcId" : 58,+          "comment" : "special case password",+          "flags" : [+            "Ascii"+          ],+          "password" : "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",+          "salt" : "9de9b71eeb9d9a34",+          "iterationCount" : 4096,+          "dkLen" : 16,+          "dk" : "2900d1f4870e09094070d986784dbcc8",+          "result" : "valid"+        }+      ]+    }+  ]+}
+ testdata/srp_init_ok_test.json view
@@ -0,0 +1,7 @@+{+  "iteration": 20433,+  "salt": "0samK84bcBmkVsswOpZbZg==",+  "protocol": "s2k",+  "b": "STVHcWTN9YOYn4IgtIJ6UPdPbvzvL+zza/l+6yUHUtdEyxwzpB78y8wqZ8QWSbVqjBcpl32iEA4T3nYp0LWZ5hD3r3yIJFloXvX0kpBJkr+Nh8EfHuW1V50A8riH6VWyuJ8m3JmOO7/xkNgP7je8GMpt/5f/7qE3AOj73e3JR0fzQ7IopdU0tlyVX0tD7T6wCyHS52GJWDdq1I2bgzurIK2/ZjR/Hwzd/67oFQPtKQgjrSRaKo5MJEfDP7C9wOlXsZqbb7igX6PeZRWrfl+iQFaA/FVeWSngB07ja3wOryY9GsYO06ELGOaQ+MpsT7mouqrGTfOJ0OMh9EgrkJEM6w==",+  "c": "e-1be-8746c235-b41c-11ef-bd17-c780acb4fe15:PRN"+}
+ testdata/trust_data_test.json view
@@ -0,0 +1,13 @@+{+  "securityCode": {+    "length": 6,+    "tooManyCodesSent": false,+    "tooManyCodesValidated": false,+    "securityCodeLocked": false,+    "securityCodeCooldown": false+  },+  "trustedPhoneNumbers": [+    { "id": 1, "numberWithDialCode": "+81 •• •••• •34", "pushMode": "sms" }+  ],+  "noTrustedDevices": false+}
+ testdata/trusted_devices_test.json view
@@ -0,0 +1,10 @@+{+  "devices": [+    {+      "deviceType": "SMS",+      "areaCode": "",+      "phoneNumber": "*******58",+      "deviceId": "1"+    }+  ]+}
+ testdata/verification_code_ok_test.json view
@@ -0,0 +1,3 @@+{+  "success": true+}