From d0bc44dbe8cc2647f24d00a374bf70daf75acf23 Mon Sep 17 00:00:00 2001 From: Arve Gengelbach Date: Wed, 25 Jan 2023 15:43:08 +0100 Subject: [PATCH] match demonstrator to verified backend closes #50 #41 --- Makefile | 2 + webdemo/app.py | 279 ++++++++++++++++--------- webdemo/bytetree.py | 5 + webdemo/static/sample-signed-vote.json | 172 +-------------- webdemo/templates/poll.html | 133 +++++++++--- 5 files changed, 287 insertions(+), 304 deletions(-) diff --git a/Makefile b/Makefile index ec45c5c..7b4fa5f 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +.PHONY: default +default: demo .PHONY: run_auth run_mixnet run_webserver demo run_auth: diff --git a/webdemo/app.py b/webdemo/app.py index c83a68e..014b48e 100644 --- a/webdemo/app.py +++ b/webdemo/app.py @@ -1,3 +1,4 @@ +import uuid import base64 import io import json @@ -18,9 +19,11 @@ mimetypes.add_type("application/wasm", ".wasm") -# Items are tuple -# (signature reference/signature, vote, is freja online) -SIGNED_VOTES = [] +# list of encrypted votes +# set of user ids that have voted (also obtainable from SIGNATURES) +VOTED_IDS = set() +# dictionary assigning session id to signature reference +SIGN_REFS = dict() def get_auth_server_url(): parsed_url = urlparse(os.getenv('AUTH_SERVER_URL')) @@ -76,52 +79,24 @@ def decorated_function(*args, **kwargs): return decorated_function -def _check_for_signed_votes(): - it = len(SIGNED_VOTES) - 1 - votes_for_verified_backend = [] - while (it >= 0): - signed_vote = SIGNED_VOTES[it] - signature_reference, encrypted_vote, freja_online = signed_vote - if freja_online: - signature, has_signed = _confirm_if_user_has_signed(signature_reference) - if _has_user_already_voted(signature): - del SIGNED_VOTES[it] - return render_template("poll.html", data=POLL_DATA, stats=STATS, vote=None) - - if signature is not None and has_signed: - modified_response_object = { - 'vote': encrypted_vote, - 'signature': signature, - } - _append_vote_to_ciphertexts(encrypted_vote) - _record_signature(signature) - del SIGNED_VOTES[it] - print(modified_response_object) - votes_for_verified_backend.append(modified_response_object) - else: - # `signature_reference` is signature in case of offline votes - if _has_user_already_voted(signature_reference): - flash('You have already voted') - del SIGNED_VOTES[it] - return render_template("poll.html", data=POLL_DATA, stats=STATS, vote=None) - - modified_response_object = { - 'vote': encrypted_vote, - 'signature': signature_reference, - } - _append_vote_to_ciphertexts(encrypted_vote) - _record_signature(signature_reference) - del SIGNED_VOTES[it] - print(modified_response_object) - votes_for_verified_backend.append(modified_response_object) - - it -= 1 - if len(votes_for_verified_backend) == 0: - return render_template("poll.html", data=POLL_DATA, stats=STATS, vote=None) - - return render_template("poll.html", data=POLL_DATA, stats=STATS, show_success=True, vote=json.dumps(votes_for_verified_backend)) - - +def _check_for_signed_votes(request): + sign_ref = get_user_sign_ref(request) + if sign_ref is None: + # ensure cookie is set + response = make_response(render_template("poll.html", data=POLL_DATA, stats=STATS)) + userno = get_or_gen_userid(request) + set_userid(response,userno) + return response + signature = _confirm_if_user_has_signed(sign_ref) + if signature is None: + flash('Waiting for signing','signing_wait') + return render_template("poll.html", data=POLL_DATA, stats=STATS, signing_waiting=True) + del_user_sign_ref(request) + flash('The encrypted vote is signed and ready for submission','msg') + return render_template("poll.html", data=POLL_DATA, stats=STATS, show_success=True, signature=signature) + +# format: json array with two ByteTree elements, which each are represented as +# byte arrays (list of integers < 256) def _append_vote_to_ciphertexts(vote): with open(FILENAME, "a") as f: print(vote, file=f) @@ -132,23 +107,10 @@ def _record_signature(signature): with open(SIGNATURES, "a") as f: f.write(f"{signature}\n") - def _has_user_already_voted(candidate_signature): if candidate_signature is None: return False - - if not os.path.exists(SIGNATURES): - return False - - with open(SIGNATURES) as f: - current_signatures = f.readlines() - - for current_signature in current_signatures: - if _get_userInfo_from_signature(current_signature) \ - == _get_userInfo_from_signature(candidate_signature): - return True - return False - + return _get_userInfo_from_signature(candidate_signature) in VOTED_IDS def _get_userInfo_from_signature(signature): jws_payload = signature.split('.')[1] @@ -165,29 +127,103 @@ def _confirm_if_user_has_signed(sign_ref): ) if r.status_code == 200: - return (r.json()['signature'], True) - - return (None, None) + return r.json()['signature'] + + return None + + +# TODO drop exemption +@csrf.exempt +@app.route("/vote_submission", methods=["POST"]) +def vote_submission(): + # check if this is a vote submission + pre_vote = request.form.get("ballot") + vote = _validate_vote(pre_vote) + + if pre_vote and isinstance(vote, dict): + signature = vote["signature"] + # a list of byte arrays + enc_vote_ba_lst = vote["vote"] + # json encoding of above + enc_vote_str = json.dumps(list(map((lambda x: list(bytes(x))),enc_vote_ba_lst))) + # byte array of above + enc_vote_ba = ByteTree(list(map((lambda x: ByteTree.from_byte_array(x)),enc_vote_ba_lst))).to_byte_array() + if _validate_vote_auth(enc_vote_ba,signature): + user_id = _get_userInfo_from_signature(signature) + logging.error(user_id) + has_voted = _has_user_already_voted(signature) + if not user_id: + flash('Invalid signature') + return redirect(url_for('root')) + if has_voted: + logging.error(f"User {user_id} attempted to revote") + flash('You have already voted') + return redirect(url_for('root')) + # add to store of votes + _append_vote_to_ciphertexts(enc_vote_str) + _record_signature(signature) + VOTED_IDS.add(user_id) + logging.error(f"{user_id} just voted successfully" ) + # confirm submission + flash('Successful submission of encrypted vote.','success') + return redirect(url_for('root')) + logging.error(f"Error verifying encrypted vote and signature. Could not submit your vote.") + flash('Error verifying encrypted vote and signature. Could not submit your vote.','error') + return redirect(url_for('root')) + +def get_userid(request): + if 'userno' in request.cookies: + return request.cookies.get('userno') + else: + return None + +def get_or_gen_userid(request): + if 'userno' in request.cookies: + return request.cookies.get('userno') + return str(uuid.uuid1()) + +def set_userid(response,userno): + response.set_cookie('userno', userno) + +def get_user_sign_ref(request): + userno = get_userid(request) + if not (userno is None) and userno in SIGN_REFS: + return SIGN_REFS[userno] + return None + +def set_user_sign_ref(request,sign_ref, userno): + SIGN_REFS[userno] = sign_ref + +def del_user_sign_ref(request): + userno = get_userid(request) + del SIGN_REFS[userno] +# either checking for signed votes, +# or accepting hash signing requests or vote submissions @app.route("/", methods=("GET", "POST")) def root(): if POLL_DATA["publicKey"] is None: return "Missing public key!" if request.method == "GET": - return _check_for_signed_votes() + # flash('You have already voted','error') + return _check_for_signed_votes(request) - vote = request.form.get("field") + # assume this is a signing request + vote_hash = request.form.get("field") user_email = request.form.get('email-for-signing') - error = _validate_vote(vote) - if error: - return error - encrypted_vote = str(vote).encode('utf-8') - hashed_encryption = sha256() - hashed_encryption.update(encrypted_vote) - hex_string = hashed_encryption.digest().hex() - beautified_hex_string = ' '.join([hex_string[i:i+4] for i in range(0, len(hex_string), 4)]) + error = _validate_hash256(vote_hash) + if error: + return error + + # ensure cookie later + userno = get_or_gen_userid(request) + # encrypted_vote = str(vote).encode('utf-8') + # hashed_encryption = sha256() + # hashed_encryption.update(encrypted_vote) + # hex_string = hashed_encryption.digest().hex() + beautified_hex_string = ' '.join([vote_hash[i:i+4] for i in range(0, len(vote_hash), 4)]) logging.error(f"Hex-string: {beautified_hex_string}") sign_request = requests.post( @@ -202,29 +238,81 @@ def root(): if sign_request.status_code == 200: response_object = sign_request.json() signature_reference = response_object['signRef'] - SIGNED_VOTES.append((signature_reference, eval(vote), True)) - - return render_template("poll.html", data=POLL_DATA, stats=STATS, show_success=True, hash=beautified_hex_string) - + # update outstanding signing request + set_user_sign_ref(request, signature_reference, userno) + response = make_response(render_template("poll.html", data=POLL_DATA, stats=STATS, show_success=True, hash=beautified_hex_string)) + set_userid(response,userno) + return response + if sign_request.status_code == 418: - flash(sign_request.json()['message']) + flash(sign_request.json()['message'],'msg') return redirect(url_for('root')) - flash('Could not cast your vote.') + flash('Could not cast your vote.','error') return redirect(url_for('root')) + +def base64urldec(string): + padlen = 4 - len(string) % 4 + return base64.urlsafe_b64decode(string + '=' * padlen) + + +# get hash value from signature and validate +def _validate_vote_auth(enc_vote_ba,signature): + hashed_encryption = sha256() + hashed_encryption.update(enc_vote_ba) + hash_dgst = hashed_encryption.digest() + +# with open('sample-signed-vote.json') as f: +# sample_signed_vote = json.loads(f.read()) + + parts = signature.split('.') + if len(parts) != 3: + return "malformed signature: expect three components in signature" + jws_payload = parts[1] + try: + jws_payload_decoded = base64urldec(jws_payload) + payload_json = json.loads(jws_payload_decoded)["signatureData"]["userSignature"] + signed = payload_json.split('.') + hash_val = ''.join(base64urldec(signed[1]).decode('ascii').split(' ')) + return hash_dgst.hex() == hash_val + except Exception as e: + return None + + +def _validate_hash256(vote_hash_str): + len_hash = len(vote_hash_str) + if len_hash != 64: + return f"Expected hash of length 64, got {len_hash}: {vote_hash_str}" + try: + x = int(vote_hash_str, 16) + except ValueError: + return f"Expected vote hash, got {vote_hash_str}" + return None + + +# returns dict with byte tree and signature string def _validate_vote(vote): try: x = json.loads(vote) + enc_vote = ByteTree.from_byte_array(bytes.fromhex(x["vote"])) + nodes = enc_vote.dest_node() + if len(nodes) != 2: + return f"Vote format error of encrypted vote" + enc_vote = list(map((lambda x: x.to_byte_array()), nodes)) except json.JSONDecodeError: - return "JSON Decode Error" + return "Vote format error (cannot decode JSON)" + except KeyError: + return "Vote format error (missing key: vote)" - len_x = len(x) - if len_x != 2: - return f"Expected 2 elements, got {len_x}" + newdict = {k: v for k, v in x.items() if k == "signature" or k == "vote"} + if len(newdict) != 2: + return f"Vote format error (missing key: signature)" + # TODO authenticate - return None + newdict["vote"] = enc_vote + return newdict def _delete_file(file): @@ -290,23 +378,6 @@ def _is_authenticated(user_identification): return False -@app.route("/offline_vote") -def offline_vote(): - """ - Endpoint for casting a vote when FrejaEID is offline. - """ - - with open(os.path.join(app.static_folder, 'sample-signed-vote.json')) as f: - sample_signed_vote = json.loads(f.read()) - - encrypted_vote = sample_signed_vote['vote'] - signature = sample_signed_vote['signature'] - - SIGNED_VOTES.append((signature, encrypted_vote, False)) - - return redirect(url_for('root')) - - @csrf.exempt @app.route("/publicKey", methods=("GET", "POST")) def publickey(): diff --git a/webdemo/bytetree.py b/webdemo/bytetree.py index 9af075b..66b006f 100755 --- a/webdemo/bytetree.py +++ b/webdemo/bytetree.py @@ -35,6 +35,11 @@ def is_node(self) -> bool: def is_leaf(self) -> bool: return self.type == ByteTree.LEAF + def dest_node(self) -> ["ByteTree"]: + if not self.is_node: + return [] + return self.value + @classmethod def from_byte_array(cls, source: ByteString) -> "ByteTree": """ diff --git a/webdemo/static/sample-signed-vote.json b/webdemo/static/sample-signed-vote.json index 17a3aee..35c294e 100644 --- a/webdemo/static/sample-signed-vote.json +++ b/webdemo/static/sample-signed-vote.json @@ -1,171 +1,5 @@ { - "vote": [ - [ - 0, - 0, - 0, - 0, - 2, - 1, - 0, - 0, - 0, - 33, - 0, - 138, - 116, - 23, - 249, - 181, - 124, - 124, - 111, - 197, - 146, - 28, - 190, - 209, - 225, - 151, - 118, - 153, - 5, - 63, - 28, - 26, - 77, - 19, - 73, - 51, - 195, - 10, - 20, - 210, - 110, - 128, - 43, - 1, - 0, - 0, - 0, - 33, - 0, - 27, - 188, - 20, - 120, - 142, - 252, - 170, - 155, - 199, - 202, - 242, - 210, - 22, - 74, - 23, - 177, - 70, - 194, - 29, - 93, - 103, - 68, - 197, - 110, - 152, - 74, - 160, - 97, - 63, - 220, - 4, - 167 - ], - [ - 0, - 0, - 0, - 0, - 2, - 1, - 0, - 0, - 0, - 33, - 0, - 244, - 77, - 44, - 202, - 27, - 219, - 17, - 36, - 12, - 12, - 152, - 221, - 130, - 236, - 174, - 188, - 164, - 196, - 64, - 58, - 127, - 201, - 164, - 90, - 104, - 204, - 102, - 111, - 80, - 243, - 237, - 99, - 1, - 0, - 0, - 0, - 33, - 0, - 72, - 107, - 45, - 190, - 158, - 4, - 103, - 196, - 83, - 230, - 50, - 232, - 86, - 115, - 87, - 222, - 81, - 22, - 150, - 19, - 94, - 54, - 206, - 76, - 243, - 189, - 111, - 216, - 216, - 18, - 70, - 211 - ] - ], - "signature": "eyJ4NXQiOiIyTFFJcklOT3p3V0FWRGhvWXlicVVjWFhtVnMiLCJhbGciOiJSUzI1NiJ9.eyJzaWduUmVmIjoidHRNN3RyeDVHak1aNDlzYk8xNVRCQVhfdzhzUDczWHpaX1hFMGwyLU9xWFdodVpFbDFucnpOUFl4aVhfQzExUiIsInN0YXR1cyI6IkFQUFJPVkVEIiwidGltZXN0YW1wIjoxNjczOTU5MjAwMzg5LCJtaW5SZWdpc3RyYXRpb25MZXZlbCI6IkJBU0lDIiwic2lnbmF0dXJlVHlwZSI6IlNJTVBMRSIsInNpZ25hdHVyZURhdGEiOnsidXNlclNpZ25hdHVyZSI6ImV5SnJhV1FpT2lJNE1UY3lNREUyTTBSRU5rWXhSRU00TkRVeFJUQkZOek13TjBNek9VUXlOVFJGTWtaQk5Ua3dNVVZFT1RKRE1FUXdSRVkzTnpZM1JURTVSVEpFTnpnMklpd2lZV3huSWpvaVVsTXlOVFlpZlEuTVRaaFl5QXlZV1kySUdSaU5EUWdaalJrTUNCaFlqWXpJR0ZtTm1JZ1kySmtaaUE0WW1Zd0lEQmhaV1VnWTJFMVpDQXdZall6SUdJME1HWWdORE15TlNCbFlXTTNJR1JtT1dZZ05HUTNZZy5PamtiQVZBb3piMDQyU3MzUU9PSGpBbmlDQm5LQlhyeENZN1MxYzhrVGhReUNYR0xJelBBOVdrcDNRSV9ZMHZmZkNZRGkyZDNuSC01TlV5RDM2Q3drNUtDMDVvTUdDdnRtX3BuV0FFbE9GOGIwS0M5aEVXeXQ4c09JTHZ6OW4wYWVROEg1ZnRTQ1R4aUsxS0p6dDlBcDJ6YXhBN0NZMkVMcmY3czRKS0c4YXA3LWQ1aDN2RllmemFMSmJKdENiY2MxNy1kRWt0bVlQYlRTZlJRcHZqNjlaZWlkVHpwYnpTQjBOdExmS0RtOFY4NVVLN3RxcXEzcHk2MU5XLXk3ZTdsOFM2OXZwbUZDN285VHk1NFZNN3JBeFlKSTNPV0o2Z2tGWUVHeU4tRDNjY3YzeWQ5SllOQVJhTXNFZzdfd0JFSHo2OUxjUGFmN3RCbElOV2k4TFM3MGciLCJjZXJ0aWZpY2F0ZVN0YXR1cyI6Ik1JSUdYUW9CQUtDQ0JsWXdnZ1pTQmdrckJnRUZCUWN3QVFFRWdnWkRNSUlHUHpDQ0FRS2lGZ1FVMWlsK0djVkhVTDZHQUhiYklkb25BNHY4bG13WUR6SXdNak13TVRFM01USTBNREF3V2pDQnJ6Q0JyRENCbGpBTkJnbGdoa2dCWlFNRUFnRUZBQVFnTFhzcklFa0JYYVZRbnZGby9HSXJMaFZOYVFaakhlN3lsR2NTWkV4UkU0OEVJQ2RCTG56cGc2V1U4V21SbGZHdURkTTFWcjFhWTZnM0tZZjl3SmFDT0gyeUFrRUFnbVlRZnBSMXErQWhqc3gzNEVDRzdoczVxMmhxYzVSNDJEd3QxU0p1OUFXdlEzbXBwZDlMeTR2bGNPM2xKeUt3Q09jV1M2dFM5Y3lVUDBmVE1kbThkNEFBR0E4eU1ESXpNREV4TnpFeU5EQXdNRnFoSlRBak1DRUdDU3NHQVFVRkJ6QUJBZ1FVQkJJd0xqUXpPVFF6TVRBMU1UQTBNRFV4TkRVd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFLaXBFVm1mNnZvMFpteUxiVGxvNHlHQzA0Y2kyMlJSVFE5bnQ1Wm8wWUR5SnBKWlpIWU1FS2Z6Y3hKWHo5cFk5UHhtNCtXMEtnZ3ZVZnNoZ3duV0NLK2dTMmlWeHY3TUxES3plcWJxMDN6SVJYRGVqbjdwM3ZsYzM4T0h0bFlxNGtrZERKYkM1QTQ0bGxqZ2Y1eEEyWmc1SWh0WmNPRFpUNDA4Q211cnlZMm9VQ1FHWS9KQ2dDNmNIVjRKQTI5Z0hFMWFGNHVKVnB4Nm5JOStyblo2cUxCN1p6WTA0aVZCVm5VNkJNZXlMRTJRL1VZa1ZIOSs5U05tbUhpOUV5endINGtTNHo4TGNuV0dwczFlVEdyVEIzRXl4MENrOGxaQWtQNXhTVExBSTUxODhWS0dBeGdBVHAxVDlFS0oxNkc1SWROSmpiUzk1akY2NjgzYXppU0hDRGVnZ2dRaE1JSUVIVENDQkJrd2dnTUJvQU1DQVFJQ0ZFbWViYjBzaGNwZXhXZ1J2R1RFWFd6MkZUbHBNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1JR0RNUXN3Q1FZRFZRUUdFd0pUUlRFU01CQUdBMVVFQnhNSlUzUnZZMnRvYjJ4dE1SUXdFZ1lEVlFSaEV3czFOVGt4TVRBdE5EZ3dOakVkTUJzR0ExVUVDaE1VVm1WeWFYTmxZeUJHY21WcVlTQmxTVVFnUVVJeERUQUxCZ05WQkFzVEJGUmxjM1F4SERBYUJnTlZCQU1URTFKVFFTQlVSVk5VSUVsemMzVnBibWNnUTBFd0hoY05NakF4TWpBNE1UTXhOVEV5V2hjTk1qTXhNakE0TVRNeE5URXlXakI4TVFzd0NRWURWUVFHRXdKVFJURVNNQkFHQTFVRUJ4TUpVM1J2WTJ0b2IyeHRNUlF3RWdZRFZRUmhFd3MxTlRreE1UQXRORGd3TmpFZE1Cc0dBMVVFQ2hNVVZtVnlhWE5sWXlCR2NtVnFZU0JsU1VRZ1FVSXhEVEFMQmdOVkJBc1RCRlJsYzNReEZUQVRCZ05WQkFNVERFOURVMUFnVTJsbmJtbHVaekNDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFQVk1XOTdSQ0hOQWFLTU5jdnJyYW5GZmtSejlHbnVaeEhEc0F1RHlpWFNEVERTV0tnazZTQS9QREJJTi9IQ0xkeWtVYVJseFFZR1NXMys1Q1NlRDNVczVmVTNQd0J4eXBLM1pjTXZINktBZ0xIakFXSDhjY0x2YnZuUXROV0VQZUNNT0tSK2YxbUdzSEdaUitna1NHa29kN0ZUeTJxbzJUZ2UxZUdZMGwwamtUS3F0elpzemRaa3NXci9WUE9Lc3VvYTMvaXNRdnAxZTN6THRveU9XSE80bEo1aXZtMm84Qk5xclJFSmVVNFlUb2FaR2hNVVNZMUlKejU0Qi9ka3dJWTQ2VTRoeVVGMmZyRkx3N012Q01IR0tnL0NYcE1DQm5KVXZBYXMwRWEyTEhMRWg0MklIdWVwVG5vczhhWkJhN09zWFpvVFF1c0NUZkZsd0p0U2dnMFVDQXdFQUFhT0JpakNCaHpBT0JnTlZIUThCQWY4RUJBTUNCc0F3REFZRFZSMFRBUUgvQkFJd0FEQWZCZ05WSFNNRUdEQVdnQlJxZklvUG5YQU9ITnBmTGFBOEpsK0k2QlcvbkRBU0JnTlZIU0FFQ3pBSk1BY0dCU29EQkFVS01CMEdBMVVkRGdRV0JCU3kxMGVMcDF5c0g3WjB0V1JsWUNzbTVQSmYwakFUQmdOVkhTVUVEREFLQmdnckJnRUZCUWNEQ1RBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWt2bU1Ta0REeEJZSjJIdVJPMGQwMko2dE1HdlhDNEg5ZkJBSVBra2p4Z0VKR2VEdmpDWkxBZllndkdleksvRWl4SUZud0ozWnRDV2ZoeTN1dE1sY3NzaDBDVW9HV3UzUUdiYlc5NHNOOUxxbEZ5N1FremJsYXYwdnVTMG5pUzVFeEcwd2VsSW91bXM4THZ6ODNzbEpvRlRaYyt4dE8xSFU3b0UwcGJNSHJLZUg4cGxKNDVUSVlWOFhMbFhqNmFxdzRtTHlQUDBMNnFLMnZVY3pEeXZIUFBGcmM3QVB2YzRiWnI4SElwR2EvcUYzbVlYNTdwVnBuMGhaUmJmRWs4cFJTN0Ywd2FqWmJ5ZHlFazE0Rm5UNjdMQ2Fwa2h4cmpNZjViNmVrQmRPK1VQU05BVGdBTXM4Sno1RDAxaTB0S1NQdTh0c05TWEVJQ2cvV3BSbFcrakNFQT09In0sInVzZXJJbmZvVHlwZSI6IkVNQUlMIiwidXNlckluZm8iOiJraXNhbWE4MDIyQGdpZnRjdi5jb20ifQ.Mb_xw7aX9pKn8omBWdTGEimwLnTjukYxp2hRHBf3RJjUrN9eacFd5U-qSkkZDIlwV9taJgNI5UAOkiqQX0DrKXDzYUUIx-fBMij2PyuG4tr39W10UaGt7-0jxPyUlbJV0GU-BrS8C7XPO_9Do__MOfA_mgsjKM2jpoACyLsyMDs3kt3YXaGTUixpMUPzgjqa6RN6a2v4yUbGdrc7IktZQV4llsa9mY8QCJgwfodsXLRDxIF-L-Pk5I89QANGebYkp5dosngOVpnOWyyVU0dLiO0ZLtCqiTVRYkhhui0X7-jR3tFWQQ6ZdwNC-IzmLnQKSRlXHJ0UDDU_oJfhkyshbQ" + "vote": "000000000200000000020100000021004582f66416a80946526186470c12784a34a9eae8b12df74d7b60f89c2087b7c501000000210047d031d945b4609017a83d569b661cc651a863c88631b2f3038a24a1da61240800000000020100000021000ad727ceb7dc79469a6c43f4ecc9ba85b712acde58cd06b7df568717ef2bb0ff010000002100849391d58bd9707011776836eb2e63819aed978b0cb54e1e3ca76889ad2a09ed", + "signature": "eyJ4NXQiOiJEaVpiekJmeXNVbTYtSXdJLUd0aWVuRXNiamMiLCJhbGciOiJSUzI1NiJ9.eyJzaWduUmVmIjoibGxpYzd0X1lBRFc2ZWdfZXBxYnU4VDA3OFpWWU41amZWX19vOEgxakNYbElCRHMyaXR5c3hMaUpuMFNKY01pWSIsInN0YXR1cyI6IkFQUFJPVkVEIiwidGltZXN0YW1wIjoxNjk1OTAwODk4NzQzLCJtaW5SZWdpc3RyYXRpb25MZXZlbCI6IkJBU0lDIiwic2lnbmF0dXJlVHlwZSI6IlNJTVBMRSIsInNpZ25hdHVyZURhdGEiOnsidXNlclNpZ25hdHVyZSI6ImV5SnJhV1FpT2lKQ1JEQkJOa1pFUmtReU9VVXhPRUkyTVRCQlJFWXpOakkyTVRsRk5qQTFOVVE1TlVGRlFVSTNSRFkwT1RFME5VUkVPVGs1UmpCRlFUUTNPVGswUVRkR0lpd2lZV3huSWpvaVVsTXlOVFlpZlEuT0RSaVl5QmxObVEwSURBNU5qY2dNelF3TmlCallUZGxJRFJrWkRFZ016Y3dZeUE0WXpJMklHVXdaV1VnTXpZeE5TQTJPVGc0SURSaE9HSWdNbVUzTUNBMll6QmhJREl4TkdVZ1pEWXhOdy5teTRkaWVVaEV6bGNzbkplYmx5RWVDZUdXVUVGUzJMUlBSTlpid1c3N1I1eEU2NmtNWm8xWXdHZldTeGlzSlNWZFhneU84aU5RQVh1dEhYX1F4ai01eHFhY3J6NU5Bd05UWm5wcHB4emJweTFrNHlNR0RnZFdjNW1DM3R5elhMOWVqR1hUck5KTW5XeXczc3JFeGhaT1JrMm1HWXdFeDlmcXo4T2NNbkRQZGVyOERDTk9UNXU0R08zUVVMOEtUTjFVTTRVWVhud1hBR2lsQ0NFUjRjVHhVWmxCSUJDd24xWGhzSHFJcHhxazBvZzU5TU0yYUxtdGdZdUR2djhESUQ5ZUpveWpLQjBMVjJWMzRXWUZtX0VKN21kSDRPbEJRbTgtMm1QN3NaYmswNnZ4bGNaTUhXNU1EWk1MRDNNWmw5VVpkMmRoVk1DY2pOOFYwMDBtTExHTmciLCJjZXJ0aWZpY2F0ZVN0YXR1cyI6Ik1JSUdYZ29CQUtDQ0JsY3dnZ1pUQmdrckJnRUZCUWN3QVFFRWdnWkVNSUlHUURDQ0FRT2lGZ1FVMWlsK0djVkhVTDZHQUhiYklkb25BNHY4bG13WUR6SXdNak13T1RJNE1URXpORFU0V2pDQnJ6Q0JyRENCbGpBTkJnbGdoa2dCWlFNRUFnRUZBQVFnTFhzcklFa0JYYVZRbnZGby9HSXJMaFZOYVFaakhlN3lsR2NTWkV4UkU0OEVJQ2RCTG56cGc2V1U4V21SbGZHdURkTTFWcjFhWTZnM0tZZjl3SmFDT0gyeUFrRUF0NFY0WmFwa3RmSTllTGV0dGMwakdwNmNCeEdLdDJxRjhOZ000REJUcE5ZOEE2ZWZXbGlYeVhaYWdrdHlBeVhQVXhsaWlqSTBCQnRjWjRoak0ydFBRb0FBR0E4eU1ESXpNRGt5T0RFeE16UTFPRnFoSmpBa01DSUdDU3NHQVFVRkJ6QUJBZ1FWQkJNd0xqUTBOakk1TlRFeU1EVTVOall6TlRNME1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRRHBrTG1wOGdqL2dqeDFKWDgvOGhEU2FsaUIzWllMNXFyYmR0Rk9IWXF5UTk1MWdqWjRTWmRDcjI5QTNHTjVVWW5rSVdXaVNsL1BNQlQ3K2g5VEFLNzdGem5PNDNTZXFiR3gyakV0VGcxMmVDT3BJSTJKMHhJV2I0QVY2dU5aVE14eG5XVEwvOXNRcTdmZ243Z0xZZmYySmVEZG03dUhOVDE2T09FZFkvS1J6TjRaS1d1ZkNZNmVtWGZJSHpIWWk2eTNINGRkZjBERXZWQkVibGJsODNQcmdJU1RYc28zTGc5cWdYVmlmMmVpZ3JSd0pDN3VTeWdSQUFkVll4eE8xaUw3TGkyZ3RXaFl4TDhKYk5OQTA2SUZUOUtQaVorVFJRTGt5V25heXBUS2NKbWhtKzNIaHZ1L2pNZ1FoQUM1aEZYZXlKaDVjRkpJQzltaCtJVWVMUDh0b0lJRUlUQ0NCQjB3Z2dRWk1JSURBYUFEQWdFQ0FoUkpubTI5TElYS1hzVm9FYnhreEYxczloVTVhVEFOQmdrcWhraUc5dzBCQVFzRkFEQ0JnekVMTUFrR0ExVUVCaE1DVTBVeEVqQVFCZ05WQkFjVENWTjBiMk5yYUc5c2JURVVNQklHQTFVRVlSTUxOVFU1TVRFd0xUUTRNRFl4SFRBYkJnTlZCQW9URkZabGNtbHpaV01nUm5KbGFtRWdaVWxFSUVGQ01RMHdDd1lEVlFRTEV3UlVaWE4wTVJ3d0dnWURWUVFERXhOU1UwRWdWRVZUVkNCSmMzTjFhVzVuSUVOQk1CNFhEVEl3TVRJd09ERXpNVFV4TWxvWERUSXpNVEl3T0RFek1UVXhNbG93ZkRFTE1Ba0dBMVVFQmhNQ1UwVXhFakFRQmdOVkJBY1RDVk4wYjJOcmFHOXNiVEVVTUJJR0ExVUVZUk1MTlRVNU1URXdMVFE0TURZeEhUQWJCZ05WQkFvVEZGWmxjbWx6WldNZ1JuSmxhbUVnWlVsRUlFRkNNUTB3Q3dZRFZRUUxFd1JVWlhOME1SVXdFd1lEVlFRREV3eFBRMU5RSUZOcFoyNXBibWN3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDFURnZlMFFoelFHaWpEWEw2NjJweFg1RWMvUnA3bWNSdzdBTGc4b2wwZzB3MGxpb0pPa2dQend3U0RmeHdpM2NwRkdrWmNVR0JrbHQvdVFrbmc5MUxPWDFOejhBY2NxU3QyWERMeCtpZ0lDeDR3RmgvSEhDNzI3NTBMVFZoRDNnakRpa2ZuOVpockJ4bVVmb0pFaHBLSGV4VTh0cXFOazRIdFhobU5KZEk1RXlxcmMyYk0zV1pMRnEvMVR6aXJMcUd0LzRyRUw2ZFh0OHk3YU1qbGh6dUpTZVlyNXRxUEFUYXEwUkNYbE9HRTZHbVJvVEZFbU5TQ2MrZUFmM1pNQ0dPT2xPSWNsQmRuNnhTOE96THdqQnhpb1B3bDZUQWdaeVZMd0dyTkJHdGl4eXhJZU5pQjducVU1NkxQR21RV3V6ckYyYUUwTHJBazN4WmNDYlVvSU5GQWdNQkFBR2pnWW93Z1ljd0RnWURWUjBQQVFIL0JBUURBZ2JBTUF3R0ExVWRFd0VCL3dRQ01BQXdId1lEVlIwakJCZ3dGb0FVYW55S0Q1MXdEaHphWHkyZ1BDWmZpT2dWdjV3d0VnWURWUjBnQkFzd0NUQUhCZ1VxQXdRRkNqQWRCZ05WSFE0RUZnUVVzdGRIaTZkY3JCKzJkTFZrWldBckp1VHlYOUl3RXdZRFZSMGxCQXd3Q2dZSUt3WUJCUVVIQXdrd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKTDVqRXBBdzhRV0NkaDdrVHRIZE5pZXJUQnIxd3VCL1h3UUNENUpJOFlCQ1JuZzc0d21Td0gySUx4bnN5dnhJc1NCWjhDZDJiUWxuNGN0N3JUSlhMTElkQWxLQmxydDBCbTIxdmVMRGZTNnBSY3UwSk0yNVdyOUw3a3RKNGt1Uk1SdE1IcFNLTHByUEM3OC9ON0pTYUJVMlhQc2JUdFIxTzZCTktXekI2eW5oL0taU2VPVXlHRmZGeTVWNCttcXNPSmk4ano5QytxaXRyMUhNdzhyeHp6eGEzT3dENzNPRzJhL0J5S1JtdjZoZDVtRitlNlZhWjlJV1VXM3hKUEtVVXV4ZE1HbzJXOG5jaEpOZUJaMCt1eXdtcVpJY2E0ekgrVytucEFYVHZsRDBqUUU0QURMUENjK1E5Tll0TFNrajd2TGJEVWx4Q0FvUDFxVVpWdm93aEE9In0sInVzZXJJbmZvVHlwZSI6IkVNQUlMIiwidXNlckluZm8iOiJhcnZlZ0BrdGguc2UifQ.XA7stV8-giIPIDnlUe9shCch7aYniCrRnWzzqT0mgdmuBvFCD9Al2V4H9IaR4jotgKfOlr-PtF7w0iqeZJ82fvOJcSS01aY2KAYPBbAP8ynTojp4-iobJxmA7hDWFcmcO0lFw0KmxeFZbTY2gePBm2jTOJWUF8TELSpH_UadwFQ3fQvsgM6z1hCwKaLYowh7_CYW3rHAddIMD9bOBsPUxqmvfpe-WToh-82xmSoesK8YukQK9Tp3H4f3HmzxvXfAr2B1yBUnViz7xKjUo4Kw1634jCeLPZpuVOWDWjoZpkbboso2SXpMt_ICPni6QvpaEqHfRqlItM6Bz-wxu0GbiQ" } + diff --git a/webdemo/templates/poll.html b/webdemo/templates/poll.html index 7ae9119..f0baa0a 100644 --- a/webdemo/templates/poll.html +++ b/webdemo/templates/poll.html @@ -12,19 +12,27 @@ } +{% with csrf_token_str = csrf_token() %}
- {% with messages = get_flashed_messages() %} + {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} -
- {{ messages[0] }} -
+
    + {% for category, message in messages %} +
  • {{ category }}: {{ message }}
  • + {% endfor %} +
{% endif %} {% endwith %} +

Electronig Voting in three steps

+ {% if not hash and not signature %}
{% else %}
{% endif %} +

1. Cast vote and request signing

- + +
    Candidates
    {% for e in data.fields %} @@ -39,30 +47,35 @@

    {{ data.question }}

- - Results - - Offline vote - - Ciphertexts - Public Key +
-
- {% if hash %} -
-
-
Verify hash values
-

{{ hash }}

+ + {% if hash and not signature %}
{% else %}
{% endif %} +

2. Compare hash and sign on second device

+ {% if hash %} +
+
+
Verify hash values with signing application
+

{{ hash }}

+
+ {% else %} +

nothing to sign yet

+ {% endif %} +
+ {% if signature %}
{% else %}
{% endif %} +

3. Submit vote

+ +
+ {% if signature %} {% else %}

sign the vote first

{% endif %} + +

{{ hash }}

+ + +
+
- {% endif %} {% if vote %}
Raw signature and encrypted vote @@ -70,6 +83,10 @@
Verify hash values
{% endif %}
@@ -95,6 +112,7 @@

About

+{% endwith %} @@ -108,19 +126,51 @@

About

window.success.className += ' hidden'; }, 1000); + // This version does not export hexbyte and byteArrayToHex + // source Verificatum-TS + function hexbyte(b) { + const digits = "0123456789abcdef"; + return digits[b >> 4 & 0xF] + digits[b & 0xF]; + } + function byteArrayToHex(bytes) { + let hexString = ""; + for (i = 0; i < bytes.length; i++) { + hexString += hexbyte(bytes[i]); + } + return hexString; + } + function captureForm() { for (const candidate of document.getElementsByClassName('candidate')) { if (candidate.checked) { + console.log("entered captureForm"); var found = false; {% for e in data.fields %} found |= candidate.value === "{{ e }}"; {% endfor %} if (!found) { - console.error("Candidate does not much any expected value"); + console.error("Candidate does not match any expected value"); return false; } - document.getElementById(candidate.value).value = encrypt(candidate.value); +/* + Future version API + // hash : uint8_t[] + // sha256 : uint8_t[] -> uint8_t[] + const sha256 = new verificatum.crypto.SHA256(); + const hash = sha256.hash(enc_vote); +*/ + + const enc_vote = encrypt(candidate.value); + const enc_vote_ba = enc_vote.toByteArray(); + const hash = verificatum.crypto.sha256.hash(enc_vote_ba) + console.log("setting enc vote (hash): " + byteArrayToHex(hash)); + document.getElementById(candidate.value).value = byteArrayToHex(hash); + + const enc_vote_hex_str = byteArrayToHex(enc_vote.toByteArray()); + localStorage.setItem("enc_vote", enc_vote_hex_str); + console.log("localStorage.enc_vote (hex): " + enc_vote_hex_str); + return true; } } @@ -156,9 +206,30 @@

About

// XXX: why? // In format expected by VMN - const encrypted0 = encrypted.values[0].toByteTree().toByteArray(); - const encrypted1 = encrypted.values[1].toByteTree().toByteArray(); - return JSON.stringify([encrypted0, encrypted1]); + const encrypted0 = encrypted.values[0].toByteTree(); + const encrypted1 = encrypted.values[1].toByteTree(); + + const ByteTree = verificatum.eio.ByteTree; + const enc_vote_bt = new ByteTree([encrypted0,encrypted1]); + return enc_vote_bt; + // return JSON.stringify([encrypted0, encrypted1]); } + + function setBallot() { + const enc_vote = localStorage.getItem('enc_vote'); + const signature = document.getElementById('signature').value; + if (!enc_vote || !signature) { + return false; + } + console.log({"vote": enc_vote, "signature": signature }); + document.getElementById('ballot').value = JSON.stringify({"vote": enc_vote, "signature": signature }); + return true; + } + +function setOfflineVote() { + localStorage.setItem('enc_vote',"000000000200000000020100000021004582f66416a80946526186470c12784a34a9eae8b12df74d7b60f89c2087b7c501000000210047d031d945b4609017a83d569b661cc651a863c88631b2f3038a24a1da61240800000000020100000021000ad727ceb7dc79469a6c43f4ecc9ba85b712acde58cd06b7df568717ef2bb0ff010000002100849391d58bd9707011776836eb2e63819aed978b0cb54e1e3ca76889ad2a09ed"); + document.getElementById('signature').value = "eyJ4NXQiOiJEaVpiekJmeXNVbTYtSXdJLUd0aWVuRXNiamMiLCJhbGciOiJSUzI1NiJ9.eyJzaWduUmVmIjoibGxpYzd0X1lBRFc2ZWdfZXBxYnU4VDA3OFpWWU41amZWX19vOEgxakNYbElCRHMyaXR5c3hMaUpuMFNKY01pWSIsInN0YXR1cyI6IkFQUFJPVkVEIiwidGltZXN0YW1wIjoxNjk1OTAwODk4NzQzLCJtaW5SZWdpc3RyYXRpb25MZXZlbCI6IkJBU0lDIiwic2lnbmF0dXJlVHlwZSI6IlNJTVBMRSIsInNpZ25hdHVyZURhdGEiOnsidXNlclNpZ25hdHVyZSI6ImV5SnJhV1FpT2lKQ1JEQkJOa1pFUmtReU9VVXhPRUkyTVRCQlJFWXpOakkyTVRsRk5qQTFOVVE1TlVGRlFVSTNSRFkwT1RFME5VUkVPVGs1UmpCRlFUUTNPVGswUVRkR0lpd2lZV3huSWpvaVVsTXlOVFlpZlEuT0RSaVl5QmxObVEwSURBNU5qY2dNelF3TmlCallUZGxJRFJrWkRFZ016Y3dZeUE0WXpJMklHVXdaV1VnTXpZeE5TQTJPVGc0SURSaE9HSWdNbVUzTUNBMll6QmhJREl4TkdVZ1pEWXhOdy5teTRkaWVVaEV6bGNzbkplYmx5RWVDZUdXVUVGUzJMUlBSTlpid1c3N1I1eEU2NmtNWm8xWXdHZldTeGlzSlNWZFhneU84aU5RQVh1dEhYX1F4ai01eHFhY3J6NU5Bd05UWm5wcHB4emJweTFrNHlNR0RnZFdjNW1DM3R5elhMOWVqR1hUck5KTW5XeXczc3JFeGhaT1JrMm1HWXdFeDlmcXo4T2NNbkRQZGVyOERDTk9UNXU0R08zUVVMOEtUTjFVTTRVWVhud1hBR2lsQ0NFUjRjVHhVWmxCSUJDd24xWGhzSHFJcHhxazBvZzU5TU0yYUxtdGdZdUR2djhESUQ5ZUpveWpLQjBMVjJWMzRXWUZtX0VKN21kSDRPbEJRbTgtMm1QN3NaYmswNnZ4bGNaTUhXNU1EWk1MRDNNWmw5VVpkMmRoVk1DY2pOOFYwMDBtTExHTmciLCJjZXJ0aWZpY2F0ZVN0YXR1cyI6Ik1JSUdYZ29CQUtDQ0JsY3dnZ1pUQmdrckJnRUZCUWN3QVFFRWdnWkVNSUlHUURDQ0FRT2lGZ1FVMWlsK0djVkhVTDZHQUhiYklkb25BNHY4bG13WUR6SXdNak13T1RJNE1URXpORFU0V2pDQnJ6Q0JyRENCbGpBTkJnbGdoa2dCWlFNRUFnRUZBQVFnTFhzcklFa0JYYVZRbnZGby9HSXJMaFZOYVFaakhlN3lsR2NTWkV4UkU0OEVJQ2RCTG56cGc2V1U4V21SbGZHdURkTTFWcjFhWTZnM0tZZjl3SmFDT0gyeUFrRUF0NFY0WmFwa3RmSTllTGV0dGMwakdwNmNCeEdLdDJxRjhOZ000REJUcE5ZOEE2ZWZXbGlYeVhaYWdrdHlBeVhQVXhsaWlqSTBCQnRjWjRoak0ydFBRb0FBR0E4eU1ESXpNRGt5T0RFeE16UTFPRnFoSmpBa01DSUdDU3NHQVFVRkJ6QUJBZ1FWQkJNd0xqUTBOakk1TlRFeU1EVTVOall6TlRNME1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRRHBrTG1wOGdqL2dqeDFKWDgvOGhEU2FsaUIzWllMNXFyYmR0Rk9IWXF5UTk1MWdqWjRTWmRDcjI5QTNHTjVVWW5rSVdXaVNsL1BNQlQ3K2g5VEFLNzdGem5PNDNTZXFiR3gyakV0VGcxMmVDT3BJSTJKMHhJV2I0QVY2dU5aVE14eG5XVEwvOXNRcTdmZ243Z0xZZmYySmVEZG03dUhOVDE2T09FZFkvS1J6TjRaS1d1ZkNZNmVtWGZJSHpIWWk2eTNINGRkZjBERXZWQkVibGJsODNQcmdJU1RYc28zTGc5cWdYVmlmMmVpZ3JSd0pDN3VTeWdSQUFkVll4eE8xaUw3TGkyZ3RXaFl4TDhKYk5OQTA2SUZUOUtQaVorVFJRTGt5V25heXBUS2NKbWhtKzNIaHZ1L2pNZ1FoQUM1aEZYZXlKaDVjRkpJQzltaCtJVWVMUDh0b0lJRUlUQ0NCQjB3Z2dRWk1JSURBYUFEQWdFQ0FoUkpubTI5TElYS1hzVm9FYnhreEYxczloVTVhVEFOQmdrcWhraUc5dzBCQVFzRkFEQ0JnekVMTUFrR0ExVUVCaE1DVTBVeEVqQVFCZ05WQkFjVENWTjBiMk5yYUc5c2JURVVNQklHQTFVRVlSTUxOVFU1TVRFd0xUUTRNRFl4SFRBYkJnTlZCQW9URkZabGNtbHpaV01nUm5KbGFtRWdaVWxFSUVGQ01RMHdDd1lEVlFRTEV3UlVaWE4wTVJ3d0dnWURWUVFERXhOU1UwRWdWRVZUVkNCSmMzTjFhVzVuSUVOQk1CNFhEVEl3TVRJd09ERXpNVFV4TWxvWERUSXpNVEl3T0RFek1UVXhNbG93ZkRFTE1Ba0dBMVVFQmhNQ1UwVXhFakFRQmdOVkJBY1RDVk4wYjJOcmFHOXNiVEVVTUJJR0ExVUVZUk1MTlRVNU1URXdMVFE0TURZeEhUQWJCZ05WQkFvVEZGWmxjbWx6WldNZ1JuSmxhbUVnWlVsRUlFRkNNUTB3Q3dZRFZRUUxFd1JVWlhOME1SVXdFd1lEVlFRREV3eFBRMU5RSUZOcFoyNXBibWN3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDFURnZlMFFoelFHaWpEWEw2NjJweFg1RWMvUnA3bWNSdzdBTGc4b2wwZzB3MGxpb0pPa2dQend3U0RmeHdpM2NwRkdrWmNVR0JrbHQvdVFrbmc5MUxPWDFOejhBY2NxU3QyWERMeCtpZ0lDeDR3RmgvSEhDNzI3NTBMVFZoRDNnakRpa2ZuOVpockJ4bVVmb0pFaHBLSGV4VTh0cXFOazRIdFhobU5KZEk1RXlxcmMyYk0zV1pMRnEvMVR6aXJMcUd0LzRyRUw2ZFh0OHk3YU1qbGh6dUpTZVlyNXRxUEFUYXEwUkNYbE9HRTZHbVJvVEZFbU5TQ2MrZUFmM1pNQ0dPT2xPSWNsQmRuNnhTOE96THdqQnhpb1B3bDZUQWdaeVZMd0dyTkJHdGl4eXhJZU5pQjducVU1NkxQR21RV3V6ckYyYUUwTHJBazN4WmNDYlVvSU5GQWdNQkFBR2pnWW93Z1ljd0RnWURWUjBQQVFIL0JBUURBZ2JBTUF3R0ExVWRFd0VCL3dRQ01BQXdId1lEVlIwakJCZ3dGb0FVYW55S0Q1MXdEaHphWHkyZ1BDWmZpT2dWdjV3d0VnWURWUjBnQkFzd0NUQUhCZ1VxQXdRRkNqQWRCZ05WSFE0RUZnUVVzdGRIaTZkY3JCKzJkTFZrWldBckp1VHlYOUl3RXdZRFZSMGxCQXd3Q2dZSUt3WUJCUVVIQXdrd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKTDVqRXBBdzhRV0NkaDdrVHRIZE5pZXJUQnIxd3VCL1h3UUNENUpJOFlCQ1JuZzc0d21Td0gySUx4bnN5dnhJc1NCWjhDZDJiUWxuNGN0N3JUSlhMTElkQWxLQmxydDBCbTIxdmVMRGZTNnBSY3UwSk0yNVdyOUw3a3RKNGt1Uk1SdE1IcFNLTHByUEM3OC9ON0pTYUJVMlhQc2JUdFIxTzZCTktXekI2eW5oL0taU2VPVXlHRmZGeTVWNCttcXNPSmk4ano5QytxaXRyMUhNdzhyeHp6eGEzT3dENzNPRzJhL0J5S1JtdjZoZDVtRitlNlZhWjlJV1VXM3hKUEtVVXV4ZE1HbzJXOG5jaEpOZUJaMCt1eXdtcVpJY2E0ekgrVytucEFYVHZsRDBqUUU0QURMUENjK1E5Tll0TFNrajd2TGJEVWx4Q0FvUDFxVVpWdm93aEE9In0sInVzZXJJbmZvVHlwZSI6IkVNQUlMIiwidXNlckluZm8iOiJhcnZlZ0BrdGguc2UifQ.XA7stV8-giIPIDnlUe9shCch7aYniCrRnWzzqT0mgdmuBvFCD9Al2V4H9IaR4jotgKfOlr-PtF7w0iqeZJ82fvOJcSS01aY2KAYPBbAP8ynTojp4-iobJxmA7hDWFcmcO0lFw0KmxeFZbTY2gePBm2jTOJWUF8TELSpH_UadwFQ3fQvsgM6z1hCwKaLYowh7_CYW3rHAddIMD9bOBsPUxqmvfpe-WToh-82xmSoesK8YukQK9Tp3H4f3HmzxvXfAr2B1yBUnViz7xKjUo4Kw1634jCeLPZpuVOWDWjoZpkbboso2SXpMt_ICPni6QvpaEqHfRqlItM6Bz-wxu0GbiQ"; +} + -{% endblock %} \ No newline at end of file +{% endblock %}