diff --git a/Makefile b/Makefile index 1413ff10e82..f2836a52853 100644 --- a/Makefile +++ b/Makefile @@ -49,8 +49,8 @@ install: init ./hack/bin/cabal-run-all-tests.sh ./hack/bin/cabal-install-artefacts.sh all -.PHONY: clean-rabbit -clean-rabbit: +.PHONY: rabbit-clean +rabbit-clean: rabbitmqadmin -f pretty_json list queues vhost name messages | jq -r '.[] | "rabbitmqadmin delete queue name=\(.name) --vhost=\(.vhost)"' | bash # Clean @@ -59,7 +59,7 @@ full-clean: clean rm -rf ~/.cache/hie-bios rm -rf ./dist-newstyle ./.env direnv reload - clean-rabbit + rabbit-clean @echo -e "\n\n*** NOTE: you may want to also 'rm -rf ~/.cabal/store \$$CABAL_DIR/store', not sure.\n" .PHONY: clean diff --git a/changelog.d/0-release-notes/configurable-argon b/changelog.d/0-release-notes/configurable-argon new file mode 100644 index 00000000000..b9e2a74cd8c --- /dev/null +++ b/changelog.d/0-release-notes/configurable-argon @@ -0,0 +1,18 @@ +Password hashing is now done using argon2id instead of scrypt. The argon2id parameters can be configured using these options: + +```yaml +brig: + optSettings: + setPasswordHashingOptions: + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... +galley: + settings: + passwordHashingOptions: + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... +``` + +These have default values, which should work for most deployments. Please see documentation on config-options for more. diff --git a/changelog.d/2-features/add-config-for-pwd-hash b/changelog.d/2-features/add-config-for-pwd-hash new file mode 100644 index 00000000000..79ba9c55f09 --- /dev/null +++ b/changelog.d/2-features/add-config-for-pwd-hash @@ -0,0 +1 @@ +Allow configuring Argon2id parameters diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 4e1e5393a2e..7c732c7b590 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -368,5 +368,6 @@ data: {{- if .setOAuthMaxActiveRefreshTokens }} setOAuthMaxActiveRefreshTokens: {{ .setOAuthMaxActiveRefreshTokens }} {{- end }} + setPasswordHashingOptions: {{ toYaml .setPasswordHashingOptions | nindent 8 }} {{- end }} {{- end }} diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 06da5a19401..bba7408c6a5 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -16,7 +16,7 @@ metrics: enabled: false # This is not supported for production use, only here for testing: # preStop: -# exec: +# exec: # command: ["sh", "-c", "curl http://acme.example"] config: logLevel: Info @@ -150,6 +150,11 @@ config: setDisabledAPIVersions: [ development ] setFederationStrategy: allowNone setFederationDomainConfigsUpdateFreq: 10 + # Options for Argon2id version 19 + setPasswordHashingOptions: + iterations: 1 + parallelism: 32 + memory: 180224 # 176 MiB smtp: passwordFile: /etc/wire/brig/secrets/smtp-password.txt proxy: {} diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 85c93804ebe..cf1426e8adb 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -96,6 +96,7 @@ data: {{- if .settings.guestLinkTTLSeconds }} guestLinkTTLSeconds: {{ .settings.guestLinkTTLSeconds }} {{- end }} + passwordHashingOptions: {{ toYaml .settings.passwordHashingOptions | nindent 8 }} featureFlags: sso: {{ .settings.featureFlags.sso }} legalhold: {{ .settings.featureFlags.legalhold }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index f169bb0e93d..877a2734039 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -70,6 +70,11 @@ config: # The lifetime of a conversation guest link in seconds. Must be a value 0 < x <= 31536000 (365 days) # Default is 31536000 (365 days) if not set guestLinkTTLSeconds: 31536000 + # Options for Argon2id version 19 + passwordHashingOptions: + iterations: 1 + parallelism: 32 + memory: 180224 # 176 MiB featureFlags: # see #RefConfigOptions in `/docs/reference` (https://github.com/wireapp/wire-server/) appLock: defaults: diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 9808bfec21e..ae843566522 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -707,6 +707,53 @@ optSettings: setOAuthMaxActiveRefreshTokens: 10 ``` +#### Argon2id password hashing parameters + +Since release 5.6.0, wire-server hashes passwords with +[argon2id](https://datatracker.ietf.org/doc/html/rfc9106) at rest. If +you do not do anything, the default parameters will be used, which +are: + +```yaml + setPasswordHashingOptions: + iterations: 1 + memory: 180224 # memory needed in kibibytes (1 kibibyte is 2^10 bytes) + parallelism: 32 +``` + +The default will be adjusted to new developments in hashing algorithm +security from time to time. + +The password hashing options are set for brig and galley: + +```yaml +brig: + optSettings: + setPasswordHashingOptions: + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... +galley: + settings: + passwordHashingOptions: + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... +``` + +**Performance implications:** scrypt takes ~80ms on a realistic test +system, and argon2id with default settings takes ~500ms. This is a +runtime increase by a factor of ~6. This happens every time a +password is entered by the user: during login, password reset, +deleting a device, etc. (It does **NOT** happen during any other +cryptographic operations like session key update or message +de-/encryption.) + +The settings are a trade-off between resilience against brute force +attacks and password secrecy. For most systems this should be safe +and not need more hardware resources for brig, but you may want to +form your own opinion. + #### Disabling API versions It is possible to disable one ore more API versions. When an API version is disabled it won't be advertised on the `GET /api-version` endpoint, neither in the `supported`, nor in the `development` section. Requests made to any endpoint of a disabled API version will result in the same error response as a request made to an API version that does not exist. diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 83cd888dbf9..d6db927d92d 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -134,6 +134,12 @@ brig: setOAuthEnabled: true setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks setOAuthMaxActiveRefreshTokens: 10 + # These values are insecure, against anyone getting hold of the hash, + # but its not a concern for the integration tests. + setPasswordHashingOptions: + iterations: 1 + parallelism: 4 + memory: 32 # This needs to be at least 8 * parallelism. aws: sesEndpoint: http://fake-aws-ses:4569 sqsEndpoint: http://fake-aws-sqs:4568 @@ -258,6 +264,13 @@ galley: federationDomain: integration.example.com disabledAPIVersions: [] + # These values are insecure, against anyone getting hold of the hash, + # but its not a concern for the integration tests. + passwordHashingOptions: + iterations: 1 + parallelism: 4 + memory: 32 # This needs to be at least 8 * parallelism. + featureFlags: sso: disabled-by-default # this needs to be the default; tests can enable it when needed. legalhold: whitelist-teams-and-implicit-consent diff --git a/libs/types-common/src/Util/Options.hs b/libs/types-common/src/Util/Options.hs index f82600dc00b..7b6cd88b08d 100644 --- a/libs/types-common/src/Util/Options.hs +++ b/libs/types-common/src/Util/Options.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. @@ -146,3 +147,21 @@ getOptions desc mp defaultPath = do parseAWSEndpoint :: ReadM AWSEndpoint parseAWSEndpoint = readerAsk >>= maybe (error "Could not parse AWS endpoint") pure . fromByteString . fromString + +data PasswordHashingOptions = PasswordHashingOptions + { iterations :: !Word32, + memory :: !Word32, + parallelism :: !Word32 + } + deriving (Show, Generic) + +instance FromJSON PasswordHashingOptions where + parseJSON = + withObject + "PasswordHashingOptions" + ( \obj -> do + iterations <- obj .: "iterations" + memory <- obj .: "memory" + parallelism <- obj .: "parallelism" + pure (PasswordHashingOptions {..}) + ) diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index e5e2290b576..9c397736cc2 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -70,7 +70,6 @@ data BrigError | LastIdentity | NoPassword | ChangePasswordMustDiffer - | PasswordAuthenticationFailed | TooManyTeamInvitations | CannotJoinMultipleTeams | InsufficientTeamPermissions @@ -254,8 +253,6 @@ type instance MapError 'NoPassword = 'StaticError 403 "no-password" "The user ha type instance MapError 'ChangePasswordMustDiffer = 'StaticError 409 "password-must-differ" "For password change, new and old password must be different." -type instance MapError 'PasswordAuthenticationFailed = 'StaticError 403 "password-authentication-failed" "Password authentication failed." - type instance MapError 'TooManyTeamInvitations = 'StaticError 403 "too-many-team-invitations" "Too many team invitations for this team" type instance MapError 'CannotJoinMultipleTeams = 'StaticError 403 "cannot-join-multiple-teams" "Cannot accept invitations from multiple teams" diff --git a/libs/wire-api/src/Wire/API/Password.hs b/libs/wire-api/src/Wire/API/Password.hs index 0935b4ca5a5..78f2ea0697f 100644 --- a/libs/wire-api/src/Wire/API/Password.hs +++ b/libs/wire-api/src/Wire/API/Password.hs @@ -26,10 +26,10 @@ module Wire.API.Password verifyPassword, verifyPasswordWithStatus, PasswordReqBody (..), + argon2OptsFromHashingOpts, -- * Only for testing hashPasswordArgon2idWithSalt, - hashPasswordArgon2idWithOptions, mkSafePasswordScrypt, parsePassword, ) @@ -52,6 +52,7 @@ import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Imports import OpenSSL.Random (randBytes) +import Util.Options -- | A derived, stretched password that can be safely stored. data Password @@ -120,19 +121,6 @@ defaultScryptParams = outputLength = 64 } --- | Recommended in the RFC as the second choice: https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice --- The first choice takes ~1s to hash passwords which seems like too much. -defaultOptions :: Argon2.Options -defaultOptions = - Argon2.Options - { iterations = 1, - -- TODO: fix this after meeting with Security - memory = 2 ^ (17 :: Int), - parallelism = 32, - variant = Argon2.Argon2id, - version = Argon2.Version13 - } - fromScrypt :: ScryptParameters -> Parameters fromScrypt scryptParams = Parameters @@ -142,6 +130,16 @@ fromScrypt scryptParams = outputLength = 64 } +argon2OptsFromHashingOpts :: PasswordHashingOptions -> Argon2.Options +argon2OptsFromHashingOpts PasswordHashingOptions {..} = + Argon2.Options + { variant = Argon2.Argon2id, + version = Argon2.Version13, + iterations = iterations, + memory = memory, + parallelism = parallelism + } + ------------------------------------------------------------------------------- -- | Generate a strong, random plaintext password of length 16 @@ -154,8 +152,8 @@ genPassword = mkSafePasswordScrypt :: (MonadIO m) => PlainTextPassword' t -> m Password mkSafePasswordScrypt = fmap ScryptPassword . hashPasswordScrypt . Text.encodeUtf8 . fromPlainTextPassword -mkSafePassword :: (MonadIO m) => PlainTextPassword' t -> m Password -mkSafePassword = fmap Argon2Password . hashPasswordArgon2id . Text.encodeUtf8 . fromPlainTextPassword +mkSafePassword :: (MonadIO m) => Argon2.Options -> PlainTextPassword' t -> m Password +mkSafePassword opts = fmap Argon2Password . hashPasswordArgon2id opts . Text.encodeUtf8 . fromPlainTextPassword -- | Verify a plaintext password from user input against a stretched -- password from persistent storage. @@ -190,16 +188,13 @@ encodeScryptPassword ScryptHashedPassword {..} = Text.decodeUtf8 . B64.encode $ hashedKey ] -hashPasswordArgon2id :: (MonadIO m) => ByteString -> m Argon2HashedPassword -hashPasswordArgon2id pwd = do +hashPasswordArgon2id :: (MonadIO m) => Argon2.Options -> ByteString -> m Argon2HashedPassword +hashPasswordArgon2id opts pwd = do salt <- newSalt 16 - pure $! hashPasswordArgon2idWithSalt salt pwd - -hashPasswordArgon2idWithSalt :: ByteString -> ByteString -> Argon2HashedPassword -hashPasswordArgon2idWithSalt = hashPasswordArgon2idWithOptions defaultOptions + pure $! hashPasswordArgon2idWithSalt opts salt pwd -hashPasswordArgon2idWithOptions :: Argon2.Options -> ByteString -> ByteString -> Argon2HashedPassword -hashPasswordArgon2idWithOptions opts salt pwd = do +hashPasswordArgon2idWithSalt :: Argon2.Options -> ByteString -> ByteString -> Argon2HashedPassword +hashPasswordArgon2idWithSalt opts salt pwd = do let hashedKey = hashPasswordWithOptions opts pwd salt in Argon2HashedPassword {..} diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index e1f92b07998..4c8282f8d71 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -151,7 +151,6 @@ sparResponseURI (Just tid) = type APIScim = OmitDocs :> "v2" :> ScimSiteAPI SparTag :<|> "auth-tokens" - :> CanThrow 'PasswordAuthenticationFailed :> CanThrow 'CodeAuthenticationFailed :> CanThrow 'CodeAuthenticationRequired :> APIScimToken diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 6e037a0489a..13398e9a049 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -35,6 +35,7 @@ module Wire.API.User SelfProfile (..), -- User (should not be here) User (..), + isSamlUser, userId, userDeleted, userEmail, @@ -584,6 +585,12 @@ data User = User deriving (Arbitrary) via (GenericUniform User) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema User) +isSamlUser :: User -> Bool +isSamlUser usr = do + case usr.userIdentity of + Just (SSOIdentity (UserSSOId _) _) -> True + _ -> False + userId :: User -> UserId userId = qUnqualified . userQualifiedId @@ -1418,8 +1425,8 @@ instance (res ~ PutSelfResponses) => AsUnion res (Maybe UpdateProfileError) wher -- | The payload for setting or changing a password. data PasswordChange = PasswordChange - { cpOldPassword :: Maybe PlainTextPassword6, - cpNewPassword :: PlainTextPassword8 + { oldPassword :: Maybe PlainTextPassword6, + newPassword :: PlainTextPassword8 } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform PasswordChange) @@ -1435,9 +1442,9 @@ instance ToSchema PasswordChange where ) . object "PasswordChange" $ PasswordChange - <$> cpOldPassword + <$> oldPassword .= maybe_ (optField "old_password" schema) - <*> cpNewPassword + <*> newPassword .= field "new_password" schema data ChangePasswordError diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs index f376b73704f..2fcca87a73e 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs @@ -24,8 +24,8 @@ import Wire.API.User (PasswordChange (..)) testObject_PasswordChange_user_1 :: PasswordChange testObject_PasswordChange_user_1 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\SOHf;0B+CKY\1040633W\ENQ\178683\66681\1079258\1036336f\SOH&\166643\1050584\1022602\1091853\145882\tX\190821\FShl\1020866^A\153646=\151238\EM_ow?\\h4\155435\68388\SYN\11851\&4O\NAKnJT\EOTr\CAN~\ACK\ETBR'\992244sX\1001766mlCHvg\1112425ba\1003664R\\\1034092o\989312\1056334*k\r)H\180403\1051096\n#\14366~\\9Q|\v;\USd.\1066580\&0SHP%\1019462\22215'!\1044148N\SUB!Kz@\NUL\74079\1087771\SUBVp\1100111\38836\STX3#\DEL\DC4}}\1094237N\120442`\169346\&7\1036101\DLE\154725^\STX{`i:\rUT!\DC3\1111700\152543\NAKWK\NUL\1098445\1102182eA\140938\ETX\172001\1034473t@?\1014650\SOHJ\1074486\&7\RSg{\78258\&5R_\DC3u\SI\153435\1082441`}\DEL\66836X\DC1\175200D\25079b\176836\&6T\141840\167124p*7\n\\'\vO#\FS\174827(H\NAKn\178850\1015713}2s\143401\&8GA&\1004513\CAN\1068132d\9056\SUB\1059104t @\1056816I/\175842\30192\DC35\28889c\EOT\1046281\22594Uk\SYN\DLE\1099103\&8\GS\1034138\94316R-x\999901\1007697\1008634\DLEO,Z\ETX\1073959\63275f*\f^>\EOTD\r\SI_AQPO33\96451/F\RS\185177y\77854|Fn\1010492E<\1047147\&9\ETX[y`e\168776\65402L\SUB@4i/*\1011887\1102541\9070Ih\SIC1\1031432\t%?kFt\ACK\DLE\US\GSN\171039\f\1094027:\aV\ETXj\18014\SYN\SYN\150071\EMK\1083674\162115\40502Uez)\1080936\FS)8vT;\GS\21613ay\ETX\SI\GS{C=\EM,\SOH\ETXO\162859\ETX&\SOH2%<2s\f\SYN\r6ivo{\1028087WN\1053937R\1039894\1030129\995717\98891[ :\USu\180666^f\1087790\CAN\137895\183333N\SI\145270\EM@pK\1078668\&9\r@Ze\152611f\DC2x\59319M\30205;j\SYN\29669K_~:v&Dpx~_\STX:b;bv\DC3=\14812\&6\SUB\41242\ESCy0Ho.B\"u*{\1018548Vw\SUBW\138263\173995rbY\51982I{q\1041374\ENQ&_Pt\182926'\917559\&5\v\150891\35898\35323Ue@YM\164633)\n\EM\GSn\EOTZ\SO\DEL\\\f`f3T+_\RS@\a\RS\186662}" } @@ -33,12 +33,12 @@ testObject_PasswordChange_user_1 = testObject_PasswordChange_user_2 :: PasswordChange testObject_PasswordChange_user_2 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "7\16927K>\97741\186669m\vG9\tO]kp\63012\SUBVQs\t\984613\1108746\ENQ\1021022!O\998098\EOT=\abrgK_D\1033730<\SYN_\1100470\1086629\ETXH\SO#w9" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\r]Gy3T\1026217\EM\1020078|R\\@\1056800Xq\155479t\EM\NAK\45450\1031406GU+p\1028583\1037856G46\1111047\a\145730u\EM\SI)@\2452\nk7\989251\22005D\11178\1075520\1105369\&7,h\154963\r\1014527\&3\a\13276ki\SIuUB3=X$\138590]\1046903\bSaAr8*t\DLEX:\1023144KA\SYNu$^rK~`\1062546)\174565MJ\1062282\1020633\SOk\SI\EOTF]\DC2\997860\b\CAN\f=p\1041758&S`\b^\179839;S\\\DC4N:\SO\f\NUL\1076187\&5f\127761~K/\ESC\137715*:\1033030\ENQB5\158024\NUL~m\DLE2\12820\1079647\NUL%\DC1{H" } @@ -46,12 +46,12 @@ testObject_PasswordChange_user_2 = testObject_PasswordChange_user_3 :: PasswordChange testObject_PasswordChange_user_3 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "\DC1\1008131\"iQg2?.V\nR\ad#NZu\SI\154091\"B\USm\1066170\DC4?_-%\SO\DEL,\b\SYN\78542\1070480%U\RS\95262%1\2330\STX#\SUBV-\163363J\142686O\ETXV\DC2Ga\DC3,\1094317O3J\1098970n0\1052934j|\23339cF?\1019037x-\1069855\1094636\&160jp^\179153\FS>\\&\ENQ\EOTg\62450]\1073387\RS\169810\US%\990256\1042714$\985984R\1044140'-^I\1083467 kT\bZ\999047F\t\1084750F8R7\SIYN\EOT:N\SYN\SI\vd\57930Uo\1017473\1052974\vi/KA/\1004923\1051639\DC1e-\47612E\SOH\SO\v\SUB\1057038c\1090019\1003618Z#\991058e'\RS\120431\"\CAN\EOT\SYN?wO\1084580\DELI\2368\1005674\1041651gYJ\147444\&9p\CAN\187441Rn1\187124A\GS1x)\146547k\23622\DC1%S\1016329C>\134586\19597G6\1003504\RS\97878~\996492avKH\GS<\1082858sNVe\7956\152082\DLE\188847\f\ETBmpc'Xi&\150774E|V\1073099}\"\NAK5\96146&\t\f\DC4l|p\a\1024356\1036737UOM%a/9\r\n\1095590\1055708P*K\1073690%\NAKyXE\165112\987387L\DLE$f\ETX\DC1L\\l\11245\49768\\Q\SI\1002707()\58946w.\172820\SI.&\31267nk\vF\143976:\1038638\2606\1016120\SO\RS^\ENQ\DC4\b\1035479\1045289\RS\EOT\EME\1072274I\"W\1104244l\ESC\131418fB\23703+R\1113063\59494?\1061998^TD\46012{k\181947n\60196[|g;\71853\1095649\18432\173156\16164` \9356\1082477\174851dT\1015692p_\13046tN._\1042851\\\52588T%\98330\DLEC\96142\1019008\9148D\NAKrPk\170211\SOwLe\1032698\20202\1022050Jj" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\EM\GSjg\rcpq\ENQydjzBvM\EM-\181560x\62219qO[p\RSF\1043280\&2\DC4\160364\1019066\EM'L+z\SYN-K\94385\&0\ENQ\US\ETXP\DLE\ESC&h8\141548\1084128c\143493\1009984 \a~=|hx\1031253)V\152928J\991022!@;*_\US]\ESCd\FSI_\nK[\DC1\b/dS\1020193v1(h\ETX\152908-UL(U\nm\1062628\\\1049985\t>\FS\EM\190594~*$\1056230\31211\148228\991805$ch\ACKyCFOIo\DLEvHeF%\168128\&8w3I-\77839(\177181\161298r.\998529s>\155909@\ACKb\EOTa\DLEf\68669_;[-.\1058443q\GS9\SI\145931U\1085428\CAN\ETX\SYNbfMq3]N\160390of?\987479\&2QU#cY\DC2\ETB\a\134728\&8c`\DC4-\1035600\&0_,\61186\DELd^\DELM\1082727\&3\NUL\SYN\DC2`\DC2(z\1073614R\1073511\158846Vqn\94033\CAN\186179Ap,\68655~:>9\SOH\986818L|\26590\984726\&6\1020946c\31513^\1077430\NUL#\68875\7357SD\t0\GS^P\nmg,oVnT%\1074906#\1079052\185568f\32331\FSG\NUL\aPl_\EOT \1071732x\DLEZ\EM\SUB\DLE\1082444\CAN\9126\NAKnSq^lw" } @@ -59,12 +59,12 @@ testObject_PasswordChange_user_3 = testObject_PasswordChange_user_4 :: PasswordChange testObject_PasswordChange_user_4 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "\996042\1013508\137442y8z\188910cH@ge\96750\bJ\ETB\RS|Z5j\f\USJ\DC3\27867#\5822 =\ETX[[#ch1T\SUB\1062971\159509Fr#\CAN\1067286=Q\1110054\162733\n\RSL-X%\1070501h\1065080\1089630xDM'\12493aC@n \SYN\RS6\"e\985673\1062591\ENQE'H\DC3\131230o%5\996218\DEL{\1010852/KU\STX[\153798\1110733/#\1111047H\DC2_bNy\1099854\vH\RS]\986900wdg\1006378\ENQ4|\991191%\GSzgvb?QC3U\vOL\1090175\1009217\171249Us\985275\1007556\1056022y\ETX\1006666a\1089443\1064461IQ\NAK\1102475/\1025821[\146525Y\1110273\&0hg\NUL~:x\DELd\fZDQU\SO;\a9m\\~\f\167899=*|0\1089233\40380R\FS^\70516B2\DC2\1019556y\a\985058/\129335@>Vh\40618\1019580\DC1h4\n;Q&P\DC2A,f(B\SOH\1028143 \138873\1052427\f\140570$3\158205\t\t\DELs\133507Vp\SUBnDA\nsv\151492!'\1098710\144726X\r2\139117r\186851!@\51165\DC2\1073571%\1026015o\"\bi\1075769tV*\1089261\1000193\SYN\52519\1026058i\"+jB-g\40752\RSL\v'\1089204Faf\988489^\997807\69921E\fo\1041666\1032996_\1042556'\1071888\&9 ,F\95367d\121251\161394\DELY\7850)\n\RS(^\"L\GS\993283\1028777.@\DEL0\DC2,w\136018\ENQ:U\US3\1074021(\26102B>\SUBLh}8\36317\1071795\&1\DC3\FS,\NUL\1036218\164959\ENQ\1101169:\1105205J\1060042\n\NUL f0O\1023842m\36567\ETX\b\STXg>bl\1028623\44691p\SOH\45834\ACKE\NUL:fQC!K\1013456\32733(Va]\FST&B\EOT\b_#`\1041118o\DC1\165469\CAN\DC1>\138365H\1018054^\983454\SO\1088879\1112501H_a\1019703M\1094145taIx!c\64005\ACK\GS$i[\147426r=\ETB\30388Dbpc\GSt\96715\51391\25397\1098750H\1008635U..\160586\136531K\131733M`u|V\1083030#s\7110v\EMP\1008700h:H\ni=\150174\49091\ACKK\63386A\SI3b\EMd\EOTk.t\FS]f\132877\ETB\22782\DLE\f\1013087}6\17773l\\\1063285D3\DC3\USa?FR sHm\ETX\1105953\b[\DC2\STX\1091150\78391O|#\STX\GS+\145799\1109990Tf\6422\1036975`\SYNNL\RS\144764\&9\SYN\97231\988154\EM\1019553\ENQ\989472.DKMf\991253}c0\US\rFZ\1025650.\1068209SK\DC3Isq$>\128748\149897^+\1101484\1014800\n\ACK\"^\177274N2Uo|\GSM\27950|nZ\1078716G\tQ\41315\1068764zzGp\FS(y\22194\13258Wg\1110206\15989\ETB\a\142998\83001K\1041605\140118\138647\1044203G\1017800" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "6%CdZ\NAK:]a\160757G\1100807\aHT_\ETX\184817=\1094974H\NAKV\n\188284\43568\SI`\GS4},m5C,)pOU-z,m)G\1085731:\13371\68388\58925\NAKVBlt:'\SYNr\161012\&9\SYNZ~\53239\r\131378\"\b`}l\ENQg\49807x\EMsO\SUB\r\ACK\ETB\1012862*\119158\ACK'\NAK0u\1063315+.N\155568\&5\DLE\996563\1059464\1095806OE[\1066634cJJZ.>#mY=\DEL \SO\1020809\161961Fi~8JT\RS\t\DC2[E\70439\SYNO\SI@\1012929J\STX=\DLE\SYN.\1056562#da\1101967\SO\FSrCR\SOH_!\ENQ-Wtm\140222u\STX\1093627\&1\SYN<\1086071Y\99519F\1092290\174518\165124,H\152431 \1016376\1086967\65320\1078045\100936\161880\64562\n\RS07e\DC4.\1017260\USl-W\1036127\169524_\rSidOZT%46Me]\bf3X\SUB\30968\r]E%uW\1037702\120955c`\SI\1084987 a`/\DC3\1066414}\EOT5\EOTCMY\SUBY\1018010l>xH\RS\169677\26707}vy\SYN\DC1 A\FS\15039\&59N\29728\1000117\SUB@\1007505G\187702!xi\59210\&2JK\ESC\a^YMk\CANQ\t0:\DLEzo\NUL\DLEx1gU\SI\1005915\&8\142146P3V&\146215l)A\168185>\SI\tz\40878R\171716,\rLb\187682\CAN\983254_G\1019834\1008637EY;\20022\DELNvs9fmb#\1103912\46381g\1086578\54419\986014fJ\60290\v\1003578\180699SFA\STXA$\188361\135582\NUL\RSA\1069366E\SUB~\997873t%.P\t\SOHN\23780=\1058283\ENQ*\42808Fm\987705^gW\STXBN?\1062464`AUpn\SO\58276i\ETB}\NAK\35802\&8GN\71264FAxE\\&q^al$\1099577\DC2`z\67120\131492f\ETXnux\149811q\FS\CANy\ACKb\1075992\61816\nWZ\24019oFZ{to\EOT\a\58806\b.\141033\1061510/'\\bL\ETB{fp\983623\1076286O\46626\GS8\1055057\1088721;Z|< \153326\1088059\1111453\&4aC\SOH\161524\SUB.*K\"\"\129454Y\167276v\986403c($`\SOHK4d\125249\FS\122897L\992931\EM\1063797/AnK\163512\&4\44876:\FS\1071653\1048482$\DC3/Ug\143227iyBpz\CAN\ESC\50988M\153299\t)p?\160170[{K\1064379C\187515/i\129567\1015971k\SOVyO\EM\1027000_\SYN\1092978\137534\37394\ENQ{+\150519\CANp\EM\120158\DC3\1039610}\ETX\ACK\rpf&:\SI\EM{[\47214\141578Pj\DC2\1042947\175183;\tz\13562\&57*\ETX\149429\a\1099670`\rM\b\1065597\a\1061713W5\146248v\61801 \63453>Z\127207\177364t\SOH\99385 \24048@Vd\1098979'6`/\RSv\ENQ<\EM\1046071:74s[\SI^rcI55&\DC4(\1044403}5\1072105\t\SUB\1019144g\1055613\ETX[\1049131\1027231\v[i\1106618\ESC$\574\31775#\bq\1086447T@8\183810\1018524\1080923\DC4.o`\f_27^6>\1018938\20504s\175505q[\161155aeG\1042361HB[\FSs\92188t\RS2)[Qc?-\1006821/z\993159-\US.k\32238\DC4Bc\72192c \b1\SOHCE\DC47\171040\ESCw2.{\1014032~|,\EOT^\1106499}x\1099466\ENQL>>P\168482,.T\1049248`\1106998b9u.(z=@&b|\1039337\DC2\21581\ETX\SI~\vz\159863E`\FSe\US\15482=\"QwNN\129353lzq\190036eiq\SYN-d\137123.-n\SOH?B( T=wf\995467\RS\DC2\179872\SYN*|\147417DM\37567*:\STX\189754AS\t7o\64289(\994294O~kP\68006z\f\ESCe\a\987232I\DC4\DC3!EL+G[\r[_\1091777\9539W<\131337\1098445iA\168912'F\RS(SE\SI^\14294l\1054709\US\DC2\SOH\n\41372j\DC3\156318c\17177DN%\140618&\1004034\ACK~^\35003\58010\EM\991741&D\156963\NAKP\SO,\SIk#\bb\33222\t\33164W\54708\b\169426li\155619my\GS\133694f\US\58936z7fFkx\181089=\96578\ETX\1045516;pk\1103897\1096717.\SOH9j\1106500$\1083366\DC1oc" } @@ -72,12 +72,12 @@ testObject_PasswordChange_user_4 = testObject_PasswordChange_user_5 :: PasswordChange testObject_PasswordChange_user_5 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe ".vT\149065\158692){\74556g\1110206\1091301s\1012653\157052\ENQ,~\ENQ\132963\ETX\SYN\EOT\1103480\155219=1\DELo\989094\ENQ{}S;\DC1 \NAK\987299Y\129547Z\GSqPuPV=I\FS\ETX\1043494\60432\SOmRV` '-#\RSGcv]\47869>~ \ETBN\DLE\155012\1109063\181243\1002700\NAKF\CANu\US\43300\78315\DC3\1075822>z\\.k\SYN[?\\\100534\&3TJWN\1033469\\\23429=PJh1\991408\1081195\47549\DC1\1021540\1100099\36799\99980yyf\SI{%a\CANK\165733?Wl%\185431;Y\"\177839~\SYN\124986n2*F\983249\&0\63886Z\ETX\SOnp\t\1008554rz=x\DC3N\50460*8\tj\1085763\1069586\1021364R>\1094815\ACK\1052450Q\US\1016757`kE [\94447;\94463c\bK\f\1003111-WW\SOHr\SOHndW=s\1064135\FS\SIO\176630\142291\1022975l\14890<\SOGx'Z\41402\26364\1054258\STXW\1047089\1022246hS\144850\EM\134018k`mW\58467\25020\&9\DLEE\995366XON+\DLE`]\SO~g\1044869,9\t;\DC3\1050886\3363pD6s\157184\ENQeem\1045132\SOH\24377Lo\1082536ctA\DLE\1113917`B\EM\94062[1<:}7]&\v\44512R\177157a\RS\63093\&6\FS\10794f\ESC\1076238\52233v!t\DLEG\1015620\\f\t^\SOHB\SOH\180364_N+\v\ETXux\NUL.d\2283\STX%{\120714\1085733\134796\1048671uO\1061770\n\EM\r\a\r\36309\DLE4\1043749Hp\1091440q\1079376bJT<\STXVw\985328I+\1034709C\t\27376\SOh.,\1103086:\917965\9480{\ETB\995773zqY\STXE\GS\51683\&8vF}\170082X\42566\983317U\NULWGiN*v\173195\162226\154581\fR?i\1049259\DC3\a\DC3O(\187320xa\NUL.\133821\1058197\1098767j`\"\64700V\176930\69639M+m\STXJx\FS\GSrVs\SYN\GSJ8Q>l\tJj\EOTDGHj\CAN$X\RS\119922D%\DELV\EM\SO8\988454Z%ah\1074629\2919KB\1036581\ETX\SIP\1041071B\142456\ACKe\1093894Re2\1077169}Q:\1006282C\ENQ\1034308<\170708yS=qL\SUBd0{a\2279s\1075662R\1019777\133916NS7\SYNG\1052457N6Z\1026683\1010570\36133mP\DELO\RS 3\1004867G\96938:,\991792\US\1040258\ETXpNgH\"i\190411\169538\CAN50H\RS\51809@jiHF\18488\1089326S=#\EOT\24653&M\186999\ENQ\188436sB\EM\NULVuJ%wk\US\USJY\US\SYN\DC4@[\133710\2562\1102116\170261N|\25196L/Fs\EOT\b~khlJ\\*\1083562tv3\STXsg\ENQ\DC2c>\48829X\985867\1024387\nRg\NAK ;\51240'{~\\\1070452oSr3\DC1P\998414K,\1058087zN\r8\27838\\\165356\SUB@\DC1\SYN\vF\DC2V\ENQU\1077217\ENQy\1105981alR_\73963W\SYN-V\DC3\1058513|\RS\NUL\14311\1069223\DELV0\aHa\162915?PXj\SYN\DEL\985879\49021&t\"V\57972hA\42234\t8\NULk.\189070 \1112762\ETB\59270\185654U\GSQ\1063565\44619p\1061081\GS\DC3\1108003\NAKKR`\57737\40884\&6\r\101067io@o\ACKkrla\174009,\1070019\&0A6#\CAN=\DC2\DC2\161497&\RSan\149845\&6y\SYND\22050a\f\149068h\162218\&3\ETB\178246#O*\ESCy\168142a\5632b\DLEL:\axng6\59689\&1\1040365\181996\65902\ETB%\164339\ENQtfJq\1045673\&2T\SO\DEL\126474q\NULq#\1012957\1002852\\\r\DC2P\1024058(C\1050472Ph\GSBthwz+'\EM" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "S\1034541[2\119932\&0h!\1064848.\987603*P:\NAK(\GS\ETB\24253$X\179274K\NUL9\b]\170369\1020647\1051557K0\138808\DLE94_\26880\SUB\994542a\176494((\998954TXm\DC1_}\994198m\\o\120794yJ\ETX#\b.K,\151241\126477\r:bj\158134.\14517[Dg\49015\152214BH mH\181369\990387[QnJ\EOTo\\\98736*r\CAN\984313m\146285s\SUBD\17341{A\78451p[\131098U\RS\ETX\"%Y\1089637\v%\21671\1105935e|\67637\DC2\ACK,}\176528u&\v\1067595\US@8+\917796?shAqmaA\DC4I\NAK\988836\SI\SUBl\at\1097599\14469vd\187527s\ETB\SI1,\1026043\1092581\68088\10003U#\NAK\NUL8\993973\SOH\165172\178585\&25L7l4K\ETB\ETB\US\183298(\141108\CAN\SYN`!=d\16001\DEL\37607\990640g \1007747\"\DC1\1035551)\ENQG\1075268JZ3\29025/\147766\RS\ACK\28620\DC2\DC2i\DC3\NAK)\DEL\ACK\NULXs`\15691MUmZ!y3\1107617\188523`n\USs<)n9\1030989\GSB\1029508g\1055800\DLEz\ETB\110827T3F\140208\&3\1088347\SOHnx\a\57612\&07\DC4;H%\SI\SYNsQS{%\tj\46313czj\DEL&\DEL\r\1044089\165481\190596,\12670L" } @@ -85,12 +85,12 @@ testObject_PasswordChange_user_5 = testObject_PasswordChange_user_6 :: PasswordChange testObject_PasswordChange_user_6 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "&\1107610,\EM\DC1\SO\GS\NUL\\Q\143835?&%e\751K\991656g\142735]6\1072568XDu\989822.N-Y\SYN\118870\NAK\177961\1082599Z\1067051\41571\"IIzef\172747\157154\1100946\US+h\1063035\156268@T\n\DC3e+ FG\1040063}\1007879p[\1019675~s\58897W\1002225\131986[h\163754$\1014199d\1001302\135635\1083326\r\15121\&9\1054919\SUB\1033452\48331B\146637\1071032g\RS\34856\SIpN(\n_\DC3\CAN\ETB\994340u\"\1055984cq\148292%\168571@_vDc\1073055CW\ENQP\136867\STXHfp;\nM\1110028\154200)mg\1000362}l\1072450h\t\ETX\14968Q\1021295Tj\\b4\FSK!J\\\996951\1037918\ETX\16997[$\1006298f\US\FS\97025h`f#cq0t=4\DC3\v(n\CANb\SUB\CAN\FS\RS\US\157568\1112545W\ETB|\DC4\26469\SOH`\152656O*E_\1014509_4Lrc\1067039\68473\FSE[\GS\95227vbvn\121463\176466WFW^\1109674\&2\1092465\1101465l=\191025\1020663R\1107046p\189999+T\36798\vy$\EOT\184549LpY}\EOTZc\118805zLS\1099150\\\119989\&9Gzc\120792\1050858_\DC2H!#\169248\DC2d\177928!229\NAK\ACK(\1096427c'\142061\"{\b7\tM\63131,#IRi\1091628\n\994326\155033`\DLE \ENQT|!\1097357p\CAN\FS\138789\STX,\94330\r2\1082495s\1097275\\|\35843\ESC\1078746o\DC2i\b\11053gkx\994356kd\1066993\EMi|\13736\65150\160960\ESC/\1010989\&8\1069363:j\1028017\"RM!\96723`()\63658\&2\135558.\1049513B\171714E\1017316\1070909\1028371\RSR\NAKJn\1032860[VZQ\127514W\NULiz`Ie\1058604I\DELMY#)R6\64879\178752\&2b6\tX\r\1048312\1069402N\171772W=\STXAS;q\123203\1083930w\a\SYNptS\NULT\fj\143164\194759T\fSp\68448\ACKR*In\DEL=X\NUL\66188\vM7\121298s\1024216v!\1084042?\1022676I\1082108hQ\1062292.\SO\\\151754j\147624\a\1077885\ETX\1074145bE\1091072\ETX'\1023670\48208\SUBK\GSP\DC3\1081278\DC1\1085046\159684\a\139723n&\1108740Z\ESC\179659 wA>\141155\NUL-\NUL#\"|\165468A\t\ETB\1041615_u)\165061\143580Le'%*\1107600Q\SI\RS\111344\"2\vc3yVbV\1042395\\e\168551\STX\1090925esJYso\163169o\FS$I\1091068oz !\DC3\119098r\RSzt7X\8274%$-\1046768?\SUB\ACKA\SUBkZk3E\t\1067050\ETB\1019523\&1D-G}\1056157\&2Y\DLE\a\at\GS\7200\nQ\182489\1094286et\USK\DLEv\tN@?}>\CANz\987816j>w\DELc@\EMw" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "b\170229n\1018971\SUB\985694C{_\70741&\vMo6gl\STX\"8\1028959\GS\165216\SI(\138759\ETB\DC4\152692\&1\996992I]\v\RS#\"\1112677&aGfR\ENQ\1018847\&1:\ENQ\183934\1047759\&4J}\GStZ\DC31b\by._\26020\165646bK\SIE\1034932\149906dq\128297\"\"\67990L\94037\SOH&\DC3AIx\1009451z\11657z_\68118<\1083603=>FA5Q\1025568]\SUBS\1067075\t\1027792b!\1011202PY\1058512\SYN\1097813\aCK\RS\151398>\1637;u\ACK\ENQ+n}NzAP\69805RXe3Q4+!5-{\43538xEx\173125\USU\ENQd\187890,\DC3]c\164824\&9YkkCUR\1052545zz\a_\US\1052913\FSh\bX:\1097581\EOTs0DC5.UEt\1082356.~\51232\188301#\993299\DC4LwU\27171\1014215q(r_\SIAq\7019\ESCg \1034226G(\SUB\b/)K\1022799\1006348z\166521\&6\1073570M(mHu\1072369B\141057}^Xz@\27397da]VDH=xZ`E|\SYNEr$P\b\SOH.\30581;8/E\1056666\156080G+\USF\1046048\189590\1079895^\1072919/\DC3\SUB\SUB\STXy\ETXl\1012320<\f\159886\DEL\EOT\47816\&5\1010161 .u\ESC\f\1084279a5\US\100760h\988443\20830_\1112230y#]R$a/\SUBg-^:,\134242t\NUL\DLEFd(\SI_r=6&R\39368t#/\30862\1083006\55251\DELd\139094\f\bLL\SYNrl(\95410_jgp^B\148359pZf\131184!\1100088\1079773\191219/S\60206\157985\SIP!\SI\1030276We\DC1Q]\DELMi\SOH2\164247s\64188\59175\179637\&6_fIJ0-E\51588\39286 \b#\99545\52587\GS\1063696\57533\1094025C\1039590\STX[m|O-)\54684\132598\189752\FS\FS\31494\v^r,^PBK\175477r,U;p&~O\1003644\154009\DC1*/f'L)\146351b1mmbu\1070260\DC4X\ACK|q\ETB\186400i$\998123Q\170080\DC1:\NULa\179425v\1057890\ESC\1046601O?\144872\1001618\&9 m&\185419G\NUL'*k7>y\185109)c\1026066\DLE\t,}I\SIzT!R\1051585\&9u\EM\DC42ixf\ETBh\1093277\45899_\ETX&\t\9508\57743F\1054634K\151449\t\ESC8\n{2\1060622\59202IL\SUB\1114011dp}_9\67990:Xd\66188\134097v\ETXjk\52228\&0x\SYNi7y_\DC4\94598La\tK\SO.\SO:#\158037ZsIp6\DC3\tG\21697j@\SO\140605V\171781\1004444\1095580\n-0x3\1070457JsH\49717\&0.g}vU\985649\175749\t\1108868txWw$p-)NErg\DC1\RS\v\61996S\97223eK~\18154\1087578\&7\139648]:\SOW\f+C\ESC\1052448\131579\1070786pH\1082515g\ENQ\DC2W77\35594Il\SO!<\1029111\ETX\42368p)}`\47291}\143330\&9\US=$SXcjkM\186140Fp.8h\1047276['Ta)Zq\175154\4734\fW\51765C\1027418\1103868\1101167\1052280u\nc/\1112595\156385\1015057c0I2]\SOH]g\161033*g)\th\1024978G\1053938\GSv\USXf5!U\54369=#\vG\DC3\ETX?\SOq%]\147834\&7\SYNW+6\t&\1113103\1100216-l&\FSv\129474\DC4\1062308\1053188\ACK\1016990a\1024054m\1046519\NUL@~\1087194RfG>\154171q\CAN\ENQ\68901\DC4`o\r\DLEt\DC3\92906\1069460\1048347\53182\STXf\1094150\1068082i\1014049\1037453u\\\f(h,`\1047778m6\DC4f\SOx\157831" } @@ -98,8 +98,8 @@ testObject_PasswordChange_user_6 = testObject_PasswordChange_user_7 :: PasswordChange testObject_PasswordChange_user_7 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\1078147\&7\65218\RSA\999884k\v\ESC\NAKg&:\NUL6K\NULn>\SUB\119974$\ESC8B'z\"Yc\1032069\&6\ESCU)\NUL\DLE\1101164Zj\44385\83195bJ-\US\"\131804L\a\1067731y\DLEs-}\141826}\GS\CAN\ACK\rR}R\CANL\FSqkZ\n\t\189000\&1|\f5\984053L\ETB8gL\18292\&5\10771cf\\V~\DC1\11412\EM\120833\990084\&1n\"\60837*\ETB\SIaTxU\DELcZ\r#5/\bk\v[\a`\1106514\NULR;(\CAN\rFMN>\995764\ESCt\ACK{'(\141540\ETX\NULT\1057079m\f6\63805B\n\987874" } @@ -107,12 +107,12 @@ testObject_PasswordChange_user_7 = testObject_PasswordChange_user_8 :: PasswordChange testObject_PasswordChange_user_8 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "b\US\1102445X\1020217\EOT.m\"\DC4\DC4\ETX:1c8\1003324>QU]\ETXRe\1032621\SOe\SI_~\t\EM\182748^BGb\991684\SUB\1032747,4Ux\44572\nA\19832\1062925\fo\CAN\1001285#+@\14237J\SYN>\SOH[Q\31322P-\ESC\DC3\1017636\64940\NAKQmD]\f\" \23277\26752\DC3F\1087665W2\GS\FS_\145580S\34366/\SOH@\1008116u\US \DC4x\989165\138589\&5\60472\47193@\995028u\DC3\127467\169198g!\174297HZM\178770\1109385j_;\39894v_~\1020633U\nzDxZ1RN\2467mdb}\152593b;s\ae|&\"\18230l\SO\1075495\73747\165342\418'\DELj\993281UB[\14633\NAK\"F\1071892\71739\&9I\1086187\SUB,+\169163\EOT\EMBtZ}\SUBZRdm\DELA\ahEj\NAK\164812\1045403\49808!i4\160130,,-4T\39327E#\FS\129309\SIKj<\1109332\1019724`R>\111006\139594\DC2Vq4\DLE\131391\&1\51249\ENQM\42303av\181926\STX\1016985\NUL5\164635!&]\22190p\v`k\67413\&6(GB\1042616\DC2L\996758a'7L\1096604[\ETBR\1022507|\1020702bZ\1060760;6\GS`\NAK\1055957\SOn\128679\1080437\1000675L\70839Vn\189246nw\EM!*bw\r\1102406\ACK3\25917J\1100924\DLE\1079071\RS;9\ETB\51636Ts\n?l\171848'y*{ G?>y\166331\&4\1028518\143808M\ENQ@\1106697\&19\62848:\GSfI%%;p\1057791j\STX\52156{7\1045649mR\170180 \1045874`b+\189602\1095783\29108<\997493b'\1113133G\1113924\187365\1018965\DLE\t)\ETB_\STX\188043\ETBq\ETBD\14549\178567\&8\GS9S%t-;~A{\1098493\1009689t\RS\997797\rD\SOH\"\1036045\1080223R\r\r\SO;b\1079046\RS\96789\64328/*m?~G\1005579Z\1029293)\141393\134174\1004939lL=\1066280O,,j;x\SOH\74911\a\n2.+-\16525&bd\142521.\NUL\1105545\DC1\61097d\1016348q\ACK\v\"\155055\1051009\1111466c\DEL\a(./\STX\10580E\1095607P;\"\1100473h\15195{\21638\1108997\1001215Wme=ny\DC2\997396(\153889\990739 \rC\DC3?>ZD\SI}j\SUB\SI\GS\177033\1081156 )V>\1073618\1110301)*l^ip&^\EOT\991196&Y\SO(L&\FSs\1025953\SUBm\194690OR*\1083553\984637UQ9a\173357\RS\"\170635pt\DC3hxbnb\144388\SI\1096629M\36441\183861fJ?}t\1042071T\21290\1041177?\GSg&V\1107865D\NAK\58427M\1083184\&0U\b\53742\1049758\1019549hjT\"\1047744\EOT^\GS#\NAK\tv+t\FS\USL{\1011965\126503x,\1024988\DC4\1026933<7\1074268\ETBw\rz\159720\985242\NAK\SYNq\65888\1019932$\1038698|:\v\SUBd\n3]0P~Q\FS+&@[\v\92526M\v\136444\DLE\US\a_S#H\ESC\1000365\178961\66613\\\"\b}8\USF\t\ENQ2|,\t\US\36910\996072\DLE+\166272D:\148639\&7 \GSe\1085048#'\DC4MpE_n\95537~\1018210@\1059219&=1\1058223\1085407)R\1035591\SOHt\39215?\94500\32949E\171322\EOTzX\1061392/mZ7\39206\ETX`m\1055925!$\v\DC1Z\SI \DC3,\14510\STXil\DEL\DLE\7164\1027803A~EU;:\ri\1083540l\35399vc$\SUB\DEL_\1081553EP4\1103837l~|\a\1051360\ENQ\SYN\1096952+\1074553J\19836\t\GS\aA\EOT\NUL4g!\62787\150045\22255\183201\STXN\ETBrrpN5ks\bZM\ACK*z\DC4(Pl\DC1\"\70701\1067954K[\1008837RH\1032341\CAN[x.\1048119ac9\1111224\59370$\19588\ACK\f\74197Jy!\29122" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\f\CANj?\ENQ\100674\27294`\v\60820S\aUZ<\190604\v\n\98721\SYN8,\SO`\ETB!\1023917\SUB\988878 \149166~\25356\&7k:\SOHo,w\US`d\1095991E\170702w\ACKl\FSOUl\DC1r\DC1\4696\1005535\177324Y\NUL:*\SO\51294x?>O@mGy+Z,\SI\31196&\EOTZ\14202.w\\\GS\CAN\DEL\1061426\FS#!\100667NFM/\b\168841 T\183374\1014354K\nG\95679\1071981G\1014345!]\GSN}\1071684m\CANH\1032944Nk?\61487K\EM\131256ov\48786V\r\184775p\61887L\1085562e\175014\ESC\173236\USr\135299\NAK\SYN\NAK\DC33r^E\43094Z\bP<\135606\138117\1066630\178853\27013\1078589\ACK\SUB\NULEa'*W\177921\163435\176746\EOTLV4\26629\"-/\118982\1108171\&3\19533\GS\SOH/xiy\1004921\31236\DELZm7s8l\1032610BT({b\59819n\DC1\154649vD\59996\95915r\66886_#d\34706~\53775;BKF\993228] 6f\126105iN^\132202\CANeN\1050181X:\"+\STX\1000519C\DC4*\164663j5\1087078=\49843\163443\&8\46178\1005505\1086358\67354\&9}SK\132067\\0\120968#v~J mt3Xx\RS\US\1053047\v3\997095\SI\DC2Tc\996715\DC4m,\SIn!b\26969'ac\29011oV\18582W\100115\1029633\t\SOH\149270\SO\1052983\&3%hmTnE+\\%C\24956\137609\&3\986293$\1010528\983647\&04|En(\17123\SO\174091`Xy\1069572\984775 \132546~\1054660]\DC3\167285\&9Y\166240\&3T/\1057195\58265\tBS3;A]\DC3\2765P;.\1046618\US\DC3\NAK\GS2[\25411\1061324 \13123\165595\"Q^0\STXT6\18123yB\DC1?\NULv\1081840xd\1060136Y\DLE\1094984\RS\168967\a\SOH4/R\113820\1029185V\DC2A6\1016176(T\\O\USTu\152631\ENQ)\22634R\ETX\vR{\r\STXm\1044646\146582\SUB\154494\32280\1053397\EM\SI\n]&<\NAK-=\1052283|S)\165884\43834Rq5e/wH\SOTh\145516tjJ\1032777g\CAN\DLE\ae\SIV3\94033`\1100278s[KR|Y\CAN\1079014!\ETBM\1091893\157876U\n\f}!K-\ACK\ACKmp\DC28:\GS\EOT\r\1063024\1023444)(\ETX\1017315\983729\60554^\1097871\101098+e\30627\t\160747N\EMi\nfN\98956\149454\&4Q\n\r.3\31727|H\ESC\990983\998110\159749" } @@ -120,12 +120,12 @@ testObject_PasswordChange_user_8 = testObject_PasswordChange_user_9 :: PasswordChange testObject_PasswordChange_user_9 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "\121027il\134081)R{5\ACKXm\1041631\\T\f\DLEp tq8\1070074\&5Q*\"\1087921@\DC4\182306\CANN,\162026\1112739;3.p\STX\fLA-\149790j\bON\ENQn\984593U:\EOT7@\GS\1102637ky)\74361eri5CF_\1086719Y\25273^Q\SYN\NAKQ\DC3[\31615\ESCaY\180511~\t\100087'\DC4M^\EMI\994154\&4\96550\1036840b\DC4r\1078078r\RS\140751\1084467\SIuHc\rg\DC1\ENQd3\SOH=p%ry\1003698\&3mhoP\1106864\v\162715yXMw<\59204\&1\f\1016334\ESC\3501lL\69237\GS$\a\1039285w\985184{)\NAKw82K)\DC36\155645v:\SOHC9i\1039062\17926R\1072663/w\99462\15991\185843e30R@T\121319_\NAK\ESC\\\1092892\n_\1069021\aiinb2I\RS\1098801\138282\SOH\992030Zep>H\1079810uUC%\tS_fH \SO\1084851OH\a\ETXx\a/\ACK\ACK\993315\165332\72410\183658b`%\31687\DC3:UIj\1021763\ETX\1053142\NAK\STXS\DEL[\1028930\&1Z\SUB\US\1088016h%\ACKw\EOT[yh\ag\RS\994622N^{g\1003836\993133\DC2\a-\1061684\rrJ\1036806}~;8\ACK0\1021569\SI0\ACK\ETXfXG\SO}H4\DC2zC\SUBz\DC2\DLEz\DC1\149550\61518H25*Q\1083850\10672\NUL$h\SO\134582\43597$,\65487\61824O)^\\nvK<\989262\&1R ,k\985467k&\1012054\1072126V\98741\170921E\ETBEP\RSR[7F(H\1006507\EOT0\\\CANk\DC1\1085575ql\150344,\DC2\NULv\SO\DLEp\SYNIqpU:$\1051572|+\DC1\SIT\1043680v:\54535\133122\SI\167063\190640\r\NUL\1080625--\NUL\1000447J\158492G\1043941\ENQ7\NUL'o8\1055620\ESC5wuH" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "<>f:;-RJq\6050x\161753QT\100505;X5.\1024124;1\fP={7\136758\151452v\DC4b\ETX\1041908\NAKV/R\SO\1003791!\SO\ENQv\SOC\v\FS[Q\ETBh\CANX-\66455\22025\1087386:BVj\1104389\ENQ\41001\97464\DC2\DC2zRZE\"\DC2\EOT0\164473+i\USE\52704R\GS\1015386\FS\EOT\154897\185499\USew\178235\vR_=\1105788\78173\ETBkB\172309\995208\&7;!?'ZN(n)\1003220|\151096\DC3\n\DC1\ESC\1047644\EM\14646wov\ENQ;\1082140]\118864\1041829\131956\&4\1085467h\42033?iD\DC2V\96442t\78699 %\177867t}%\168450\1068330N\EM\b\7477(\6702\&9~\168927*A\ETB\35836\1087213\ESC\EM7`s\988223G\CAN\171597Q\1032850\"\EMi|\ACK\151936\&4\49571\&1 ^\1034297\38608\1080861BBO\ETB\188460sz\GS\1113432\20959_PsX\152878-)\1013286\f\11345ZAT\ETX^\1103065$\1002688\1102176M\DC3\1106060\1083723\31676\135940\1010227}+p)H\EOT]\61870(fiL\74358\STX]m(c\1099516\1058859wN\135817Zs;&;\1101239\STXc?\nP\164370R\1073337\8218\DEL$\62817\1035797'(v\94886\RS\131427z\USV\DC1\995931+J:\1044870\28567<\161564\EOT\t\SI;Ll\31033Bl\1035926g]\EOT4yZ\143213)Qs\127306b] \149682\NAK{\\\\]\48933\1040819b{\1049468F\182947\&1\"O\NAK*\24604\US\989988\52604\&35>>|\CANH\GSZ_4}6\1056732Ty{?\CAN\"\1061905\1004010dM\a\21624.@\120724\DC3\984067hX\DC299hs}\ESC\n$\ENQ\1044696o+\3801Z\66465h7\172119fa$gT\t\1000627\1111076\1075382X9!\40902\a\EOTH\100163\1019832[\"\SO\1061034|};\1001901\55166D([N\SO\1037726g\180696\22235\142179Bxq\r\DEL\1109671\"o\4735\165730\ACK\20074T\38821do\au\151559\39351\SOH\SOo" } @@ -133,12 +133,12 @@ testObject_PasswordChange_user_9 = testObject_PasswordChange_user_10 :: PasswordChange testObject_PasswordChange_user_10 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "^g\165188\183325!D\1071796\&8I\rv4MC%k\\X\NAK@]\990411\EOTkKg\46739h\v-\EM\1003941d\r\n\46818\164542\DC1Yf\1031947\100661SN\t_q\34663\1024752nn,9\1002494\134950;vL\1005623?\vL#\1004806\159319\1005755Y\49264k\185970\EOTt\54951\&8\SYN\GS\DC1W\ACKxK\100106Dc\1028596k:\1088790\DC4\1024192\&2D?pHS@i\1055560\&5\"A!\127257\DC4\57827M+CS\EOTK\DLE\DEL?y\1098181\162494\47866<\41529y\FSk\SYN${\r\1005146\190068/\DLEY\20575\b\1039849A&\STX\b\998416E\a\1032363J2\120490\1018750\GSa+\nL\ETBE\DEL\NAKnAd\ETB>\ENQO&\b\1011115@:I~-a\SYNk\DEL}\USE)C\DLE)ts\SOH&\128490f\1031578O)8\83270e\59254\&9\58057O\v\r \bt\188311\n\1013246\46070l\SIWGb\1008559@\1059413\22227u\1026214B\1029435t\1109601Sx4bL\f\171190^:6\ETB\EOTz5x\FS\RS_\ESC\1105088\DC3\1111332|(w\1030422\ENQ:\45632\72881k\1036191\RSwC\186931E\1106146\RS\NAKJ\1043833)\120159\1023499a\1068709S\DLE-p\142797Y8~\ACK\f\SYNq\SYN\191139\1061750Vq\SOWh\EM\6136\EOTA\"}P^M1 \150446;\ENQ[\83011\78574cG]o\EM):r\185345\1099699bCeS\1095638E\SYN\39700\42082<\ESC\31948\83108\142987\&0(`\SOH\rr%Y\ACK+\1082430\142137\RSK\38850\1042506\&2ZK\EMc\EM\NAK\US\69438\51321\tyI~b\983734\&4\\t\CANu\1070201W\SYN\53093O\SIwT/\1054638Q\1005484\157400\&0\a\1040610a?\95679\NAK\SIpZi\2699o\RS}R\bm?\137670}\70000\42449\32037\ACK`\NUL\ETB\36412zO$\CANo\tX,gCWQ\FS\137405X\aiI\22269\1099880\SI\1056230" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe ";\STXW\3658\&9e\61104\1010281u\1054825;z\1102201.\CANm\GS8HiR[\1090721\113679h\30385\1099071P\13786ux\EOT=\EOT@-\NAKU~\30622nm\181728\v\SI\"87xzsd\t/\SIABdv\SYN\1079721\38516ov\r\61169\DELC=|EJ.I1ZH\r\1109850A\157023I\DC3h,\150284\157340\40343\DC1\1101518\SIvxV<\100481\CAN\992513k79\6439\&7\1105382\29451\51856\ACKw(d.\r\986761tHWFI\b\2063|c\139209.ZR\ESC\1043464XOH\b\1065603\185045\DELKC\SOe\ENQb\STXtwM;\1063586Qb\EM+C\a*\991991\SI\68430#DEv\70295\&5p\EOT\1017741~&\92197\6498?\DC3A%4\111178\"v\NULg\tmD\991529\DEL?\148659\v\\7\n\n\US\1052346EbL\NAK!` \996371\a]\1050364:\99420o\98763\1038145\r\US}nre\190462=\RS!\1026220\RS_\n\r^+\1112053\155114j\144557h\164197\EOT\1456\1080248\&4jpQ\ETBv\1058697\&6XK\DLE{X\182304|\FS\30623c?\ACKW[\FS)_~,\187940c\133750Ihm\68052\GS\NULJ\"#\b\41024\&0Zh\147884\1013140F&\ETBJ`O\191380H\917827&'Ox!4\ESC\DC1\1038348+DgC\95526d\r'\US{\1062664\47822?z\DC2\17591~\96360\155417\1068401l\14806r\NULN6<\57679\1055613\141513]Hp8\1063393\&0WrR(\25161\19762'gc#_^\997158\31893\23078\179623E$zk\t\NAK4g\STX6)\993627]\DC3\187429\34110p\1012714C\1078346Z2\DC4M\FSJ68E\37649H6\EM&\131250/\DC3ovwm\14557&m\152064M\1027607F\146051>\f\21330\NULG\994703l1*bd\SUB.\ETBWy\154255yo\1081513 zbzr\1034720\32843\EOT\1000002\16121A\133186\btwr(t\ETX\NUL\1038295?9\CAN~j\1018782E\176705$\EM(&\DC4f#eP\EM~\CANCT0i\1007344e\1041535.\1023395u=\DELu1\173333\DEL=\NAKYL" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\\2~\tLsG\1010318N\73893\19920\SIN\bA\1084446\149836\n\96723\&3\1005158%\53931F\SOH?2\985088\1043578H\184226\tWe(B\26887\SUBU\ENQ\v\188995\"|\53505U\1043137QJ\FSN\1098083\1056930eN\DC1pc\SUBY)\STXR,w\1068893q_\SYN\ETXM\179588\ETB5\19176*\182041\aig2\n-\au" } @@ -159,12 +159,12 @@ testObject_PasswordChange_user_11 = testObject_PasswordChange_user_12 :: PasswordChange testObject_PasswordChange_user_12 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "cA\1013876}1\986971`\11039gQ(6O\148533\ACK\ENQ\144020\1097898S\b\1026784\185340\1076893\1038191.tZ" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "*~\EM\SI\1035297\65924\ETXSi\917783ua\DLE\DC4\SOHgPRF\50551}J\SOH?.\35757\26118S\99244Wb\US{\191213\DC3PD6z;bU\SOc\168740{\NAKC|!\134012w\24191_\15393'XFa>Ds\1032722\&6&\160998BY\1105510\987553u/\146729\187725\NUL\DC2>A.>\1026064\&4~\1066860\983616\1065813\194763\41670n\NAKe4`v#\54249\154796+L\au[\119894H\1003679^\1052741PIo\EM/R\RS\DC3\150872>\a\1011374\&8}ahwE3\1009691k\19141\1096715\23648\131344\SOH>BwWU#\135314} \b\SYNK\1035065\157575\27111\&98-\995803\167979Y\SYNRkE~MQr.\47537\a\EOT\100066FE\SI2re&eE1\SUB\CAN3\30141eKHFCl6\185160;/k\r\SUB\t\41097@&\159450 \47257\vM\\r\SYN$\NAK\40078n\983737\1100061u?D>vA\95624CnF\989092(\71111\176451\62790RhM[\nB>*A\SUBgDpp\NUL\137828\1044414\DC3mQ\ACK\1088526e\164816r\NUL\129550\3404\62114\GS-lk\140775/\1031265>\RS\ETB\DC3\128406LFAs\176184*M\1085893\NULpt\b\100127JN4QU{Jev\EOT\STX\1108765E\t\153121\1033318I4K\\\163474L )RYPBk ~\v\RSn\1013852Xxv6#\1111946|\1067819\f\134655\987741\ESCY\31029z\NUL\ac%i< \n,\128826l\RS\1004102\119256\94776\986312P\1051689V-p\twc\CANV\185920;4\41787\1020199.\b_\1035216)\DC4K\EM,IX\DC13\NUL\1035747\&9h\US:\SYN\ETB81\160334\&1\36963\SI\RSC`\141966\fn\f\100236(\164834\180065-\SI\DC1$g\1046824x\DC1\99084~\181210ADm\NUL\1033535\40647z\999919Q[\STX\v\188766)q\DC14\134546j\DC1$\1038869\178209\1020722\ACKi\995076\&6by\986338E\DEL5\31674\1053862=I\ETB\FSD@C0`\n\1108426JR\97512j\STX\1011610\132328q9\12587\1110037%\ENQ\DC1)\SI \133259S\EOT\an?L\17808\ENQi#6:\39370\984528$VZ\ETB.{m\1105413\ETB\1096254\n\1029048yP?CQN\58229\DC2c\1016719\28430\37793\1021922\1037171b@<\FS0T\SUBs\NUL:\EOT+\NAKe\EMv{u\35899aS5ztr\1095275T\1076768\1067480\1055258\t\138199p\24191!\63947\57751\184259W'/t'\998026Zf?Kaa cX[\DLE@K\rP\DLE\SI6F.\\\1105071\EM$\ETB\185348\995728\1111114\1056306{\CAN\131737\SOF\"+J3g\14443W|\1025079gsE\b\RS\146118\1044328eZJ\rN3\v\r\\\RSTp?\1047550l?|\1052685H\STXI\1041763f\119524>)\150862\1004663v\997187\DC4\62145E\r\NUL:\DELi<\ETB\NAK\1034255\SOHyq\189759\39702\99276\1069813`\SOHt\DC2\1027302\1081467 ^I8\DC4\SUB\t\47100>\DLE\1007609-\154421\US;&\996462jK\\%Bg\170965:\a%\1030923QF*0\173510\1106863HW\52749\ETXs\1026228aAS\1916NbLWp\1051310^{T\29700\179151YL+~\DC2Zv,}\1111746\167987\59316\t\38658v\60679\176523\ETXM\52344,\DC3\1111272~\RS?_y_\DC4u\a\30652\SIOiiFo\1051777\DLEO\DC3{\40043o\1047361\ETB\27903\DC37S'\ENQ;|\1046730Eq!\1073488\160026\1006141It\175865Z\1065806\DC1h#\DEL\138456\7911'\ACK\169193ei\STXs\1104017\1064860sf-\57677\1006904\992638\RS8\ACK\SOH\CAN\146274GJ\141048\GSHp\61180\GS\EOT\186295z\EOT\tp\STX\FS\SUB\96038:)\1029325\42428\SYN\SIg$\139209M\1014224\DC4\ACK8^\NAKt\9753e\1093154\1043578\1079420[\NAKi\66187Q\48349\&3IV3\171979\v\DC3\SUBxDHJ}MT\r\SO\179759\145196\f**Z\64475\1056846]7R\ETB2Ez\DC2$\176439\1000728BM'9\ETXEa\SI\1065849I\1098032\98480" } @@ -172,8 +172,8 @@ testObject_PasswordChange_user_12 = testObject_PasswordChange_user_13 :: PasswordChange testObject_PasswordChange_user_13 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\tY6b\1071064\CANJ0\RS\DC3\fg]\8556^\DC2\1029089S<\ETXn\DLE\161054\NUL\1009413\"\NUL\135103\SIU\1028385K;la\SO\132923`\50089$0#\19913\f~\RS\1050195kyBx\a\ENQ<\DELbY%\1106346{\144787\ENQ\1006226\SUB\n\1111798=\1082031\nl\1027190\49972\t:Z\\\45927\&7,\ETB\\\1043520\1040129m^\ENQ+\ETXepC\CANVO2:!\155826\n\CAN6g}\1100418q6\1056075\&6<\171664\SOH%yP\175359\STX\ACKo\179550g\1071640p\1006475T\1018644\ENQ\SO0'F#^IVd\n\157140\141227A,@\1053337C-\181395g``\166195\NUL\7801\1049487\138364\&5}q\50268\SYN\1089481\134438u(P\33463\SOP\47384e\n\DC1\164033\&6\1083698\SYNM\168121\&7\1027817\DC49\1039185O9 ,Op\983226UR\DC3\NUL\48061\1049901>(\1025638\EM_Nn8\FSb\DEL\92741r[p\1113723za1\DC2w\DC1\24935\SUB\169669\FS_\CAN\EOTN6Vyi&)\1012450^\135732\EOT\CAN\41126BuJ\DC2`\78370)qq^@$*\SYN\119136P\v\7875\ACKg\134713Mg}\ESC\EM\993564\1036198w\983924-e\31379p&\SOd\1022808\74004a\15280\1040139\1056286\RS\143232\1056072'E\181014\98120\&9\DC4\DC4A$\180660h/A`\DC1l]3Qv\14807MR3W\FSsn] a\NUL:3`\95284{`\32597\n\US\DC2.\172218:?y`\DC4\1085202_%S\155378:\NUL\171483\EMk\"\fWYu8-jr)\184?D\12340c\1107469\1096889\1089369^x\SOH{b\DEL3Sl:&0xgT321\180495FU\1068409N\1113930P*L\145663\64596i:\48860\SYN\164807\&0#\ACK\48791\&0v\1049613n\SO\159015P:" } @@ -181,8 +181,8 @@ testObject_PasswordChange_user_13 = testObject_PasswordChange_user_14 :: PasswordChange testObject_PasswordChange_user_14 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "6go<\1060200f\41213Y\1084615*KE\1038629])9\1028527\1090910K\2404\1065550\&1\99810>qb2`\NAK7*al\v3I*\156801\SO\1090154\f}\DLE\139358~\129615@jjd\SOHO\SUB\SOH\94999lv\16578B\b3\1066265?Ih1bv\v\SUBz\SOH\FS\128520\&0.\ENQb\50990\FSR<\1111211\EOT\DC1l\"E\125002X6\NAKS\1011858?\ESC\EOTEK\210h\54053\16688\f6\917624\59462>\v_Zd n<\DC2k\1086856s\1069883~\SOH\1011269\CANr\DLEG\998802\NUL`.)lj\DLERr/\149432\\\176664\999860\187741\59007\96806kI\1040467\&9\\.!\STXpj&X?=r\1072676A5\169615\18716\NULex`\SOH{ \121420\ETX\999279^\98959D\DEL\1051244\163196\132146m\58414\35040\&61JU\NULtn@pCF3\fM\155170ZrHM\1024580i\136496zhn\1010172\983207\ENQ\ESC\v\US\ETX\1078490\1027708\DC4lU\DEL\GS\10612[\DC1B\EOT Y\162831&cnLev\20431awe\n\175441+O\69646N\1039476\986854\59235=x[7\"B\RS\DC3\DC1\bQy\DLENq~\1100372G*\1040946C\191033\DELHo[\96055\&9'\1018134\&6\186449e#\ETBK\49381W\SO\1108069&kH7MQ\ETX\EM,v2\SOHN/\1044045\tO\169061\SO\1010256b\185510\1081515\148501q\1037709\1091186ww}=$D\ETXw\DLEb\1069094\EM~\142428T\ESC\CAN\74821Y~{\f{p\138353*w\1062006Juo\n0\150906sYXHT\USK\a\1009732\ENQ\n%q iF\95870\ESC\GS.c\GS\1014409\1066933\USW\EM%x\1003810\NAK>\DC3\1058760" } @@ -190,12 +190,12 @@ testObject_PasswordChange_user_14 = testObject_PasswordChange_user_15 :: PasswordChange testObject_PasswordChange_user_15 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "n\DLE\SOH1\ESC\1030226%\1069394\RS\182899Guy\1039539+\1113955\1023913@\NAK\5561U-dZ'\fB\1055523\30303\SI\US\CAN;V\SYN=\FSz\"\1085023#`\USt]KYs{\v\45407\"\8592\1064953\1006367PP\n\t\31925\1041417\44390\&5u\60622O\29903\SOy\SOH\1051143\184117j2\60717\1030594\14253K\100794\SUB]A\DEL#VL\RS[\\\1044640F\b$UXg5](A\f#b%\1086075\NUL\1041235?\45258\1073954\SOf\1095011\GS\140034b\1052232\&1\998007\996181>\49135rnE \ETB\f\f\FSM\984153DQ\GS" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\137821\"AZ~;\1071515sK>\1003094\32681rdU\SYNZ\FS\1013170|~\68860{W=X9*\1094479^]h\v\1037179\SI\1105700\ETXy\b\121173\EOTB\RS\SUB\ETB\1087347$\1086752\&4V\DLE*,JI\EM3d\169902\&8`0&\182876r\61161*T\ETX\151630_\"\SUBT\EOT\SYNyw\43410\100742\nP\1111807\vJ\GS;'\a\1019800b}\USu\15085\b\SOH\DC1>f\ESC\185821\SYN\ESC\29398\&6B\FS?*HjHc\1072255\1058427}\1624h\n:\154398\"\1051991\1099837\&9d\128882AXf.Y\139982\&2>3\985533~\DEL\RSaL\DC2!\n}\DEL\DLE\aV\b5\1004615w\44033\ENQ\STX\986416oe\47326\&9\"\1012652\EM\\Z\31242\\\1054641\SOu\1042537\CAN-\GS`7&aJP\1066356\999545_Di1\ap \SUB\STXv2;\170127Fmg\SOHX\1102996;d%\ETXx\99896\EMOr\EOT\DC2\162508\GSvo\1035769\ETX\51961\&8M\NUL~bI\1096210L{p\aq\1026887W{PVOSq\132165\96511!n\t\16523U\t\ESC\1014032!\EM-IFP\1096087\97677G\1056015+l\a\EM.\1051294\f\1031336H\1049728\ESC$f\CAN\NUL*\bnr\1049928\1075881(\189737^\DC3\98799B^\170344[F\999872\ETX\NAKJ9;y\ACK\SYN\66458!51w\64275?Y\EM\DC4-1(x1?(:\SO\181899P\59702_\27711\163618Vx\ETXuN\v\1055926r\CAN\FS\SUB\SIu{\1026849\136958{ \CAN. \1004662\1081463\ENQ\SOHD\DEL@\179223J\ACK:\167491Wy\14989\147498j0(\ENQ\1102373\1014240\&7\SOHTI\RS.\118990#\"\162391/tg ,\1019276\1069245\&1m\186668i4\67826Di:\SOU3M\144745\1112930\1006102\&7A1B\47962\159987\ESC[O}\1028140\1033214\1061595\1000273G?~\SO\1105814\n\23793\CAN\132894K\1109537\157688\&4\\C'\171760t\1092105\1069028^\154207\NUL&aW9OlY%1\t\163491bT\n\133769_DO\70287\&7\EM(z_B\14519\153806Fg7\SYN\EME\1096879' \1105838\ESC2\fm(WW\1091836we?\1088332\26513\CAN\155517LZJ\NUL\DEL\FSYIk}\120430\EOT\4637+kZ\SI\156899x\SUB_\DC4%\177759\1057446VC\1097314\1074153\1072386Oqn\a\RS\1056654\"\18164%8a\28468\132645Tb\"C3\1103957vG\1089945LF#{\96210\998246\160936i\STX :\163339\61888i|\DC1\1011444T\t2\RS2?IHPdw@fhLKXq\61905\1046908qz\1038449z(\189299,\b\SOV\\8C\DC4\5575\&1>\SO\ETX!\131673&O%}XKML\43288j6\NAK\1080490\SO9t9Ku\141219\154727\RS:\1022834df\ETB\996821s\16300\US\173093\986989\ETBL2\"\64028\1047440\ta\bZ\185810,u\1054582\1022464\991444\n\SI\1090918\ESC.B\1024218B$\SOH\SOHv\23330yD\1082294UZ\996426Zy\1031823oX\EOT\SOH\174801'\a\125038ub" } @@ -203,9 +203,9 @@ testObject_PasswordChange_user_15 = testObject_PasswordChange_user_16 :: PasswordChange testObject_PasswordChange_user_16 = PasswordChange - { cpOldPassword = + { oldPassword = Just (plainTextPassword6Unsafe "S+OT\38751b\DLE/B[\100483\&3\47760\GS\180067O#o\25466\&5T,8M~\GST#\987895U{y"), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "5\1103104\NAK\1014216?M\ETBj`-\30597\181188\1026387Z\1094596[\1092626\te=\991832E\DC1'RlS\DLEZJ&L\1107431~G\CAN$\vd6 5\US\1006596\ENQd\r\b\SO\1100302\1110521i)jc@S\156632\1002333\v7\24501tU\a\1049077\\hD\1110213\v\DEL;P0\SYN\\q:\\I\990426Ty\1097835wk\154857z\DELC\36957\GS\3138]Z\16454\SO\US)\133053g\DLE\DLE\GS\ETB|\44640-\STX~\1024260\a\1000452{\ETBK\DC3 ~.Z3\SUBC\986330f(_/\1110859\1055634\1003279\&7\183j-\171356zX\STX+;zT@?\amp\SUB!\36089h\r\992554\SO\SIt?\b\54803/ y\NUL\95035\1077028%\1099069\NUL\1063994\DC2\DC2!|!\DEL\t;D\ESCD\1041733OT\1061393UJWf\1113505\178024\ESC\154767\1050223\FSX\1026016\1020780\CAN|ix\1091727jZ\187257b\NAK\SO\1030980r\DC1)\1053891:\163447\45030\&9<{e\1079093\30596L\NUL\STX\1019960;~\985116}\1052410?+&\NULz\144674\1086689\&7\1030068%x\FS\1036306~\120570\RS\US*\ETXp\1034462\&1\149891\13986\1055542\STX@7yY+\ETX\NUL\1062210$J\1067009T:\EOTzl,!\SUB\DC1%O\DC3\SOHX\FS]\1013399$\152121\1104444\\\139341PX40\CAN\v_,yU^R%\DC3e+-g\172222\SI\DEL%f+h&W\ESC9,Jg,'x|\51952/{Y\r\NAK\1057765\DC2[\1038364\SI\28850Nl\46666\1885\NAK\NAK'\DC2/H<\180011]\ACK\1090504@9\127306Y\150151(\US8\53321\993078c\n'8]\SO\186951q7RH5L\1028090\165@\8885\30083NB2x-\1014943\985470{7o\94409)\1031807#\ETX\42922rid_^Wy7\1029256i\1062709\SYN\99669\n=\21963\&5\8639\1035935\1067300\53855\NUL\vO<\175839\&6\67816\ESC\ETB\EMJpG\ETX}\nM\177929\96385\ENQ\NUL[\1007534\US\1085889\"bl6\\`6\EOT\RS\",6?>Jk\1044669\160533\993117!?4\FS>,\DC1%\61901?y_\ETX\1016387\v\FS\DLEUQ\27172\187044\73101\24011\SOH\169041\NAK\1044569aK\r)f\SUB~N\1020859\DC3k\1012707$B\DEL\SO\ETB\ACK\EM\73084\58832~tVD\RS\SI\n\RS\1037043G<\52368\1007888o\ESCftC\186158\n\36317\b,~\ESCM!\ESC\174873\134091`\1046265\998677b}k\67343\1077779`]1^-\NULO\1013355r\24494\149416\36343`\127285\ACKW\1097424\996658\&3.tS=\983895\ACKs6p[\989667\SOb\180485\1076744W\CANdO\128541da\1063827=\1113561n;\180045\&8Wn=\SI)\1025924%\EOTj\1043094\NUL~D#W%\NAK\b \64862\DC2jr\27380tb\GS\1014983r\bD\ACK\175197oH\4243lJ\51936\1017192\59111\1024329L<=\v\78854*\54478 97q\1013840nS'{->/t'\1065169Xq\917836tmel\1025953\1010549\1013101\SO\DC4\"$\US\SUB\1098531\18016\r\DC3\140813\95239s\28689omb\SOH\1102241P_&\67318X\f*lfw~n!\SYN\ACK\a\60339\1012508U\1104365Y-d\126581\1068676\NAK5\DC1\SYNO\1060779z:\RS)\188550\NAK\1026997\59211\5670n\CANh\1072150F\9559\a\133215\165806\NAK*C/\44946.)\SYN\aP\1107161\1043226\DC4\1087020\515\67972\DLEL\n\180263y5a\146153\54746Iy\11497a(\SIv\SO!GW#g4\EOTb\SI$(\ACK\niKxu\DLEQ>\1038539wGc_NKl\r\13222x\83063z\DC37\RS\1096948\\\NULB\vC\141810\GS\169437C?&q\1009432)+PhcHd\186025\DLEA/F\1035548Y\47461\14070J\1012685jIQ>y\2014\1058904N\98611y\SO~\26014@e\1061608&x\189240\1080205\"Yh%g\SYN$\1069145\1046629|\EOTT\EMP\1011180\1084918v\RS-.e8\SI" } @@ -213,8 +213,8 @@ testObject_PasswordChange_user_16 = testObject_PasswordChange_user_17 :: PasswordChange testObject_PasswordChange_user_17 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\1046443X2cZf\tI\DLE.3\27153\41641\987805\SO\167150\31997\157768U\23766\159716\DC2\993933xy\1103378DB\1095912\USP!\8776\&3\46231\f\14600.1\1020378\1043279ji\135553C\95086\16967\37206\19099\US\NUL~BYB\RSUYc\ETX\1091112e\127528\187472\4411\"cN&\t.9\1098365?\GS^\v\SOH\b\CAN\41627&\13579\1108825a\1014432)z\62357\&6Z\179494\1092724\ESCX\SI\13823xJ\SYN\1428\EM-\DC1I\nB=\1040975n!l\131479~U\1069398;\113684X\187497\59277\EOT\159297\1023481uY\40199z\1054394D\1020153\fFbZtt\CAN:\CANYQ\SOHh\1006361W\1110330\DLE\168743!1}k\\\1055615z+\NAK\1106543\SOH\1094136%\17474?v\1108035h\fN\f\DC2\NUL\SUB\189591\996341P\GSbP\DEL\1107736>ie\1100530\7924i\168174-\30280]4i-\STX/\GSA\b&\v\1043901<\1102709\1106671M\\\991694-pG\FS\169333\DLEHEJO\a|\t(\9209D=x<\ETB aV\1012721O\999045n|mdg\1043448@\1110847f\a\1025181W\190988\19816\DLEh\166909\1092096\ETB~\10652K~\1072426\ESC|\rdi\GS\64637\94773\1081217;\1026647\&8e^\142140\DELT/D\US\NAK\983847hTTe|N8\1077575\&1\1092491tJR\a\155288iJ2\998006}\36187\28713\25201\SO\1109108\&0\2753!y\SOH1W\USzX\SOH0\991532s\119987h\78486\135733#\1074355\138222SR\988575,V\180455v\NAK\164938\&1g\SUB\ESC\97713\1081062\STXQ):\US/E'\131476\DLEz!\SO@\1020670Hy5*R\1010303\SUB\990422\1044281\1014588\1063943\178348\1062043`\49558\DC2.M\1113770W\171312\ACK\1024710Imf#dHF\ETBc\194659\DC3QcG\1070916B\NULW<\RSC\1059704\988425\1022019L7" } @@ -222,8 +222,8 @@ testObject_PasswordChange_user_17 = testObject_PasswordChange_user_18 :: PasswordChange testObject_PasswordChange_user_18 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "$\1043357izIh\65323E\152268b\fi\165052v=:\t9\1029608\r\10484!\1051779:\1003340&/#\1091275G\188407$W\990383>\EMDo`F\nY\EML\EOT\t\NUL5\996488bC8\5233Bq\1018037$p\NUL\v\9478R^\SYNGF&\1012032+]\156711]\22754\38792;:\131701\155917w\1065591\NAK\DC4\SYN\1060773\1015476gi\SI\"\vq\6329vV\1040593\DLEYya\1102677};,K3\DLEn\ETX\ACK\DC2+\184693\142191\SO^q\DC34+Iby-\ENQ\1053606\162697_" } @@ -231,12 +231,12 @@ testObject_PasswordChange_user_18 = testObject_PasswordChange_user_19 :: PasswordChange testObject_PasswordChange_user_19 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "k\ENQ\SIW\142801|YQ\999097H)\EMa\35968gXC|&\fE`\176817UQ\1096875\GS\1042874\ACKj\94562\142093\ENQc\t\1015620\SYN/8\SOHL\986768\&6\132434\1071731\34028\SOHy& \ETB\52652\SIf\1005119\&5\t\1060616K6A\a FxP\26949i\35802rc\18038\186543\172362\151462H\149276h[GU\nuX\SI%~I\184399Sv\r\DC494\DC3\SOH\989634E~q\DC2\990048\120529\tR\SI1$\NAK\ETX1\165481\1009573#\nD{\1034729@\1045950q\1036461J\97887\au\SUBB#4\EOT\8381\1087000\161668g\1011547q6(=\SUB\58393\n\13236\58038g%\SO\1066841l\1003446\1011686\997871\153172\NAK\f\CAN~\1051732qs\155291I0|\62022\SUB\161505\1084819\\Dq\SUB{z=\CANKL\53422\GS\DC4\1095233G7ewkJ1\35446J8 O\152777\96173V(\n\SOHuT\184493\142630\&4-\988150\&0\v#\1008772$qO-\SOH/T1\NUL@\53323\1012898\n2s8Bfh\"{vy\EOTG\28934\ETB\DC2g\NAKx\40967$\1111313:\1096564z\984205\r\1113615\50569\1016459\1089112z\1059587\62507U\992158ksD\DC2W,%\STX}\SYNY\1063541\EM\148916\1026506\SYNu\1068118a\DLExoH\b\96516ro^\ESC|\14524\137137\174774\&2\1015701!ReL.)\GS\995824a\134494\111281\38182\ETX\1055512\DLE\53907\DLE5?\DC3\988857Y:\1077940\t)\96370\48426\147806*\158714\1042527`\STX\NAK\FS\GSg\t\1084955*fM\994607\1029549,\ESCTL\STX\NUL\986074\1096953:\a_9i\524\168231\986631Wxh%\1104374`t\1062137i\139608mD\30436\ESC\18940\RSzJ\1014566" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "L`\EOT,Xp\US\SUB\DC39\SOH\986402+\ACKQ\1011739\163475\SYN(\126117S8c\EOT\SOH=d\152742\FSL\34501\EOT\US]\94933~6\DLE:\1038349\46131\RS\DELU}dYj\vm\DC33\ESC\EOT\ESC\STX\SOho\n[P)\EOT\"r\148842s\132918\&6\1013939\1054104X%g^\1111091MDA\GS=\131957I8\r\1059039\DC4-\1093354\95894\992259\155020\".\146604~-\24057]\bBLv\ACK-u5\1099612^st\26172;\SUBq\FS\ESCd\998793\&3\GSII\STXS\177535\DC20\SUBHy\1108265\18293>\DC1e4';\ESCv\f\SYNxF\RSWD\40069\NUL\15936WB\FS\145512/v\1094497\&9\SUB[a\1031802\t\n\187075y%\1065833\&5B,hyc\"!b#h\1092617XC\GS7\995391mZ\NULECj:O:\v/J\SO\1102347\996658\&9\EMs\DC4\a\1059269d>HEz\FS\171554/n\NULeC\1004734\CAN\65713\&2\181341\STX\ACK\1013277v\1000956\94105\986760E`xtrZWt7\164746wMA9r<\1021337\15097Ovo{\1112295\v#f\1040937\991008\SOH\63011j\FSb\r\1011414\v\FS9e\136229?\1019925q\1021008\f\172280/X\24799\STX\ENQl\FS\v\74972\131088RC>Y\ENQ\1073582\&2v\GS\ETB\US-\\,\1041777e\nf\1021970\GSA\DC1\DC4y\1007481\1102343q8\SYN#\NAK\984437\43846j.\n6Is\SUB\1049642\t\1020034tL\1049999\DELT{\173861\1059180Sz\68055\988553\EM\US[\DLE\48766\r<6CnyQ\DLE\RS146\1059541J\DC4\1059543ceb\NULr6(P\917894(\1072768ic\34855/\ENQ\50857\18315\&7\DC2^b\CAN\1000777\f\US#\15234r8-\154704u\r\1016712\SYN\NAKH\SO\985948\27600\1011459\"'\46452\ESC'=\SOH\19188b_\DC1\186563\SUB\174895x`\a\1041293\140522c\EM|\984810\aA\rQV?\1058487\fZ\f\ESC\SI0D\SOHQI\ETX\990028prt\163629\94675\36885\171880\1096809\&9\46899u\EM\1102387\n\13498\DC3}h\1032138o\DLE\1063962tFT\1095317%Az\1086440\&3\US\SOH\EM\38682'\DC3^1\14526(>E\DLE(\n\1066401z[Wg\1100054ad\1007846Mnv\8290\1091875|e\190345\\g\DC4\51159jIsn\DC2\16061\178290\&0\DLE>Lr*Q:\ETX\"\183845\DLE\98183\STXq\DEL" } @@ -244,12 +244,12 @@ testObject_PasswordChange_user_19 = testObject_PasswordChange_user_20 :: PasswordChange testObject_PasswordChange_user_20 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "{dNa\GSEIDDNi\"&P\"Dx~\96634s \NAK&\ESC\SOHe\917580,/}}@\1024844G\USRi\177540\vG\EOT\1068093\"dIcX\128456?\53433h4\RS8}E\b\fAI+\138835\DLEN\ETBg'l\DC4\DEL1lg}\1002968\GS9u\t=\186263)\1048038A)\ETBBD\ENQ\1741%\CAN3\b+,\151430G)0%\74936\78333t8\1105056\CAN\988091oU\DC2N\NUL\DC1U~\1100670[\138598\1110439\&6##\151597<\a\SYN\986482V6\vb>\NULh\NAKq\f\176602<5dHa\tg\DEL\24672\66025\&2=tZ\1050161L9M\a2k\1001329\987951vOkA\r)\r\60697O \63131lNli\34835\\\"b`G\52957\1039861\161828n\DLEP\1077887i0k\1015841w\1040786?\\\ENQg\1005909\RS[Z\SOHN\SOH\CAN\186595:\FS\185811\40960_kBD\"C\DLEB4]w\DEL/JF\NUL?L9V\\9\1096654W\1104044'\FS={e\153126>\1098415\139415D\1112130A\a7G\ETXb\983698Crt*Y\nhD\150279\&5\151537)F\NUL\ETB*\1035725yCu\ETX\SUB%\SIbZ; G\1079499)\SO\1012440_\NAK\DC3,~\175703\SO\153562d\1101051\1084728\&4\1018181R\1059397\19127\1099372\1004409^\161681\32886\&1\DC3\USn\1102891*!\FS|\r=\166562[ql\189334S\NAKr\ESC'\SUBE(\SYN\f{\1112073 |\b\50511\42582\155138\1009867E\NAK\139848\&4\151681\t\68617X\1000541\EOT\1104748.Z\1085819\177246\176778\DC4t\CAN\DC3\23081\&5HV\ESC:$e,L\STX\992003\&7R\1012763.wq\62951\24985:\60845\SOH\SOH\a\67714\8047\&0*.\1022795\1087787\120217P\r\b\167713\1096692\&5\147092\121232\149850\DC1\bsc-\1082366i\DC1\2721\183884\154420A\NAK$\190574jNR\917908\SI\120778\16684\989256;\5681\1057323\SYNRd\STXI;\EM\aK\20933\59636,\EM%\1073632\ENQ\1089709J\1061355nR$Spf\1093436Lsp\1046367[\\\1105079\97069y\t\SYNbC7}|\DLE\SUBg5@]2\1017800S4E\be[\1054254\&6\RS4\146792z\DC2d\nm\83369/JqK\SOHQf\1081923\1079670\&0\95005\SOHHa\1014928&8\111343\61186)~m\101024\RS\vG\SYNz(p\EOT\1052203V\f4\"^+?A|\1037820\n\340r\USF&\CANt\1037756r\tP\SYNDW\DLE2\DC1E|PgQ/\1055897\1034173P\rNH\SYNS\30936\1050463\29463" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\tNYm7k;\985171|w~ue]St\52529[\GS\983717e\DC21\EM\SI\"C\1059834\&0\1003638!\995247|xiw\1027219~YT\57860+\ESC'\185609\&6\1010421(;\ETX!\b\1071987&Y9tW\984137\72988\GS\ACK\1083519\1086906\1107857&a,\NAK\31149\1088114y/d\1080408\SUB\169799\150046wOS\atp\1000950B\181672\ETBi\DC2\1090827\1080180\DELek=r\138679\60557\ESCrf\3126UkFh\ENQ9YA\t]\NULUJS@1?o_-P\ETBxW\171817\139732\48291<\1060487\133433&\DC4\SO6na\1000867!Z}'H\1052135w\r0W*\24217J\SYNIwk\9238AZ\1023004\30337\1013798w\1015506\ETX>\1080073S\158446\1061588o\190641\175249\1070034J\EOTu\STX:(\1066396\172284\1054181@\1030039\n\DC3xMJ\30746\147879Oxj@Np\1066698\1000349\1087808x\SI\ACK\US\988847T\v_w,)w7j\ACK\1046770\1038846\US'h\31697\&4\NAK\138144V\37643g\f\1099746\&3\129560\ETXR\SIPdc{a\STX\191154\DLE\ETX((\CANf\EOT\f\188879e~[+\RSg=g#&MQ\DC2%4\r\r\ETX\65235<\170329#\1109142\&5\36874\USv\bpt\DC3'\EMF\"2\1113106\SUBe\1087311$\1010352 \1068376bK>m>\f.\1052106m\64101MvQ\1065915Q\70336\177129)/\1056483<\CANy\995545J!\DEL#1\v\aq\DC2\1102215\DLE\CAN\1089020D\ACK7W\EMw\"\151987\&3\STXv\21304\126082\ETXxW\189371\1054427<^~\993642\r:WGlhl-!|W.\3598;n\1077840`<\CAN\1109050;NJi\DC3\53248\t ]\DLEH\100145_Z\996436\24307\"\185147\1002533\71437\24999a\DEL\US\1084155\132179\&4U\1017349v\1098626S\166457S.\36067i(\ENQB|VD\43028gW\"->N4\153954R\190825\992013\DC1\NAK\59376\20565%\160113[`\120495@B\168437qjKW\DLEm z\1034188\167428j\1029865P%\SI\98769._\r\DC1(N\990561\DC3\b\DC1\1072625e\41522'olW\ACK>\SYNp\988282H\RSe{\"RN\51331\ETB\DC2\">\1007951Q\DLEYoj+~\FSSMU\"ubD\142953KtW\FST\99243\20978\SOHQm:\RS8)g\1040404\ayZ\156789\1022349E\99162j n83Hf\163774\DC3\47323/2C\DC4\FS]A8-\1067911vp` PlainTextPassword6 -> AuthenticationSubsystem m (Either AuthenticationSubsystemError ()) + Reauthenticate :: UserId -> Maybe PlainTextPassword6 -> AuthenticationSubsystem m () CreatePasswordResetCode :: EmailKey -> AuthenticationSubsystem m () ResetPassword :: PasswordResetIdentity -> PasswordResetCode -> PlainTextPassword8 -> AuthenticationSubsystem m () VerifyPassword :: PlainTextPassword6 -> Password -> AuthenticationSubsystem m (Bool, PasswordStatus) @@ -39,3 +43,12 @@ data AuthenticationSubsystem m a where InternalLookupPasswordResetCode :: EmailKey -> AuthenticationSubsystem m (Maybe PasswordResetPair) makeSem ''AuthenticationSubsystem + +authenticate :: + ( Member (Error AuthenticationSubsystemError) r, + Member AuthenticationSubsystem r + ) => + UserId -> + PlainTextPassword6 -> + Sem r () +authenticate uid pwd = authenticateEither uid pwd >>= either throw pure diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs index 095bb9dfdbc..882882da7c0 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs @@ -14,11 +14,7 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.AuthenticationSubsystem.Error - ( AuthenticationSubsystemError (..), - authenticationSubsystemErrorToHttpError, - ) -where +module Wire.AuthenticationSubsystem.Error where import Imports import Wire.API.Error @@ -31,8 +27,12 @@ data AuthenticationSubsystemError | AuthenticationSubsystemInvalidPasswordResetCode | AuthenticationSubsystemInvalidPhone | AuthenticationSubsystemAllowListError - | AuthenticationSubsystemMissingAuth | AuthenticationSubsystemBadCredentials + | AuthenticationSubsystemInvalidUser + | AuthenticationSubsystemSuspended + | AuthenticationSubsystemEphemeral + | AuthenticationSubsystemPendingInvitation + | AuthenticationSubsystemMissingAuth deriving (Eq, Show) instance Exception AuthenticationSubsystemError @@ -45,5 +45,9 @@ authenticationSubsystemErrorToHttpError = AuthenticationSubsystemResetPasswordMustDiffer -> errorToWai @E.ResetPasswordMustDiffer AuthenticationSubsystemInvalidPhone -> errorToWai @E.InvalidPhone AuthenticationSubsystemAllowListError -> errorToWai @E.AllowlistError - AuthenticationSubsystemMissingAuth -> errorToWai @E.MissingAuth AuthenticationSubsystemBadCredentials -> errorToWai @E.BadCredentials + AuthenticationSubsystemInvalidUser -> errorToWai @E.BadCredentials + AuthenticationSubsystemSuspended -> errorToWai @E.AccountSuspended + AuthenticationSubsystemEphemeral -> errorToWai @E.AccountEphemeral + AuthenticationSubsystemPendingInvitation -> errorToWai @E.AccountPending + AuthenticationSubsystemMissingAuth -> errorToWai @E.MissingAuth diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs index 89dc1f3b39a..763181865a4 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs @@ -23,11 +23,12 @@ module Wire.AuthenticationSubsystem.Interpreter where import Data.ByteString.Conversion +import Data.HavePendingInvitations import Data.Id import Data.Misc import Data.Qualified import Data.Time -import Imports hiding (lookup) +import Imports hiding (local, lookup) import Polysemy import Polysemy.Error import Polysemy.Input @@ -36,21 +37,24 @@ import Polysemy.TinyLog qualified as Log import System.Logger import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Allowlists qualified as AllowLists -import Wire.API.Password as Password +import Wire.API.Password (Password, PasswordStatus (..)) +import Wire.API.Password qualified as Password +import Wire.API.Password qualified as Pasword import Wire.API.User import Wire.API.User.Password -import Wire.AuthenticationSubsystem (AuthenticationSubsystem (..)) +import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem.Error import Wire.EmailSubsystem import Wire.HashPassword import Wire.PasswordResetCodeStore -import Wire.PasswordStore (PasswordStore) +import Wire.PasswordStore (PasswordStore, upsertHashedPassword) import Wire.PasswordStore qualified as PasswordStore import Wire.Sem.Now import Wire.Sem.Now qualified as Now import Wire.SessionStore import Wire.UserKeyStore -import Wire.UserSubsystem (UserSubsystem) +import Wire.UserStore +import Wire.UserSubsystem (UserSubsystem, getLocalAccountBy) import Wire.UserSubsystem qualified as User interpretAuthenticationSubsystem :: @@ -64,13 +68,17 @@ interpretAuthenticationSubsystem :: Member (Input (Local ())) r, Member (Input (Maybe AllowlistEmailDomains)) r, Member PasswordStore r, - Member EmailSubsystem r + Member EmailSubsystem r, + Member UserStore r ) => InterpreterFor UserSubsystem r -> InterpreterFor AuthenticationSubsystem r interpretAuthenticationSubsystem userSubsystemInterpreter = interpret $ userSubsystemInterpreter . \case + AuthenticateEither uid pwd -> authenticateImpl uid pwd + -- TODO: fix reuth to also return Either like we did for Authenticate + Reauthenticate uid pwd -> reauthenticateImpl uid pwd CreatePasswordResetCode userKey -> createPasswordResetCodeImpl userKey ResetPassword ident resetCode newPassword -> resetPasswordImpl ident resetCode newPassword VerifyPassword plaintext pwd -> verifyPasswordImpl plaintext pwd @@ -98,6 +106,65 @@ instance Exception PasswordResetError where displayException InvalidResetKey = "invalid reset key for password reset" displayException InProgress = "password reset already in progress" +authenticateImpl :: + ( Member UserStore r, + Member HashPassword r, + Member PasswordStore r + ) => + UserId -> + PlainTextPassword6 -> + Sem r (Either AuthenticationSubsystemError ()) +authenticateImpl uid plaintext = do + runError $ + getUserAuthenticationInfo uid >>= \case + Nothing -> throw AuthenticationSubsystemInvalidUser + Just (_, Deleted) -> throw AuthenticationSubsystemInvalidUser + Just (_, Suspended) -> throw AuthenticationSubsystemSuspended + Just (_, Ephemeral) -> throw AuthenticationSubsystemEphemeral + Just (_, PendingInvitation) -> throw AuthenticationSubsystemPendingInvitation + Just (Nothing, _) -> throw AuthenticationSubsystemBadCredentials + Just (Just password, Active) -> do + case Pasword.verifyPasswordWithStatus plaintext password of + (False, _) -> throw AuthenticationSubsystemBadCredentials + (True, PasswordStatusNeedsUpdate) -> do + for_ (plainTextPassword8 . fromPlainTextPassword $ plaintext) (hashAndUpdatePwd uid) + (True, _) -> pure () + where + hashAndUpdatePwd u pwd = do + hashed <- hashPassword8 pwd + upsertHashedPassword u hashed + +-- | Password reauthentication. If the account has a password, reauthentication +-- is mandatory. If the account has no password, or is an SSO user, and no password is given, +-- reauthentication is a no-op. +reauthenticateImpl :: + ( Member (Error AuthenticationSubsystemError) r, + Member UserStore r, + Member UserSubsystem r, + Member (Input (Local ())) r + ) => + UserId -> + Maybe (PlainTextPassword' t) -> + Sem r () +reauthenticateImpl user plaintextMaybe = + getUserAuthenticationInfo user >>= \case + Nothing -> throw AuthenticationSubsystemInvalidUser + Just (_, Deleted) -> throw AuthenticationSubsystemInvalidUser + Just (_, Suspended) -> throw AuthenticationSubsystemSuspended + Just (_, PendingInvitation) -> throw AuthenticationSubsystemPendingInvitation + Just (Nothing, _) -> for_ plaintextMaybe $ const (throw AuthenticationSubsystemBadCredentials) + Just (Just pw', Active) -> maybeReAuth pw' + Just (Just pw', Ephemeral) -> maybeReAuth pw' + where + maybeReAuth pw' = case plaintextMaybe of + Nothing -> do + local <- input + musr <- getLocalAccountBy NoPendingInvitations (qualifyAs local user) + unless (maybe False isSamlUser musr) $ throw AuthenticationSubsystemMissingAuth + Just p -> + unless (Password.verifyPassword p pw') do + throw AuthenticationSubsystemBadCredentials + createPasswordResetCodeImpl :: forall r. ( Member PasswordResetCodeStore r, @@ -225,7 +292,7 @@ resetPasswordImpl ident code pw = do Just uid -> do Log.debug $ field "user" (toByteString uid) . field "action" (val "User.completePasswordReset") checkNewIsDifferent uid pw - hashedPw <- hashPassword pw + hashedPw <- hashPassword8 pw PasswordStore.upsertHashedPassword uid hashedPw codeDelete key deleteAllCookies uid @@ -271,7 +338,9 @@ verifyPasswordImpl plaintext password = do pure $ Password.verifyPasswordWithStatus plaintext password verifyProviderPasswordImpl :: - (Member PasswordStore r, Member (Error AuthenticationSubsystemError) r) => + ( Member PasswordStore r, + Member (Error AuthenticationSubsystemError) r + ) => ProviderId -> PlainTextPassword6 -> Sem r (Bool, PasswordStatus) @@ -283,7 +352,9 @@ verifyProviderPasswordImpl pid plaintext = do verifyPasswordImpl plaintext password verifyUserPasswordImpl :: - (Member PasswordStore r, Member (Error AuthenticationSubsystemError) r) => + ( Member PasswordStore r, + Member (Error AuthenticationSubsystemError) r + ) => UserId -> PlainTextPassword6 -> Sem r (Bool, PasswordStatus) diff --git a/libs/wire-subsystems/src/Wire/HashPassword.hs b/libs/wire-subsystems/src/Wire/HashPassword.hs index 48444c0d691..c91854f4316 100644 --- a/libs/wire-subsystems/src/Wire/HashPassword.hs +++ b/libs/wire-subsystems/src/Wire/HashPassword.hs @@ -1,7 +1,9 @@ +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} module Wire.HashPassword where +import Crypto.KDF.Argon2 qualified as Argon2 import Data.Misc import Imports import Polysemy @@ -9,10 +11,26 @@ import Wire.API.Password (Password) import Wire.API.Password qualified as Password data HashPassword m a where - HashPassword :: PlainTextPassword8 -> HashPassword m Password + HashPassword6 :: PlainTextPassword6 -> HashPassword m Password + HashPassword8 :: PlainTextPassword8 -> HashPassword m Password makeSem ''HashPassword -runHashPassword :: (Member (Embed IO) r) => InterpreterFor HashPassword r -runHashPassword = interpret $ \case - HashPassword pw -> liftIO $ Password.mkSafePassword pw +runHashPassword :: + ( Member (Embed IO) r + ) => + Argon2.Options -> + InterpreterFor HashPassword r +runHashPassword opts = + interpret $ + \case + HashPassword6 pw6 -> hashPasswordImpl opts pw6 + HashPassword8 pw8 -> hashPasswordImpl opts pw8 + +hashPasswordImpl :: + (Member (Embed IO) r) => + Argon2.Options -> + PlainTextPassword' t -> + Sem r Password +hashPasswordImpl opts pwd = do + liftIO $ Password.mkSafePassword opts pwd diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 55373c0a37d..96fa494ea14 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -9,6 +9,7 @@ import Data.Id import Imports import Polysemy import Polysemy.Error +import Wire.API.Password import Wire.API.User import Wire.Arbitrary import Wire.StoredUser @@ -67,6 +68,7 @@ data UserStore m a where IsActivated :: UserId -> UserStore m Bool LookupLocale :: UserId -> UserStore m (Maybe (Maybe Language, Maybe Country)) UpdateUserTeam :: UserId -> TeamId -> UserStore m () + GetUserAuthenticationInfo :: UserId -> UserStore m (Maybe (Maybe Password, AccountStatus)) makeSem ''UserStore diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 66d35568d27..4d122d13498 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -9,6 +9,7 @@ import Imports import Polysemy import Polysemy.Embed import Polysemy.Error +import Wire.API.Password (Password) import Wire.API.User hiding (DeleteUser) import Wire.StoredUser import Wire.UserStore @@ -31,6 +32,17 @@ interpretUserStoreCassandra casClient = IsActivated uid -> isActivatedImpl uid LookupLocale uid -> lookupLocaleImpl uid UpdateUserTeam uid tid -> updateUserTeamImpl uid tid + GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid + +getUserAuthenticationInfoImpl :: UserId -> Client (Maybe (Maybe Password, AccountStatus)) +getUserAuthenticationInfoImpl uid = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity uid))) + where + f (pw, st) = (pw, fromMaybe Active st) + authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) + authSelect = + [sql| + SELECT password, status FROM user WHERE id = ? + |] getUsersImpl :: [UserId] -> Client [StoredUser] getUsersImpl usrs = diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs index f553aa595dc..87509b688de 100644 --- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs @@ -33,7 +33,9 @@ import Wire.PasswordStore import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) import Wire.SessionStore +import Wire.StoredUser import Wire.UserKeyStore +import Wire.UserStore type AllEffects = [ AuthenticationSubsystem, @@ -50,6 +52,8 @@ type AllEffects = State (Map PasswordResetKey (PRQueryData Identity)), TinyLog, EmailSubsystem, + UserStore, + State [StoredUser], State (Map EmailAddress [SentMail]) ] @@ -57,6 +61,8 @@ runAllEffects :: Domain -> [User] -> Maybe [Text] -> Sem AllEffects a -> Either runAllEffects localDomain preexistingUsers mAllowedEmailDomains = run . evalState mempty + . evalState mempty + . inMemoryUserStoreInterpreter . emailSubsystemInterpreter . discardTinyLogs . evalState mempty @@ -320,7 +326,7 @@ verifyPasswordProp plainTextPassword passwordHash = counterexample ("Password doesn't match, plainText=" <> show plainTextPassword <> ", passwordHash=" <> show passwordHash) $ fmap (Password.verifyPassword plainTextPassword) passwordHash == Just True -hashAndUpsertPassword :: (Member PasswordStore r, Member HashPassword r) => UserId -> PlainTextPassword8 -> Sem r () +hashAndUpsertPassword :: (Member PasswordStore r) => UserId -> PlainTextPassword8 -> Sem r () hashAndUpsertPassword uid password = upsertHashedPassword uid =<< hashPassword password diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs index 84c8897292a..6684ca34c47 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs @@ -5,23 +5,29 @@ import Data.Misc import Data.Text.Encoding qualified as Text import Imports import Polysemy -import Wire.API.Password +import Wire.API.Password as Password import Wire.HashPassword staticHashPasswordInterpreter :: InterpreterFor HashPassword r staticHashPasswordInterpreter = interpret $ \case - HashPassword password -> - pure . Argon2Password $ - hashPasswordArgon2idWithOptions - fastArgon2IdOptions - "9bytesalt" - (Text.encodeUtf8 (fromPlainTextPassword password)) + HashPassword6 password -> hashPassword password + HashPassword8 password -> hashPassword password + +hashPassword :: (Monad m) => PlainTextPassword' t -> m Password +hashPassword password = + pure . Argon2Password $ + hashPasswordArgon2idWithSalt + fastArgon2IdOptions + "9bytesalt" + (Text.encodeUtf8 (fromPlainTextPassword password)) fastArgon2IdOptions :: Argon2.Options fastArgon2IdOptions = let hashParallelism = 4 - in defaultOptions - { iterations = 1, + in Argon2.Options + { variant = Argon2.Argon2id, + version = Argon2.Version13, + iterations = 1, parallelism = hashParallelism, -- This needs to be min 8 * hashParallelism, otherewise we get an -- unsafe error diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index a4c05c44b5c..02852fb9b58 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -71,6 +71,7 @@ inMemoryUserStoreInterpreter = interpret $ \case modify $ map (\u -> if u.id == uid then u {teamId = Just tid} :: StoredUser else u) + GetUserAuthenticationInfo _uid -> error "Not implemented" storedUserToIndexUser :: StoredUser -> IndexUser storedUserToIndexUser storedUser = diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 3aa2ea8ba36..f814af8c799 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -228,6 +228,10 @@ optSettings: setOAuthEnabled: true setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks setOAuthMaxActiveRefreshTokens: 10 + setPasswordHashingOptions: # in testing, we want these settings to be faster, not secure against attacks. + iterations: 1 + memory: 128 + parallelism: 1 logLevel: Warn logNetStrings: false diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index 4c3e9c4a563..655b08d4cd9 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -22,7 +22,6 @@ import Brig.API.Handler import Brig.API.Types import Brig.API.User import Brig.App -import Brig.Data.User qualified as User import Brig.Options import Brig.User.Auth qualified as Auth import Brig.ZAuth hiding (Env, settings) @@ -49,12 +48,12 @@ import Wire.API.User.Auth hiding (access) import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso -import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem qualified as Authentication import Wire.BlockListStore import Wire.EmailSubsystem (EmailSubsystem) import Wire.Events (Events) import Wire.GalleyAPIAccess -import Wire.PasswordStore (PasswordStore) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem @@ -97,7 +96,6 @@ sendLoginCode _ = login :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member PasswordStore r, Member UserKeyStore r, Member UserStore r, Member Events r, @@ -162,7 +160,6 @@ listCookies lusr (fold -> labels) = removeCookies :: ( Member TinyLog r, - Member PasswordStore r, Member UserSubsystem r, Member AuthenticationSubsystem r ) => @@ -213,7 +210,7 @@ reauthenticate :: ReAuthUser -> Handler r () reauthenticate luid@(tUnqualified -> uid) body = do - User.reauthenticate uid body.reAuthPassword !>> reauthError + (lift . liftSem $ Authentication.reauthenticate uid body.reAuthPassword) !>> reauthError case reAuthCodeAction body of Just action -> Auth.verifyCode (reAuthCode body) action luid diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index d5282714d12..d90c560ae95 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -103,6 +103,7 @@ import Wire.API.User.Client.Prekey import Wire.API.UserEvent import Wire.API.UserMap (QualifiedUserMap (QualifiedUserMap, qualifiedUserMap), UserMap (userMap)) import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem qualified as Authentication import Wire.DeleteQueue import Wire.EmailSubsystem (EmailSubsystem, sendNewClientEmail) import Wire.Events (Events) @@ -271,7 +272,7 @@ rmClient u con clt pw = -- Temporary clients don't need to re-auth TemporaryClientType -> pure () -- All other clients must authenticate - _ -> Data.reauthenticate u pw !>> ClientDataError . ClientReAuthError + _ -> (lift . liftSem $ Authentication.reauthenticate u pw) !>> ClientDataError . ClientReAuthError lift $ execDelete u (Just con) client claimPrekey :: diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 052c5cdb59f..072c9b646c4 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -65,7 +65,7 @@ import Data.Time.Clock.System import Imports hiding (head) import Network.Wai.Utilities as Utilities import Polysemy -import Polysemy.Error qualified +import Polysemy.Error qualified as Polysemy import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) @@ -102,6 +102,7 @@ import Wire.FederationConfigStore ) import Wire.FederationConfigStore qualified as E import Wire.GalleyAPIAccess (GalleyAPIAccess) +import Wire.HashPassword (HashPassword) import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) import Wire.InvitationStore import Wire.NotificationSubsystem @@ -144,7 +145,8 @@ servantSitemap :: Member PropertySubsystem r, Member (Input (Local ())) r, Member IndexedUserStore r, - Member (Polysemy.Error.Error UserSubsystemError) r + Member (Polysemy.Error UserSubsystemError) r, + Member HashPassword r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -196,6 +198,7 @@ accountAPI :: Member PropertySubsystem r, Member Events r, Member PasswordResetCodeStore r, + Member HashPassword r, Member InvitationStore r ) => ServerT BrigIRoutes.AccountAPI (Handler r) @@ -246,7 +249,7 @@ teamsAPI :: Member InvitationStore r, Member TeamInvitationSubsystem r, Member UserSubsystem r, - Member (Polysemy.Error.Error UserSubsystemError) r, + Member (Polysemy.Error UserSubsystemError) r, Member Events r, Member (Input (Local ())) r, Member IndexedUserStore r @@ -468,6 +471,7 @@ createUserNoVerify :: Member UserKeyStore r, Member UserSubsystem r, Member (Input (Local ())) r, + Member HashPassword r, Member PasswordResetCodeStore r ) => NewUser -> @@ -488,6 +492,7 @@ createUserNoVerifySpar :: Member TinyLog r, Member UserSubsystem r, Member Events r, + Member HashPassword r, Member PasswordResetCodeStore r ) => NewUserSpar -> diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 0ab2f89a4fe..7b2e95c63fd 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -57,7 +57,10 @@ import Wire.API.Routes.Internal.Brig.OAuth qualified as I import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig.OAuth import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem qualified as Authentication import Wire.Error +import Wire.HashPassword (HashPassword) +import Wire.HashPassword qualified as HashPassword import Wire.Sem.Jwk import Wire.Sem.Jwk qualified as Jwk import Wire.Sem.Now (Now) @@ -66,7 +69,7 @@ import Wire.Sem.Now qualified as Now -------------------------------------------------------------------------------- -- API Internal -internalOauthAPI :: ServerT I.OAuthAPI (Handler r) +internalOauthAPI :: (Member HashPassword r) => ServerT I.OAuthAPI (Handler r) internalOauthAPI = Named @"create-oauth-client" registerOAuthClient :<|> Named @"i-get-oauth-client" getOAuthClientById @@ -95,19 +98,25 @@ oauthAPI = -------------------------------------------------------------------------------- -- Handlers -registerOAuthClient :: OAuthClientConfig -> (Handler r) OAuthClientCredentials +registerOAuthClient :: (Member HashPassword r) => OAuthClientConfig -> (Handler r) OAuthClientCredentials registerOAuthClient (OAuthClientConfig name uri) = do guardOAuthEnabled credentials@(OAuthClientCredentials cid secret) <- OAuthClientCredentials <$> randomId <*> createSecret - safeSecret <- liftIO $ hashClientSecret secret + safeSecret <- hashClientSecret secret lift $ wrapClient $ insertOAuthClient cid name uri safeSecret pure credentials where createSecret :: (MonadIO m) => m OAuthClientPlainTextSecret createSecret = OAuthClientPlainTextSecret <$> rand32Bytes - hashClientSecret :: (MonadIO m) => OAuthClientPlainTextSecret -> m Password - hashClientSecret = mkSafePassword . plainTextPassword8Unsafe . toText . unOAuthClientPlainTextSecret + hashClientSecret :: (Member HashPassword r) => OAuthClientPlainTextSecret -> (Handler r) Password + hashClientSecret = + lift + . liftSem + . HashPassword.hashPassword8 + . plainTextPassword8Unsafe + . toText + . unOAuthClientPlainTextSecret rand32Bytes :: (MonadIO m) => m AsciiBase16 rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 @@ -358,7 +367,7 @@ revokeOAuthAccountAccess :: PasswordReqBody -> (Handler r) () revokeOAuthAccountAccess luid@(tUnqualified -> uid) cid req = do - reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied + (lift . liftSem $ Authentication.reauthenticate uid req.fromPasswordReqBody) !>> toAccessDenied revokeOAuthAccountAccessV6 luid cid where toAccessDenied :: ReAuthError -> HttpError @@ -372,7 +381,7 @@ deleteOAuthRefreshTokenById :: PasswordReqBody -> (Handler r) () deleteOAuthRefreshTokenById (tUnqualified -> uid) cid tokenId req = do - reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied + (lift . liftSem $ Authentication.reauthenticate uid req.fromPasswordReqBody) !>> toAccessDenied mInfo <- lift $ wrapClient $ lookupOAuthRefreshTokenInfo tokenId case mInfo of Nothing -> pure () diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 017f225a190..066a0fe67e5 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -156,6 +156,7 @@ import Wire.Events (Events) import Wire.FederationConfigStore (FederationConfigStore) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.HashPassword (HashPassword) import Wire.IndexedUserStore (IndexedUserStore) import Wire.InvitationStore import Wire.NotificationSubsystem @@ -303,7 +304,8 @@ servantSitemap :: Member (Concurrency 'Unsafe) r, Member BlockListStore r, Member (ConnectionStore InternalPaging) r, - Member IndexedUserStore r + Member IndexedUserStore r, + Member HashPassword r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -743,6 +745,7 @@ createUser :: Member Events r, Member UserSubsystem r, Member PasswordResetCodeStore r, + Member HashPassword r, Member EmailSending r ) => Public.NewUserPublic -> @@ -967,7 +970,14 @@ removeEmail self = lift . exceptTToMaybe $ API.removeEmail self checkPasswordExists :: (Member PasswordStore r) => UserId -> (Handler r) Bool checkPasswordExists = fmap isJust . lift . liftSem . lookupHashedPassword -changePassword :: (Member PasswordStore r, Member UserStore r) => UserId -> Public.PasswordChange -> (Handler r) (Maybe Public.ChangePasswordError) +changePassword :: + ( Member PasswordStore r, + Member UserStore r, + Member HashPassword r + ) => + UserId -> + Public.PasswordChange -> + (Handler r) (Maybe Public.ChangePasswordError) changePassword u cp = lift . exceptTToMaybe $ API.changePassword u cp changeLocale :: diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index bccba92ac46..4ba39e4df30 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -47,7 +47,6 @@ module Brig.API.User deleteAccount, checkHandles, isBlacklistedHandle, - Data.reauthenticate, -- * Activation sendActivationCode, @@ -137,6 +136,8 @@ import Wire.Error import Wire.Events (Events) import Wire.Events qualified as Events import Wire.GalleyAPIAccess as GalleyAPIAccess +import Wire.HashPassword (HashPassword) +import Wire.HashPassword qualified as HashPassword import Wire.InvitationStore (InvitationStore, StoredInvitation) import Wire.InvitationStore qualified as InvitationStore import Wire.NotificationSubsystem @@ -191,6 +192,7 @@ createUserSpar :: ( Member GalleyAPIAccess r, Member TinyLog r, Member UserSubsystem r, + Member HashPassword r, Member Events r ) => NewUserSpar -> @@ -203,7 +205,7 @@ createUserSpar new = do -- Create account account <- lift $ do - (account, pw) <- wrapClient $ newAccount new' Nothing (Just tid) handle' + (account, pw) <- newAccount new' Nothing (Just tid) handle' let uid = userId account @@ -316,6 +318,7 @@ createUser :: Member Events r, Member (Input (Local ())) r, Member PasswordResetCodeStore r, + Member HashPassword r, Member InvitationStore r ) => NewUser -> @@ -369,7 +372,7 @@ createUser new = do -- Create account account <- lift $ do - (account, pw) <- wrapClient $ newAccount new' mbInv tid mbHandle + (account, pw) <- newAccount new' mbInv tid mbHandle let uid = userId account liftSem $ do @@ -839,15 +842,22 @@ mkActivationKey (ActivateEmail e) = ------------------------------------------------------------------------------- -- Password Management -changePassword :: (Member PasswordStore r, Member UserStore r) => UserId -> PasswordChange -> ExceptT ChangePasswordError (AppT r) () +changePassword :: + ( Member PasswordStore r, + Member UserStore r, + Member HashPassword r + ) => + UserId -> + PasswordChange -> + ExceptT ChangePasswordError (AppT r) () changePassword uid cp = do activated <- lift $ liftSem $ isActivated uid unless activated $ throwE ChangePasswordNoIdentity currpw <- lift $ liftSem $ lookupHashedPassword uid - let newpw = cpNewPassword cp - hashedNewPw <- mkSafePassword newpw - case (currpw, cpOldPassword cp) of + let newpw = cp.newPassword + hashedNewPw <- lift . liftSem $ HashPassword.hashPassword8 newpw + case (currpw, cp.oldPassword) of (Nothing, _) -> lift . liftSem $ upsertHashedPassword uid hashedNewPw (Just _, Nothing) -> throwE InvalidCurrentPassword (Just pw, Just pw') -> do diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index b2967854fd6..7391ff23c8d 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -33,6 +33,7 @@ import Polysemy.TinyLog (TinyLog) import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Federation.Client qualified import Wire.API.Federation.Error +import Wire.API.Password import Wire.ActivationCodeStore (ActivationCodeStore) import Wire.ActivationCodeStore.Cassandra (interpretActivationCodeStoreToCassandra) import Wire.AuthenticationSubsystem @@ -262,7 +263,7 @@ runBrigToIO e (AppT ma) = do . interpretIndexedUserStoreES indexedUserStoreConfig . interpretUserStoreCassandra e.casClient . interpretUserKeyStoreCassandra e.casClient - . runHashPassword + . runHashPassword (argon2OptsFromHashingOpts e.settings.passwordHashingOptions) . interpretFederationAPIAccess federationApiAccessConfig . rethrowHttpErrorIO . mapError propertySubsystemErrorToHttpError diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 320d096d6e2..a5011075766 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -56,7 +56,6 @@ import Bilge.Retry (httpHandlers) import Brig.AWS import Brig.App import Brig.Data.User (AuthError (..), ReAuthError (..)) -import Brig.Data.User qualified as User import Brig.Types.Instances () import Cassandra as C hiding (Client) import Cassandra.Settings as C hiding (Client) @@ -92,6 +91,7 @@ import Wire.API.User.Client hiding (UpdateClient (..)) import Wire.API.User.Client.Prekey import Wire.API.UserMap (UserMap (..)) import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem qualified as Authentication data ClientDataError = TooManyClients @@ -144,9 +144,9 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients caps = do let typed = filter ((== newClientType c) . clientType) clients let count = length typed let upsert = any exists typed - when (reAuthPolicy count upsert) $ - fmapLT ClientReAuthError $ - User.reauthenticate (tUnqualified u) (newClientPassword c) + when (reAuthPolicy count upsert) do + lift . liftSem $ do + Authentication.reauthenticate (tUnqualified u) (newClientPassword c) let capacity = fmap (+ (-count)) limit unless (maybe True (> 0) capacity || upsert) $ throwE TooManyClients diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index caaa7c160cc..b6c2fc4ab11 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -27,9 +27,6 @@ module Brig.Data.User newAccount, newAccountInviteViaScim, insertAccount, - authenticate, - reauthenticate, - isSamlUser, -- * Lookups lookupUser, @@ -74,7 +71,6 @@ import Data.Handle (Handle) import Data.HavePendingInvitations import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) -import Data.Misc import Data.Qualified import Data.Range (fromRange) import Data.Time (addUTCTime) @@ -86,10 +82,10 @@ import Wire.API.Provider.Service import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.RichInfo -import Wire.AuthenticationSubsystem as AuthenticationSubsystem -import Wire.PasswordStore +import Wire.HashPassword -- | Authentication errors. +-- TODO: Rethink these two error types in terms of subsystem data AuthError = AuthInvalidUser | AuthInvalidCredentials @@ -114,12 +110,12 @@ data ReAuthError -- fact that we're setting getting @mbHandle@ from table @"user"@, and when/if it was added -- there, it was claimed properly. newAccount :: - (MonadClient m, MonadReader Env m) => + (Member HashPassword r) => NewUser -> Maybe InvitationId -> Maybe TeamId -> Maybe Handle -> - m (User, Maybe Password) + AppT r (User, Maybe Password) newAccount u inv tid mbHandle = do defLoc <- defaultUserLocale <$> asks (.settings) domain <- viewFederationDomain @@ -129,7 +125,7 @@ newAccount u inv tid mbHandle = do (Just (toUUID -> uuid), _) -> pure uuid (_, Just uuid) -> pure uuid (Nothing, Nothing) -> liftIO nextRandom - passwd <- maybe (pure Nothing) (fmap Just . liftIO . mkSafePasswordScrypt) pass + passwd <- maybe (pure Nothing) (fmap Just . liftSem . hashPassword8) pass expiry <- case status of Ephemeral -> do -- Ephemeral users' expiry time is in expires_in (default sessionTokenTimeout) seconds @@ -180,69 +176,6 @@ newAccountInviteViaScim uid externalId tid locale name email = do ManagedByScim defSupportedProtocols --- | Mandatory password authentication. -authenticate :: - forall r. - (Member PasswordStore r, Member AuthenticationSubsystem r) => - UserId -> - PlainTextPassword6 -> - ExceptT AuthError (AppT r) () -authenticate u pw = - -- FUTUREWORK: Move this logic into auth subsystem. - lift (wrapHttp $ lookupAuth u) >>= \case - Nothing -> throwE AuthInvalidUser - Just (_, Deleted) -> throwE AuthInvalidUser - Just (_, Suspended) -> throwE AuthSuspended - Just (_, Ephemeral) -> throwE AuthEphemeral - Just (_, PendingInvitation) -> throwE AuthPendingInvitation - Just (Nothing, _) -> throwE AuthInvalidCredentials - Just (Just pw', Active) -> do - res <- lift $ liftSem (AuthenticationSubsystem.verifyPassword pw pw') - case res of - (False, _) -> throwE AuthInvalidCredentials - (True, PasswordStatusNeedsUpdate) -> do - -- FUTUREWORK(elland): 6char pwd allowed for now - -- throwE AuthStalePassword in the future - for_ (plainTextPassword8 . fromPlainTextPassword $ pw) (lift . hashAndUpdatePwd u) - (True, _) -> pure () - where - hashAndUpdatePwd :: UserId -> PlainTextPassword8 -> AppT r () - hashAndUpdatePwd uid pwd = do - hashed <- mkSafePassword pwd - liftSem $ upsertHashedPassword uid hashed - --- | Password reauthentication. If the account has a password, reauthentication --- is mandatory. If the account has no password, or is an SSO user, and no password is given, --- reauthentication is a no-op. -reauthenticate :: - (Member AuthenticationSubsystem r) => - UserId -> - Maybe PlainTextPassword6 -> - ExceptT ReAuthError (AppT r) () -reauthenticate u pw = - wrapClientE (lookupAuth u) >>= \case - Nothing -> throwE (ReAuthError AuthInvalidUser) - Just (_, Deleted) -> throwE (ReAuthError AuthInvalidUser) - Just (_, Suspended) -> throwE (ReAuthError AuthSuspended) - Just (_, PendingInvitation) -> throwE (ReAuthError AuthPendingInvitation) - Just (Nothing, _) -> for_ pw $ const (throwE $ ReAuthError AuthInvalidCredentials) - Just (Just pw', Active) -> maybeReAuth pw' - Just (Just pw', Ephemeral) -> maybeReAuth pw' - where - maybeReAuth pw' = case pw of - Nothing -> do - musr <- wrapClientE $ lookupUser NoPendingInvitations u - unless (maybe False isSamlUser musr) $ throwE ReAuthMissingPassword - Just p -> - unlessM (fst <$> lift (liftSem (AuthenticationSubsystem.verifyPassword p pw'))) do - throwE (ReAuthError AuthInvalidCredentials) - -isSamlUser :: User -> Bool -isSamlUser usr = do - case usr.userIdentity of - Just (SSOIdentity (UserSSOId _) _) -> True - _ -> False - insertAccount :: (MonadClient m) => User -> @@ -393,11 +326,6 @@ lookupUserTeam u = (runIdentity =<<) <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity u))) -lookupAuth :: (MonadClient m) => UserId -> m (Maybe (Maybe Password, AccountStatus)) -lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity u))) - where - f (pw, st) = (pw, fromMaybe Active st) - -- | Return users with given IDs. -- -- Skips nonexistent users. /Does not/ skip users who have been deleted. @@ -519,9 +447,6 @@ idSelect = "SELECT id FROM user WHERE id = ?" nameSelect :: PrepQuery R (Identity UserId) (Identity Name) nameSelect = "SELECT name FROM user WHERE id = ?" -authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) -authSelect = "SELECT password, status FROM user WHERE id = ?" - richInfoSelect :: PrepQuery R (Identity UserId) (Identity RichInfoAssocList) richInfoSelect = "SELECT json FROM rich_info WHERE user = ?" diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 7aeacd70efa..04da3707661 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -584,7 +584,9 @@ data Settings = Settings oAuthRefreshTokenExpirationTimeSecsInternal :: !(Maybe Word64), -- | The maximum number of active OAuth refresh tokens a user is allowed to have. -- use `oAuthMaxActiveRefreshTokens` as the getter function which always provides a default value - oAuthMaxActiveRefreshTokensInternal :: !(Maybe Word32) + oAuthMaxActiveRefreshTokensInternal :: !(Maybe Word32), + -- | Options to override the default Argon2id settings for specific operators. + passwordHashingOptions :: !(PasswordHashingOptions) } deriving (Show, Generic) diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 65070d3b420..5d2f9c8e313 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -115,7 +115,7 @@ import Wire.API.Routes.Public.Brig.Services (ServicesAPI) import Wire.API.Team.Feature qualified as Feature import Wire.API.Team.LegalHold (LegalholdProtectee (UnprotectedBot)) import Wire.API.Team.Permission -import Wire.API.User hiding (cpNewPassword, cpOldPassword) +import Wire.API.User import Wire.API.User qualified as Public (UserProfile, mkUserProfile) import Wire.API.User.Auth import Wire.API.User.Client @@ -127,6 +127,8 @@ import Wire.EmailSending (EmailSending) import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.HashPassword (HashPassword) +import Wire.HashPassword qualified as HashPassword import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe)) import Wire.UserKeyStore (mkEmailKey) import Wire.UserSubsystem @@ -180,6 +182,7 @@ providerAPI :: ( Member GalleyAPIAccess r, Member AuthenticationSubsystem r, Member EmailSending r, + Member HashPassword r, Member VerificationCodeSubsystem r ) => ServerT ProviderAPI (Handler r) @@ -209,6 +212,7 @@ internalProviderAPI = Named @"get-provider-activation-code" getActivationCode newAccount :: ( Member GalleyAPIAccess r, Member EmailSending r, + Member HashPassword r, Member VerificationCodeSubsystem r ) => Public.NewProvider -> @@ -223,10 +227,12 @@ newAccount new = do let emailKey = mkEmailKey email wrapClientE (DB.lookupKey emailKey) >>= mapM_ (const $ throwStd emailExists) (safePass, newPass) <- case pass of - Just newPass -> (,Nothing) <$> mkSafePassword newPass + Just newPass -> do + hashed <- lift . liftSem $ HashPassword.hashPassword6 newPass + pure (hashed, Nothing) Nothing -> do newPass <- genPassword - safePass <- mkSafePassword newPass + safePass <- lift . liftSem $ HashPassword.hashPassword8 newPass pure (safePass, Just newPass) pid <- wrapClientE $ DB.insertAccount name safePass url descr let gen = mkVerificationCodeGen email @@ -239,8 +245,8 @@ newAccount new = do (Timeout (3600 * 24)) -- 24h (Just (toUUID pid)) let key = codeKey code - let val = codeValue code - lift $ sendActivationMail name email key val False + let value = codeValue code + lift $ sendActivationMail name email key value False pure $ Public.NewProviderResponse pid newPass activateAccountKey :: @@ -251,9 +257,9 @@ activateAccountKey :: Code.Key -> Code.Value -> (Handler r) (Maybe Public.ProviderActivationResponse) -activateAccountKey key val = do +activateAccountKey key value = do guardSecondFactorDisabled Nothing - c <- (lift . liftSem $ verifyCode key IdentityVerification val) >>= maybeInvalidCode + c <- (lift . liftSem $ verifyCode key IdentityVerification value) >>= maybeInvalidCode (pid, email) <- case (codeAccount c, Just (codeFor c)) of (Just p, Just e) -> pure (Id p, e) _ -> throwStd (errorToWai @'E.InvalidCode) @@ -317,20 +323,21 @@ beginPasswordReset (Public.PasswordReset target) = do completePasswordReset :: ( Member GalleyAPIAccess r, Member AuthenticationSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member HashPassword r ) => Public.CompletePasswordReset -> (Handler r) () -completePasswordReset (Public.CompletePasswordReset key val newpwd) = do +completePasswordReset (Public.CompletePasswordReset key value newpwd) = do guardSecondFactorDisabled Nothing - code <- (lift . liftSem $ verifyCode key VerificationCode.PasswordReset val) >>= maybeInvalidCode + code <- (lift . liftSem $ verifyCode key VerificationCode.PasswordReset value) >>= maybeInvalidCode case Id <$> code.codeAccount of Nothing -> throwStd (errorToWai @E.InvalidPasswordResetCode) Just pid -> do whenM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid newpwd)) do throwStd (errorToWai @E.ResetPasswordMustDiffer) - wrapClientE $ do - DB.updateAccountPassword pid newpwd + hashedPwd <- lift . liftSem $ HashPassword.hashPassword6 newpwd + wrapClientE $ DB.updateAccountPassword pid hashedPwd lift . liftSem $ deleteCode key VerificationCode.PasswordReset -------------------------------------------------------------------------------- @@ -377,7 +384,8 @@ updateAccountEmail pid (Public.EmailUpdate email) = do updateAccountPassword :: ( Member GalleyAPIAccess r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member HashPassword r ) => ProviderId -> Public.PasswordChange -> @@ -388,7 +396,8 @@ updateAccountPassword pid upd = do throwStd (errorToWai @E.BadCredentials) whenM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid upd.newPassword)) do throwStd (errorToWai @E.ResetPasswordMustDiffer) - wrapClientE $ DB.updateAccountPassword pid (newPassword upd) + hashedPwd <- lift . liftSem $ HashPassword.hashPassword6 upd.newPassword + wrapClientE $ DB.updateAccountPassword pid hashedPwd addService :: (Member GalleyAPIAccess r) => diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index b5bb0243120..60a26fb0063 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -29,7 +29,7 @@ import Data.Set qualified as Set import Data.Text qualified as Text import Imports import UnliftIO (mapConcurrently) -import Wire.API.Password +import Wire.API.Password as Password import Wire.API.Provider import Wire.API.Provider.Service hiding (updateServiceTags) import Wire.API.Provider.Service.Tag @@ -115,14 +115,13 @@ deleteAccount pid = retry x5 $ write cql $ params LocalQuorum (Identity pid) updateAccountPassword :: (MonadClient m) => ProviderId -> - PlainTextPassword6 -> + Password -> m () updateAccountPassword pid pwd = do - p <- liftIO $ mkSafePassword pwd - retry x5 $ write cql $ params LocalQuorum (p, pid) + retry x5 $ write cql $ params LocalQuorum (pwd, pid) where cql :: PrepQuery W (Password, ProviderId) () - cql = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET password = ? where id = ?" + cql = "UPDATE provider SET password = ? where id = ?" -------------------------------------------------------------------------------- -- Unique (Natural) Keys diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 597c8156554..cb60347f6e0 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -40,7 +40,6 @@ import Brig.App import Brig.Budget import Brig.Data.Activation qualified as Data import Brig.Data.Client -import Brig.Data.User qualified as Data import Brig.Options qualified as Opt import Brig.Types.Intra import Brig.User.Auth.Cookie @@ -72,11 +71,12 @@ import Wire.API.User import Wire.API.User.Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.Sso -import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem qualified as Authentication +import Wire.AuthenticationSubsystem.Error import Wire.Events (Events) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.PasswordStore (PasswordStore) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem (UserSubsystem) @@ -90,7 +90,6 @@ login :: forall r. ( Member GalleyAPIAccess r, Member TinyLog r, - Member PasswordStore r, Member UserKeyStore r, Member UserStore r, Member VerificationCodeSubsystem r, @@ -105,13 +104,17 @@ login :: login (MkLogin li pw label code) typ = do uid <- resolveLoginId li lift . liftSem . Log.debug $ field "user" (toByteString uid) . field "action" (val "User.login") - wrapHttpClientE $ checkRetryLimit uid - Data.authenticate uid pw `catchE` \case - AuthInvalidUser -> wrapHttpClientE $ loginFailed uid - AuthInvalidCredentials -> wrapHttpClientE $ loginFailed uid - AuthSuspended -> throwE LoginSuspended - AuthEphemeral -> throwE LoginEphemeral - AuthPendingInvitation -> throwE LoginPendingActivation + wrapClientE $ checkRetryLimit uid + + (lift . liftSem $ Authentication.authenticateEither uid pw) >>= \case + Right a -> pure a + Left e -> case e of + AuthenticationSubsystemInvalidUser -> lift (decrRetryLimit uid) >> throwE LoginFailed + AuthenticationSubsystemBadCredentials -> lift (decrRetryLimit uid) >> throwE LoginFailed + AuthenticationSubsystemSuspended -> throwE LoginSuspended + AuthenticationSubsystemEphemeral -> throwE LoginEphemeral + AuthenticationSubsystemPendingInvitation -> throwE LoginPendingActivation + _ -> pure () -- TODO: constraint this error with a wrapper? Same for reauth. Check previous implementation for how it was done / what is meant. verifyLoginCode code uid newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label where @@ -120,9 +123,9 @@ login (MkLogin li pw label code) typ = do luid <- lift $ qualifyLocal uid verifyCode mbCode Login luid `catchE` \case - VerificationCodeNoPendingCode -> wrapHttpClientE $ loginFailedWith LoginCodeInvalid uid - VerificationCodeRequired -> wrapHttpClientE $ loginFailedWith LoginCodeRequired uid - VerificationCodeNoEmail -> wrapHttpClientE $ loginFailed uid + VerificationCodeNoPendingCode -> lift (decrRetryLimit uid) >> throwE LoginCodeInvalid + VerificationCodeRequired -> lift (decrRetryLimit uid) >> throwE LoginCodeRequired + VerificationCodeNoEmail -> lift (decrRetryLimit uid) >> throwE LoginFailed verifyCode :: forall r. @@ -137,7 +140,7 @@ verifyCode mbCode action luid = do mbFeatureEnabled <- liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` mbTeamId pure $ fromMaybe ((def @(Feature Public.SndFactorPasswordChallengeConfig)).status == Public.FeatureStatusEnabled) mbFeatureEnabled account <- lift . liftSem $ User.getAccountNoFilter luid - let isSsoUser = maybe False Data.isSamlUser account + let isSsoUser = maybe False isSamlUser account when (featureEnabled && not isSsoUser) $ do case (mbCode, mbEmail) of (Just code, Just email) -> do @@ -158,23 +161,26 @@ verifyCode mbCode action luid = do userTeam =<< mbAccount ) -loginFailedWith :: (MonadClient m, MonadReader Env m) => LoginError -> UserId -> ExceptT LoginError m () -loginFailedWith e uid = decrRetryLimit uid >> throwE e - -loginFailed :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () -loginFailed = loginFailedWith LoginFailed - -decrRetryLimit :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () -decrRetryLimit = withRetryLimit (\k b -> withBudget k b $ pure ()) +decrRetryLimit :: UserId -> (AppT r) () +decrRetryLimit = wrapClient . withRetryLimit (\k b -> withBudget k b $ pure ()) -checkRetryLimit :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () -checkRetryLimit = withRetryLimit checkBudget +checkRetryLimit :: + ( MonadReader Env m, + MonadClient m + ) => + UserId -> + ExceptT LoginError m () +checkRetryLimit uid = + flip withRetryLimit uid $ \budgetKey budget -> + checkBudget budgetKey budget >>= \case + BudgetExhausted ttl -> throwE . LoginBlocked . RetryAfter . floor $ ttl + BudgetedValue () remaining -> pure $ BudgetedValue () remaining withRetryLimit :: (MonadReader Env m) => - (BudgetKey -> Budget -> ExceptT LoginError m (Budgeted ())) -> + (BudgetKey -> Budget -> m (Budgeted ())) -> UserId -> - ExceptT LoginError m () + m () withRetryLimit action uid = do mLimitFailedLogins <- asks (.settings.limitFailedLogins) forM_ mLimitFailedLogins $ \opts -> do @@ -183,10 +189,7 @@ withRetryLimit action uid = do Budget (timeoutDiff $ Opt.timeout opts) (fromIntegral $ Opt.retryLimit opts) - bresult <- action bkey budget - case bresult of - BudgetExhausted ttl -> throwE . LoginBlocked . RetryAfter . floor $ ttl - BudgetedValue () _ -> pure () + action bkey budget logout :: (ZAuth.TokenPair u a) => @@ -219,7 +222,6 @@ renewAccess uts at mcid = do revokeAccess :: ( Member TinyLog r, - Member PasswordStore r, Member UserSubsystem r, Member AuthenticationSubsystem r ) => @@ -232,8 +234,9 @@ revokeAccess luid@(tUnqualified -> u) pw cc ll = do lift . liftSem $ Log.debug $ field "user" (toByteString u) . field "action" (val "User.revokeAccess") isSaml <- lift . liftSem $ do account <- User.getAccountNoFilter luid - pure $ maybe False Data.isSamlUser account - unless isSaml $ Data.authenticate u pw + pure $ maybe False isSamlUser account + unless isSaml do + lift . liftSem $ Authentication.authenticateEither u pw >>= undefined -- TODO: ! Map to the right error? lift $ wrapHttpClient $ revokeCookies u cc ll -------------------------------------------------------------------------------- @@ -389,17 +392,16 @@ ssoLogin :: CookieType -> ExceptT LoginError (AppT r) (Access ZAuth.User) ssoLogin (SsoLogin uid label) typ = do - (Data.reauthenticate uid Nothing) `catchE` \case - ReAuthMissingPassword -> pure () - ReAuthCodeVerificationRequired -> pure () - ReAuthCodeVerificationNoPendingCode -> pure () - ReAuthCodeVerificationNoEmail -> pure () - ReAuthError e -> case e of - AuthInvalidUser -> throwE LoginFailed - AuthInvalidCredentials -> pure () - AuthSuspended -> throwE LoginSuspended - AuthEphemeral -> throwE LoginEphemeral - AuthPendingInvitation -> throwE LoginPendingActivation + lift + -- TODO: fix it! + (liftSem $ Authentication.reauthenticate uid Nothing) + `catchE` \case + AuthenticationSubsystemBadCredentials -> throwE LoginFailed + AuthenticationSubsystemInvalidUser -> pure () + AuthenticationSubsystemSuspended -> throwE LoginSuspended + AuthenticationSubsystemEphemeral -> throwE LoginEphemeral + AuthenticationSubsystemPendingInvitation -> throwE LoginPendingActivation + _ -> pure () -- TODO: newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label -- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. @@ -414,7 +416,7 @@ legalHoldLogin :: CookieType -> ExceptT LegalHoldLoginError (AppT r) (Access ZAuth.LegalHoldUser) legalHoldLogin (LegalHoldLogin uid pw label) typ = do - (Data.reauthenticate uid pw) !>> LegalHoldReAuthError + (lift . liftSem $ Authentication.reauthenticate uid pw) !>> LegalHoldReAuthError -- legalhold login is only possible if -- the user is a team user -- and the team has legalhold enabled diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index f37ad772048..16eb88d3cae 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -62,7 +62,7 @@ import UnliftIO.Async hiding (wait) import Util import Util.Timeout import Wire.API.Conversation (Conversation (..)) -import Wire.API.Password (Password, mkSafePassword) +import Wire.API.Password as Password import Wire.API.User as Public import Wire.API.User.Auth as Auth import Wire.API.User.Auth.LegalHold @@ -101,10 +101,11 @@ tests conf m z db b g n = test m "testLoginFailure - failure" (testLoginFailure b), test m "throttle" (testThrottleLogins conf b), test m "testLimitRetries - limit-retry" (testLimitRetries conf b), - test m "login with 6 character password" (testLoginWith6CharPassword b db), + test m "login with 6 character password" (testLoginWith6CharPassword conf b db), testGroup "sso-login" [ test m "email" (testEmailSsoLogin b), + test m "login-non-sso-fails" (testEmailSsoLoginNonSsoUser b), test m "failure-suspended" (testSuspendedSsoLogin b), test m "failure-no-user" (testNoUserSsoLogin b) ], @@ -168,8 +169,8 @@ tests conf m z db b g n = ] ] -testLoginWith6CharPassword :: Brig -> DB.ClientState -> Http () -testLoginWith6CharPassword brig db = do +testLoginWith6CharPassword :: Opts.Opts -> Brig -> DB.ClientState -> Http () +testLoginWith6CharPassword opts brig db = do (uid, Just email) <- (userId &&& userEmail) <$> randomUser brig checkLogin email defPassword 200 let pw6 = plainTextPassword6Unsafe "123456" @@ -193,7 +194,7 @@ testLoginWith6CharPassword brig db = do updatePassword :: (MonadClient m) => UserId -> PlainTextPassword6 -> m () updatePassword u t = do - p <- liftIO $ mkSafePassword t + p <- mkSafePassword (argon2OptsFromHashingOpts opts.settings.passwordHashingOptions) t retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) userPasswordUpdate :: PrepQuery W (Password, UserId) () @@ -577,16 +578,35 @@ testLegalHoldLogout brig galley = do -- right password. testEmailSsoLogin :: Brig -> Http () testEmailSsoLogin brig = do - -- Create a user - uid <- Public.userId <$> randomUser brig + teamid <- snd <$> createUserWithTeam brig + let ssoid = UserSSOId mkSimpleSampleUref + -- creating user with sso_id, team_id + profile :: SelfProfile <- + responseJsonError + =<< postUser "dummy" True False (Just ssoid) (Just teamid) brig Http () +testEmailSsoLoginNonSsoUser brig = do + -- Create a user + uid <- Public.userId <$> randomUser brig + -- Login and do some checks + void $ + ssoLogin brig (SsoLogin uid Nothing) PersistentCookie + @@ -522,7 +522,7 @@ addCodeUnqualified :: Member (Input (Local ())) r, Member (Input UTCTime) r, Member (Input Opts) r, - Member (Embed IO) r, + Member HashPassword r, Member TeamFeatureStore r ) => Maybe CreateConversationCodeRequest -> @@ -545,11 +545,11 @@ addCode :: Member (ErrorS 'GuestLinksDisabled) r, Member (ErrorS 'CreateConversationCodeConflict) r, Member ExternalAccess r, + Member HashPassword r, Member NotificationSubsystem r, Member (Input UTCTime) r, Member (Input Opts) r, - Member TeamFeatureStore r, - Member (Embed IO) r + Member TeamFeatureStore r ) => Local UserId -> Maybe ZHostValue -> @@ -569,7 +569,7 @@ addCode lusr mbZHost mZcon lcnv mReq = do Nothing -> do ttl <- realToFrac . unGuestLinkTTLSeconds . fromMaybe defGuestLinkTTLSeconds . view (settings . guestLinkTTLSeconds) <$> input code <- E.generateCode (tUnqualified lcnv) ReusableCode (Timeout ttl) - mPw <- for (mReq >>= (.password)) mkSafePassword + mPw <- for (mReq >>= (.password)) HashPassword.hashPassword8 E.createCode code mPw now <- input let event = Event (tUntagged lcnv) Nothing (tUntagged lusr) now (EdConvCodeUpdate (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri)) diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index baa3284e861..26a50c87bff 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -106,8 +106,10 @@ import UnliftIO.Exception qualified as UnliftIO import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Federation.Error +import Wire.API.Password import Wire.API.Team.Feature import Wire.GundeckAPIAccess (runGundeckAPIAccess) +import Wire.HashPassword import Wire.NotificationSubsystem.Interpreter (runNotificationSubsystemGundeck) import Wire.Rpc import Wire.Sem.Delay @@ -118,6 +120,7 @@ import Wire.Sem.Random.IO type GalleyEffects0 = '[ Input ClientState, Input Env, + HashPassword, Error InvalidInput, Error InternalError, -- federation errors can be thrown by almost every endpoint, so we avoid @@ -251,6 +254,7 @@ evalGalley e = . mapError toResponse . mapError toResponse . mapError toResponse + . runHashPassword (argon2OptsFromHashingOpts e._options._settings._passwordHashingOptions) . runInputConst e . runInputConst (e ^. cstate) . mapError toResponse -- DynError diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index a2b233b5e13..be813e6ee3d 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -18,7 +18,7 @@ -- with this program. If not, see . module Galley.Options - ( Settings, + ( Settings (..), httpPoolSize, maxTeamSize, maxFanoutSize, @@ -53,6 +53,7 @@ module Galley.Options logFormat, guestLinkTTLSeconds, defGuestLinkTTLSeconds, + passwordHashingOptions, GuestLinkTTLSeconds (..), ) where @@ -141,7 +142,8 @@ data Settings = Settings _disabledAPIVersions :: !(Set VersionExp), -- | The lifetime of a conversation guest link in seconds with the maximum of 1 year (31536000 seconds). -- If not set use the default `defGuestLinkTTLSeconds` - _guestLinkTTLSeconds :: !(Maybe GuestLinkTTLSeconds) + _guestLinkTTLSeconds :: !(Maybe GuestLinkTTLSeconds), + _passwordHashingOptions :: !(PasswordHashingOptions) } deriving (Show, Generic)