From 692f57959d2272d0d671fa00bddb8a4db8aa6494 Mon Sep 17 00:00:00 2001 From: charliemirabile <46761267+charliemirabile@users.noreply.github.com> Date: Sun, 5 May 2024 21:31:49 -0400 Subject: [PATCH 1/7] orbit: db: schema: introduce the possibility of a null password hash A better approach to handling registration (that importantly does not involve storing plain text passwords in a database ever) will involve storing a null password hash for an unregistered user. The binary blob is updated to reflect the new schema. A check is added to the check_credentials function to handle a null password hash and prevent the user from logging in even though currently hyperspace will never create such accounts. --- orbit/db.py | 2 +- orbit/radius.py | 2 ++ test.sh | 86 ++++++++++++++++++++++++------------------------- 3 files changed, 46 insertions(+), 44 deletions(-) diff --git a/orbit/db.py b/orbit/db.py index 516fe738..26a64ab9 100755 --- a/orbit/db.py +++ b/orbit/db.py @@ -13,7 +13,7 @@ class Meta: class User(BaseModel): username = peewee.TextField(unique=True) - pwdhash = peewee.TextField() + pwdhash = peewee.TextField(null=True) student_id = peewee.TextField(unique=True, null=True) diff --git a/orbit/radius.py b/orbit/radius.py index 1de2f79b..e2613da6 100644 --- a/orbit/radius.py +++ b/orbit/radius.py @@ -30,6 +30,8 @@ def check_credentials(username, password): if not (user := db.User.get_or_none(db.User.username == username)): return False + if not user.pwdhash: + return False return bcrypt.checkpw(password.encode(), user.pwdhash.encode()) diff --git a/test.sh b/test.sh index eb951888..eadc14ff 100755 --- a/test.sh +++ b/test.sh @@ -81,49 +81,49 @@ ${DOCKER} volume export singularity_orbit-db > test/orbit_orig.tar # Import an empty orbit db with no users or sessions xxd -r <<- 'EOF' | gunzip | ${DOCKER} volume import singularity_orbit-db - 00000000: 1f8b 0800 0000 0000 0003 edda 5d4f d350 ............]O.P -00000010: 18c0 f196 b76e c826 5e2c 8d72 735c 24ba .....n.&^,.rs\$. -00000020: 2064 2881 186e 18d8 e8e2 1c32 ba04 6e5c d(..n.....2..n\ -00000030: 4a56 a511 366c bb80 37c4 f151 fc28 c60f JV..6l..7..Q.(.. -00000040: 665f f6d2 1554 bc40 49f3 ff25 5b7b 5efa f_...T.@I..%[{^. -00000050: 9c9d e774 4b4e bab6 7d60 b94b cd03 e906 ...tKN..}`.K.... -00000060: 153d ab2b 2bc1 7139 762c 2e3f f3df c3f3 .=.++.q9v,.?.... -00000070: 9077 beb6 525c 9544 f126 3f54 5fc7 710d .w..R\.D.&?T_.q. -00000080: db1b f25f 8c75 0bed ee54 2cd7 141f daf6 ..._.u...T,..... -00000090: b1e1 8ae7 d2ac 24cb d286 105e 53ca 7b4d ......$....^S.{M -000000a0: 47ba fae5 8948 59be 46f8 94b4 647f cb64 G....HY.F...d..d -000000b0: 37a4 f49d 1fd2 ddcd ecf7 eceb ccd7 ecbd 7............... -000000c0: cc83 99f3 9947 5e15 0000 0080 bff4 3ead .....G^.......>. -000000d0: a80b 39b9 9bb6 5a4d f3ac e398 76c3 713b ..9...ZM....v.q; -000000e0: 4db3 e536 aca6 5f9c deaa 6925 5d13 f56a M..6.._...i%]..j -000000f0: 79a7 ae89 72f5 a5b6 27f2 b18e 79b1 5d0d y...r...'...y.]. -00000100: 2bf3 e249 3e52 5fd8 4b29 ea63 2fbe 3c8c +..I>R_.K).c/.<. -00000110: efbf b58c 63d3 3fa6 7f1d bddf 6d34 f6a0 ....c.?.....m4.. -00000120: b6d0 bdaf 286a 2e27 5fe4 5ce3 e028 88e6 ....(j.'_.\..(.. -00000130: bf52 bd88 7a69 b3a2 0daf f33f 63b9 aa6b .R..zi.....?c..k -00000140: afb4 9aa8 6eeb a25a af54 c4bb 5af9 6da9 ....n..Z.T..Z.m. -00000150: b62f de68 fb4f c530 b4d0 b53d 7dd0 cb6b ./.h.O.0...=}..k -00000160: 3939 6d1e 1ace e1e5 8668 06fc b682 d8d5 99m......h...... -00000170: 6be5 2dbd 39a5 a88b 7372 3713 ccda 311d k.-.9...sr7...1. -00000180: c76a b706 33ea 9595 2be7 1eef 1c4e bf57 .j..3...+....N.W -00000190: 1bcb 4076 4251 e7bc 619c 2003 bd3e bdc3 ..@vBQ..a. ..>.. -000001a0: e468 1e22 01dc f627 b315 9bcb b553 619e .h."...'.....Sa. -000001b0: 9d58 f697 bcf0 8257 06f5 fd69 2f4e 4ea9 .X.....W...i/NN. -000001c0: a539 590a 67fd f9c8 db1d 368c 8edb 0eca .9Y.g.....6..... -000001d0: 8dfe cc96 7b27 5352 b82f 94ce c615 756d ....{'SR./....um -000001e0: 5eee 3e0c fad9 e647 cb71 6dc3 8d66 215a ^.>....G.qm..f!Z -000001f0: 3971 65de aebc 2c4c 5eb4 6934 83e7 638a 9qe...,L^.i4..c. -00000200: baee 8d5c b83c f270 69a3 d5e3 7f1e 3bfe ...\.<.pi.....;. -00000210: ad88 8f1e fd76 74d7 6545 9d9f 972f 5e04 .....vt.eE.../^. -00000220: 2b18 ed1a 3d1f 1b5d cb78 c46b dcdb f11b +...=..].x.k.... -00000230: 35ba a4bf b9ef 0dc7 396d dbf1 6bfa cb9d 5.......9m..k... -00000240: f117 6ff6 bffe 7c01 0000 0000 801b 163c ..o...|........< -00000250: e067 ff0f 0000 0000 40a2 b1ff 0700 0000 .g......@....... -00000260: 0020 f9f8 ff3f 0000 0000 00c9 c7f3 7f00 . ...?.......... -00000270: 0000 0000 928f fd3f 0000 0000 00c9 c7ff .......?........ -00000280: ff01 0000 0000 483e 9eff 0300 0000 0090 ......H>........ -00000290: 7cec ff01 0000 0000 0000 0000 0000 0000 |............... -000002a0: 0000 0000 0000 e076 fa09 3ff3 0562 00c8 .......v..?..b.. -000002b0: 0000 .. +00000010: 18c0 f196 b76e c826 5e90 2672 735c 4274 .....n.&^.&rs\Bt +00000020: 51c8 5002 31dc 30b0 d1c5 3964 9404 6e5c Q.P.1.0...9d..n\ +00000030: 4a56 a511 366c bb80 37c6 71e3 f7f0 a378 JV..6l..7.q....x +00000040: ef77 f2b4 dd4b 5751 8909 4a9a ff2f d9da .w...KWQ..J../.. +00000050: 737a fa9c 3ee7 744b 4eda b67b e8f8 4bcd sz..>.tKN..{..K. +00000060: 43e5 1a95 a4d5 9595 70bb 9cd8 9696 1f07 C.......p....... +00000070: dfd1 7e44 eeaf ad94 5615 51ba ce8b eaeb ..~D....V.Q..... +00000080: 78be e5ca 2eff 455f 37d0 ee4e d5f1 6df1 x.....E_7..N..m. +00000090: b6ed 9e58 be78 a2cc 2aaa aa6c 0821 0f65 ...X.x..*..l.!.e +000000a0: e467 3ad6 3428 4fc4 caea 15c2 6794 25f7 .g:.4(O.....g.%. +000000b0: 6b2e bfa1 646f 7d57 6e6f e6bf e55f e43e k...do}Wno..._.> +000000c0: e7ef e4ee ce7c 9959 9455 0000 0000 fed6 .....|.Y.U...... +000000d0: 9bac a63f 9c53 bb59 a7d5 b4cf 3b9e ed36 ...?.S.Y....;..6 +000000e0: 3cbf d3b4 5b7e c369 06c5 e9ad ba51 360d <...[~.i.....Q6. +000000f0: b157 abec ec19 a252 7b66 ec8b 42a2 6141 .W.....R{f..B.aA +00000100: 6cd7 a2ca 8278 5088 d517 f733 9a7e 5fc6 l....xP....3.~_. +00000110: 5787 f183 af96 7562 07db ecaf a3f7 9b8d W.....ub........ +00000120: c61e d416 bbb3 9aa6 cfcd a917 e3be 7578 ..............ux +00000130: 1c46 0b3e 995e 44b3 bc59 3586 e705 d758 .F.>.^D..Y5....X +00000140: a999 c673 a32e 6adb a6a8 ed55 abe2 75bd ...s..j....U..u. +00000150: f2aa 5c3f 102f 8d83 4762 185a 98c6 be39 ..\?./..Gb.Z...9 +00000160: 6825 8f9c 9e35 8f2c ef28 3a20 cbf1 c483 h%...5.,.(: .... +00000170: aaa2 d835 eb95 2db3 39a5 e98b f36a 3717 ...5..-.9....j7. +00000180: 26eb d99e e7b4 5b83 447a 65ed d294 938d &.....[.Dze..... +00000190: a3ac 7bb5 89c4 f313 9a3e 2fbb f1c2 c47b ..{......>/....{ +000001a0: 6d7a 9bc9 d1f4 6301 fcf6 7bbb 95c8 edca mz....c...{..... +000001b0: 2360 9f9f 3aee c782 90c1 ab83 fa7e da8b #`..:........~.. +000001c0: 9353 7a79 5e55 a2ac 3f1c cbd5 61c3 eaf8 .Szy^U..?...a... +000001d0: edb0 dce8 67b6 dcdb 9952 a275 a172 3eae ....g....R.u.r>. +000001e0: e96b 0b6a f75e d8ce b5df 399e ef5a 7e7c .k.j.^....9..Z~| +000001f0: 14e2 9513 978e dba5 a745 8317 3f34 3a82 .........E..?4:. +00000200: 9fc6 347d 5df6 5cfc b9e7 e1d4 c6ab c7ff ..4}].\......... +00000210: dc77 f2c7 90ec 3dfe a3e8 aeab 9abe b0a0 .w....=......... +00000220: 5e3c 0d67 30de 34be 3f36 3a97 c988 57b8 ^<.g0.4.?6:...W. +00000230: a593 376a 7c4a 7f73 bb5b 9e77 d676 93e7 ..7j|J.s.[.w.v.. +00000240: f4a7 3b17 4cde ec7f fdd7 0200 0000 0000 ..;.L........... +00000250: d72c 7cc0 cffa 1f00 0000 0080 5463 fd0f .,|.........Tc.. +00000260: 0000 0000 40fa f1fe 3f00 0000 0000 e9c7 ....@...?....... +00000270: f37f 0000 0000 00d2 8ff5 3f00 0000 0000 ..........?..... +00000280: e9c7 fbff 0000 0000 00a4 1fcf ff01 0000 ................ +00000290: 0000 483f d6ff 0000 0000 0000 0000 0000 ..H?............ +000002a0: 0000 0000 0000 0000 0070 33fd 007a eebd .........p3..z.. +000002b0: 9700 c800 00 ..... EOF # Restore the old orbit db after testing completes From 5a0b39ebe9f6cdcce50cff3c66cc5fa987cde4b4 Mon Sep 17 00:00:00 2001 From: charliemirabile <46761267+charliemirabile@users.noreply.github.com> Date: Sun, 5 May 2024 21:32:45 -0400 Subject: [PATCH 2/7] orbit: hyperspace: remove password requirement for creating user Now that password hash can be null, we don't need to require a password when creating a user. The user can be created and then their password can be set later using `hyperspace --mutate-password`. --- orbit/hyperspace.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/orbit/hyperspace.py b/orbit/hyperspace.py index 6c750901..a552af9c 100755 --- a/orbit/hyperspace.py +++ b/orbit/hyperspace.py @@ -65,12 +65,14 @@ def do_bcrypt_hash(args): def do_newuser(args): - need(args, u=True, p=True) - new_hash = do_bcrypt_hash(args) + need(args, u=True) + new_hash = None + if args.password is not None: + new_hash = do_bcrypt_hash(args) try: db.User.create(username=args.username, pwdhash=new_hash, student_id=args.studentid) - if args.studentid: + if args.studentid and new_hash: db.Registration.create(username=args.username, password=args.password, student_id=args.studentid) From e0f9e96cb135630ca3387f4072079c30013f574f Mon Sep 17 00:00:00 2001 From: charliemirabile <46761267+charliemirabile@users.noreply.github.com> Date: Sun, 5 May 2024 21:33:37 -0400 Subject: [PATCH 3/7] orbit: hyperspace: add the ability clear a password hash if a user has a null password hash they will not be able to log in so this can be used to lock the account. It can be unlocked again by setting a password with `hyperspace --mutate-password`. --- orbit/hyperspace.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/orbit/hyperspace.py b/orbit/hyperspace.py index a552af9c..42c916fc 100755 --- a/orbit/hyperspace.py +++ b/orbit/hyperspace.py @@ -49,6 +49,15 @@ def do_change_password(args): nou(args.username) +def do_reset_password(args): + need(args, u=True) + query = (db.User + .update({db.User.pwdhash: None}) + .where(db.User.username == args.username)) + if query.execute() < 1: + nou(args.username) + + def do_delete_user(args): need(args, u=True) query = (db.User @@ -112,6 +121,9 @@ def hyperspace_main(raw_args): actions.add_argument('-m', '--mutatepassword', action='store_const', help='Change password for supplied username to supplied password', # NOQA: E501 dest='do', const=do_change_password) + actions.add_argument('-c', '--clearpassword', action='store_const', + help='clear password for supplied username so they canot login', # NOQA: E501 + dest='do', const=do_reset_password) actions.add_argument('-w', '--withdrawuser', action='store_const', help='Delete ("withdraw") the supplied username', dest='do', const=do_delete_user) From ece94ddf47eb7a1b6f7639997069c269fe7187ef Mon Sep 17 00:00:00 2001 From: charliemirabile <46761267+charliemirabile@users.noreply.github.com> Date: Sun, 5 May 2024 21:34:25 -0400 Subject: [PATCH 4/7] orbit: radius: introduce function to find credentials for register This function makes it easier to add more ways to retrieve credentials because it can use positive logic and early return if it is successful and leave the negative logic early return for the main function. --- orbit/radius.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/orbit/radius.py b/orbit/radius.py index e2613da6..f23a3169 100644 --- a/orbit/radius.py +++ b/orbit/radius.py @@ -384,6 +384,16 @@ def handle_dashboard(rocket): return handle_stub(rocket, ['dashboard in development, check back later']) +def find_creds_for_registration(student_id): + query = (db.Registration + .delete() + .where(db.Registration.student_id == student_id) + .returning(db.Registration)) + if registration := next(iter(query.execute()), None): + return (registration.username, registration.password) + return None + + def handle_register(rocket): def form_respond(): return rocket.respond(''' @@ -398,18 +408,15 @@ def form_respond(): if not (student_id := rocket.body_args_query('student_id')): rocket.msg('you must provide a student id') return form_respond() - query = (db.Registration - .delete() - .where(db.Registration.student_id == student_id) - .returning(db.Registration)) - if not (registration := next(iter(query.execute()), None)): + if not (creds := find_creds_for_registration(student_id)): rocket.msg('no such student') return form_respond() + username, password = creds rocket.msg('welcome to the classroom') return rocket.respond(f'''

Save these credentials, you will not be able to access them again


-

Username: {registration.username}


-

Password: {registration.password}


''') +

Username: {username}


+

Password: {password}


''') def handle_cgit(rocket): From 7fa744005a375709d75d56c8bada34875dbb6ca7 Mon Sep 17 00:00:00 2001 From: charliemirabile <46761267+charliemirabile@users.noreply.github.com> Date: Sun, 5 May 2024 21:35:10 -0400 Subject: [PATCH 5/7] orbit: radius: allow users with null password hash to register They will receive a new randomly generated password and the hash will be updated in the database. --- orbit/config.py | 3 +++ orbit/radius.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/orbit/config.py b/orbit/config.py index 07fe92a3..cace6907 100644 --- a/orbit/config.py +++ b/orbit/config.py @@ -7,3 +7,6 @@ # duration of authentication token validity period minutes_each_session_token_is_valid = 180 + +# length in bytes of amount of random data used to generate passwords +num_bytes_entropy_for_pw = 24 diff --git a/orbit/radius.py b/orbit/radius.py index f23a3169..b876746c 100644 --- a/orbit/radius.py +++ b/orbit/radius.py @@ -391,6 +391,19 @@ def find_creds_for_registration(student_id): .returning(db.Registration)) if registration := next(iter(query.execute()), None): return (registration.username, registration.password) + + password = secrets.token_urlsafe(nbytes=config.num_bytes_entropy_for_pw) + salt = bcrypt.gensalt() + pwdhash = bcrypt.hashpw(password.encode(), salt).decode() + + query = (db.User + .update({db.User.pwdhash: pwdhash}) + .where((db.User.student_id == student_id) & + db.User.pwdhash.is_null()) + .returning(db.User)) + if (user := next(iter(query.execute()), None)): + return user.username, password + return None From 0c431ed53ee6bbe3a7cb54cd17b0fb31cd58aea1 Mon Sep 17 00:00:00 2001 From: charliemirabile <46761267+charliemirabile@users.noreply.github.com> Date: Sun, 5 May 2024 21:35:57 -0400 Subject: [PATCH 6/7] tests: capture random password from registration We can stop providing a password when creating the user so that a random one will be generated when they register using the new system. We then capture the password from the registration page to use it when logging in instead of a hard-coded password. --- test.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test.sh b/test.sh index eadc14ff..56f5919a 100755 --- a/test.sh +++ b/test.sh @@ -146,7 +146,7 @@ curl --url "https://$SINGULARITY_HOSTNAME/login" \ | grep "msg = authentication failure" # Check that we can create a user -orbit/warpdrive.sh -u user -p pass -i 1234 -n +orbit/warpdrive.sh -u user -i 1234 -n add_cleanup "orbit/warpdrive.sh -u user -w" @@ -166,6 +166,8 @@ curl --url "https://$SINGULARITY_HOSTNAME/register" \ | tee test/register_success \ | grep "msg = welcome to the classroom" +REGISTER_PASS="$(sed -nr 's/.*Password: ([^<]*).*/\1/p' < test/register_success | tr -d '\n')" + # Check that registration fails when student id is used for a second time curl --url "https://$SINGULARITY_HOSTNAME/register" \ --unix-socket ./socks/https.sock \ @@ -186,7 +188,7 @@ curl --url "https://$SINGULARITY_HOSTNAME/login" \ curl --url "https://$SINGULARITY_HOSTNAME/login" \ --unix-socket ./socks/https.sock \ "${CURL_OPTS[@]}" \ - --data "username=user&password=pass" \ + --data "username=user&password=${REGISTER_PASS}" \ | tee test/login_success \ | grep "msg = user authenticated by password" @@ -194,7 +196,7 @@ curl --url "https://$SINGULARITY_HOSTNAME/login" \ curl --url "pop3s://$SINGULARITY_HOSTNAME" \ --unix-socket ./socks/pop3s.sock \ "${CURL_OPTS[@]}" \ - --user user:pass \ + --user "user:${REGISTER_PASS}" \ | tee test/pop_get_empty \ | diff <(printf '\r\n') /dev/stdin @@ -207,7 +209,7 @@ curl --url "smtps://$SINGULARITY_HOSTNAME" \ --mail-from "user@$SINGULARITY_HOSTNAME" \ --mail-rcpt "other@$SINGULARITY_HOSTNAME" \ --upload-file - \ - --user 'user:pass' < Date: Sun, 5 May 2024 21:37:50 -0400 Subject: [PATCH 7/7] orbit: db: remove registration table Now that passwords can be generated for students with a null password hash, we can remove the old way of dealing with pending account data. We can remove the code that looks for entries in it during registration, and remove the code that adds credentials to it during account creation. Those are the only references to the table, so we can remove the class for it from the db file, and update the binary blob for the empty db to reflect the new schema. --- orbit/db.py | 6 ---- orbit/hyperspace.py | 4 --- orbit/radius.py | 7 ---- test.sh | 78 ++++++++++++++++++++------------------------- 4 files changed, 34 insertions(+), 61 deletions(-) diff --git a/orbit/db.py b/orbit/db.py index 26a64ab9..b8c458d2 100755 --- a/orbit/db.py +++ b/orbit/db.py @@ -23,11 +23,5 @@ class Session(BaseModel): expiry = peewee.FloatField() -class Registration(BaseModel): - student_id = peewee.TextField(unique=True) - username = peewee.TextField(unique=True) - password = peewee.TextField() - - if __name__ == '__main__': DB.create_tables(BaseModel.__subclasses__()) diff --git a/orbit/hyperspace.py b/orbit/hyperspace.py index 42c916fc..05d897b5 100755 --- a/orbit/hyperspace.py +++ b/orbit/hyperspace.py @@ -81,10 +81,6 @@ def do_newuser(args): try: db.User.create(username=args.username, pwdhash=new_hash, student_id=args.studentid) - if args.studentid and new_hash: - db.Registration.create(username=args.username, - password=args.password, - student_id=args.studentid) except db.peewee.IntegrityError as e: errx(f'cannot create user with duplicate field: "{e}"') diff --git a/orbit/radius.py b/orbit/radius.py index b876746c..88adf403 100644 --- a/orbit/radius.py +++ b/orbit/radius.py @@ -385,13 +385,6 @@ def handle_dashboard(rocket): def find_creds_for_registration(student_id): - query = (db.Registration - .delete() - .where(db.Registration.student_id == student_id) - .returning(db.Registration)) - if registration := next(iter(query.execute()), None): - return (registration.username, registration.password) - password = secrets.token_urlsafe(nbytes=config.num_bytes_entropy_for_pw) salt = bcrypt.gensalt() pwdhash = bcrypt.hashpw(password.encode(), salt).decode() diff --git a/test.sh b/test.sh index 56f5919a..45fe52d5 100755 --- a/test.sh +++ b/test.sh @@ -80,50 +80,40 @@ ${DOCKER} volume export singularity_orbit-db > test/orbit_orig.tar # Import an empty orbit db with no users or sessions xxd -r <<- 'EOF' | gunzip | ${DOCKER} volume import singularity_orbit-db - -00000000: 1f8b 0800 0000 0000 0003 edda 5d4f d350 ............]O.P -00000010: 18c0 f196 b76e c826 5e90 2672 735c 4274 .....n.&^.&rs\Bt -00000020: 51c8 5002 31dc 30b0 d1c5 3964 9404 6e5c Q.P.1.0...9d..n\ -00000030: 4a56 a511 366c bb80 37c6 71e3 f7f0 a378 JV..6l..7.q....x -00000040: ef77 f2b4 dd4b 5751 8909 4a9a ff2f d9da .w...KWQ..J../.. -00000050: 737a fa9c 3ee7 744b 4eda b67b e8f8 4bcd sz..>.tKN..{..K. -00000060: 43e5 1a95 a4d5 9595 70bb 9cd8 9696 1f07 C.......p....... -00000070: dfd1 7e44 eeaf ad94 5615 51ba ce8b eaeb ..~D....V.Q..... -00000080: 78be e5ca 2eff 455f 37d0 ee4e d5f1 6df1 x.....E_7..N..m. -00000090: b6ed 9e58 be78 a2cc 2aaa aa6c 0821 0f65 ...X.x..*..l.!.e -000000a0: e467 3ad6 3428 4fc4 caea 15c2 6794 25f7 .g:.4(O.....g.%. -000000b0: 6b2e bfa1 646f 7d57 6e6f e6bf e55f e43e k...do}Wno..._.> -000000c0: e7ef e4ee ce7c 9959 9455 0000 0000 fed6 .....|.Y.U...... -000000d0: 9bac a63f 9c53 bb59 a7d5 b4cf 3b9e ed36 ...?.S.Y....;..6 -000000e0: 3cbf d3b4 5b7e c369 06c5 e9ad ba51 360d <...[~.i.....Q6. -000000f0: b157 abec ec19 a252 7b66 ec8b 42a2 6141 .W.....R{f..B.aA -00000100: 6cd7 a2ca 8278 5088 d517 f733 9a7e 5fc6 l....xP....3.~_. -00000110: 5787 f183 af96 7562 07db ecaf a3f7 9b8d W.....ub........ -00000120: c61e d416 bbb3 9aa6 cfcd a917 e3be 7578 ..............ux -00000130: 1c46 0b3e 995e 44b3 bc59 3586 e705 d758 .F.>.^D..Y5....X -00000140: a999 c673 a32e 6adb a6a8 ed55 abe2 75bd ...s..j....U..u. -00000150: f2aa 5c3f 102f 8d83 4762 185a 98c6 be39 ..\?./..Gb.Z...9 -00000160: 6825 8f9c 9e35 8f2c ef28 3a20 cbf1 c483 h%...5.,.(: .... -00000170: aaa2 d835 eb95 2db3 39a5 e98b f36a 3717 ...5..-.9....j7. -00000180: 26eb d99e e7b4 5b83 447a 65ed d294 938d &.....[.Dze..... -00000190: a3ac 7bb5 89c4 f313 9a3e 2fbb f1c2 c47b ..{......>/....{ -000001a0: 6d7a 9bc9 d1f4 6301 fcf6 7bbb 95c8 edca mz....c...{..... -000001b0: 2360 9f9f 3aee c782 90c1 ab83 fa7e da8b #`..:........~.. -000001c0: 9353 7a79 5e55 a2ac 3f1c cbd5 61c3 eaf8 .Szy^U..?...a... -000001d0: edb0 dce8 67b6 dcdb 9952 a275 a172 3eae ....g....R.u.r>. -000001e0: e96b 0b6a f75e d8ce b5df 399e ef5a 7e7c .k.j.^....9..Z~| -000001f0: 14e2 9513 978e dba5 a745 8317 3f34 3a82 .........E..?4:. -00000200: 9fc6 347d 5df6 5cfc b9e7 e1d4 c6ab c7ff ..4}].\......... -00000210: dc77 f2c7 90ec 3dfe a3e8 aeab 9abe b0a0 .w....=......... -00000220: 5e3c 0d67 30de 34be 3f36 3a97 c988 57b8 ^<.g0.4.?6:...W. -00000230: a593 376a 7c4a 7f73 bb5b 9e77 d676 93e7 ..7j|J.s.[.w.v.. -00000240: f4a7 3b17 4cde ec7f fdd7 0200 0000 0000 ..;.L........... -00000250: d72c 7cc0 cffa 1f00 0000 0080 5463 fd0f .,|.........Tc.. -00000260: 0000 0000 40fa f1fe 3f00 0000 0000 e9c7 ....@...?....... -00000270: f37f 0000 0000 00d2 8ff5 3f00 0000 0000 ..........?..... -00000280: e9c7 fbff 0000 0000 00a4 1fcf ff01 0000 ................ -00000290: 0000 483f d6ff 0000 0000 0000 0000 0000 ..H?............ -000002a0: 0000 0000 0000 0000 0070 33fd 007a eebd .........p3..z.. -000002b0: 9700 c800 00 ..... +00000000: 1f8b 0800 0000 0000 0003 edda 4d8f d240 ............M..@ +00000010: 18c0 f176 79a9 9af0 7221 3d70 9970 51a2 ...vy...r!=p.pQ. +00000020: bb01 25ae 37c5 b531 44ec ba6c 49d8 8ba4 ..%.7..1D..lI... +00000030: a4dd 6ce3 2eac 6d89 eb11 13bf 88df c6ab ..l...m......... +00000040: dfc1 efe0 d119 282f 4b34 d90b 6af0 ff4b ......(/K4..j..K +00000050: 3a33 cf74 78a6 0fe9 6502 a370 10c4 7bde :3.tx...e..p..{. +00000060: 40db a09a f4b8 d198 f6f5 b5be 56db 574d @...........V.WM +00000070: 32ae 25e3 fd46 fda1 266a 9b7c a8b9 7114 2.%..F..&j.|..q. +00000080: bba1 dcf2 4fec f50f 3a3e 6a07 b12f 4e47 ....O...:>j../NG +00000090: e185 1b8b 475a 51d3 75ed 9910 f256 465e ....GZQ.u....VF^ +000000a0: c6ca 5215 a757 62fd 06e9 33da 5ef8 2557 ..R..Wb...3.^.%W +000000b0: f8a1 6573 9fb5 c293 c2d7 fcb7 fcd3 dc77 ..es...........w +000000c0: 1900 0000 00c0 ffe8 6dd6 30ef 97f4 c9ed ........m.0..... +000000d0: 60e8 f957 e3c8 0ffb 513c f6fc 61dc 0f3c `..W....Q<..a..< +000000e0: 151a 071d abe9 58a2 6bb7 8eba 9668 d92f ......X.k....h./ +000000f0: ac9e a8ac 2dac 8843 7b36 5911 f72a 2bf3 ....-..C{6Y..*+. +00000100: d55e c630 efca fcfa 32bf 6a86 ee85 affa .^.0....2.j..... +00000110: ecef b3cf 975d cfbd 98ad 4e8a 69c3 2c95 .....]....N.i.,. +00000120: f44f a9d8 1d9c 4fb3 a92b 9364 749a cfdb .O....O..+.dt... +00000130: d6f2 73ea 195b b663 bdb4 3ac2 3e74 84dd ..s..[.c..:.>t.. +00000140: 6db7 c59b 4eeb 75b3 7322 5e59 270f c432 m...N.u.s"^Y'..2 +00000150: b570 ac9e b358 25ef 5c7e f0ce dce8 6c76 .p...X%.\~....lv +00000160: 43c6 ab85 aba9 aa38 763a ad03 c74b 19e6 C......8v:...K.. +00000170: 6e59 9fe4 a6c5 467e 1405 a3e1 a290 244e nY....F~......$N +00000180: ffb2 e4f5 c5b3 aa93 d9b5 c2f3 ba61 96e5 .............a.. +00000190: 36d1 b4f0 644d d2ed 5c2f 7f25 413c 7ae7 6...dM..\/.%A