diff --git a/change_log.md b/change_log.md index 5176d9f..501c512 100644 --- a/change_log.md +++ b/change_log.md @@ -1,5 +1,10 @@ # Major Change Log: +# Wipe & Respawn Codes/Tickets +### Aug 14, 2026 +Introduced Ticket/Billing-Code Respawn, with dual paths on the ticket prep orchestration, it's helper functions, and the main prep controller. This introduced the functions, which were tested with a demo CLI client and local server, and appears to be stable. Server-side endpoints will be pushed as well. Note: This update couples profiles more closely with which ticket slot is being used for it, but they are not yet officially tied. In the future, they should be. +
+ # Reduce Redundancy ### Aug 13, 2026 Isolated download file modules to their own module, then had both the install application versions (browsers), and install dependencies both use that same module. diff --git a/core/Constants.py b/core/Constants.py index 8b4255a..bc64add 100644 --- a/core/Constants.py +++ b/core/Constants.py @@ -51,9 +51,12 @@ class Constants: HV_PROFILE_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles' HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/profiles' - # ticketing group: + # ── ticketing ───────────────────────────────────────────── HV_TICKETING_CONFIG_HOME: Final[str] = f"{HV_CONFIG_HOME}/ticketing" HV_TICKETING_DATA_HOME: Final[str] = f"{HV_DATA_HOME}/ticket_data" + TICKET_TRACKER_PATH = f"{HV_TICKETING_CONFIG_HOME}/ticket_tracker.json" + # ── end of ticketing ────────────────────────────────────── + HV_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications' HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents' diff --git a/core/controllers/tickets/TicketPayController.py b/core/controllers/tickets/TicketPayController.py index 4e2c9d0..1c4756d 100644 --- a/core/controllers/tickets/TicketPayController.py +++ b/core/controllers/tickets/TicketPayController.py @@ -15,7 +15,7 @@ from core.errors.logger import logger from core.errors.exceptions import ServerSideError, NetworkingError from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist - +from core.services.prepare_tickets.setup_ticket_tracker import setup_ticket_tracker # from core.services.networking.api_requests.step1_get_or_post import send_data_to_server from core.services.networking.httpx import connect @@ -56,6 +56,9 @@ def initiate_payment( if tickets_exist_already: invoice_data_object.add_error_code("already_exists") return invoice_data_object + else: + made_ticket_tracker = setup_ticket_tracker(how_many_profiles=how_many_profiles) + logger.debug(f"Ticket Tracker doesn't exist, so we made it: {made_ticket_tracker} for {how_many_profiles} profiles") billing_id = do_we_have_billing_id() if billing_id: diff --git a/core/controllers/tickets/TicketPrepController.py b/core/controllers/tickets/TicketPrepController.py index 6dee302..7a3af0a 100644 --- a/core/controllers/tickets/TicketPrepController.py +++ b/core/controllers/tickets/TicketPrepController.py @@ -11,6 +11,13 @@ from core.services.helpers.get_how_many_profiles_were_ordered import ( get_how_many_profiles_were_ordered, ) from core.observers.BaseObserver import BaseObserver +from core.models.Result import Result, ResultError +from core.models.session.SessionProfile import SessionProfile +from core.models.system.SystemProfile import SystemProfile +from core.errors.logger import logger + +# generic +from typing import Optional """ Goal: @@ -26,24 +33,33 @@ If it doesn't have the public key, then it checks the config file. If it doesn't have a config file, then it uses the temp billing id to get the public key from the server. """ - +# Bulk Prep: def prepare_tickets( how_many_profiles: int, ticket_observer: TicketObserver, connection_observer: ConnectionObserver, + which_ticket: Optional[int] = None, + billing_code: Optional[int] = None ) -> dict: + # these two are for preparation of a single ticket to a profile. + # can't have one blank without the other. (profile or ticket) + if bool(which_ticket) != bool(billing_code): + return {"valid": False, "error_code": "failed_input"} + # make sure it's a number: if not isinstance(how_many_profiles, int): ticket_observer.notify("failed_input", None) return {"valid": False, "error_code": "failed_input"} - # make sure that "how_many_profiles" is actually the number of profiles ordered - # (which is based on locally saved data from the previous step): - how_many_ordered = get_how_many_profiles_were_ordered() - if how_many_profiles != how_many_ordered: - ticket_observer.notify("failed_input", None) - return {"valid": False, "error_code": "failed_input"} + # allow single prep: + if how_many_profiles != 1: + # but if it's not a single ticket, then make sure that "how_many_profiles" is actually the number of profiles ordered + # (which is based on locally saved data from the previous step): + how_many_ordered = get_how_many_profiles_were_ordered() + if how_many_profiles != how_many_ordered: + ticket_observer.notify("failed_input", None) + return {"valid": False, "error_code": "failed_input"} # make sure this guy has a public key to verify against: does_he_have_public_key = make_sure_pub_key_exists(connection_observer) @@ -56,7 +72,11 @@ def prepare_tickets( # ok now we have the pre-reqs, let's use this high level orchestrator, prep_results = ticket_prep_orchestrator( - how_many_profiles, ticket_observer, connection_observer + how_many_profiles=how_many_profiles, + ticket_observer=ticket_observer, + connection_observer=connection_observer, + which_ticket=which_ticket, # only relevant for single ticket prep + billing_code=billing_code # only relevant for single ticket prep ) # rest of this function is evaluating the results: @@ -83,3 +103,85 @@ def prepare_tickets( notification = f"Error with Ticket Preparation or Verification!" ticket_observer.notify("preparing", subject=notification) return prep_results + + +def respawn_billing_code_into_ticket( + profile: Union[SessionProfile, SystemProfile], + which_ticket: int, + ticket_observer: TicketObserver, + connection_observer: ConnectionObserver +) -> bool: + """ + Converts a valid VPN billing code, into a valid unused ticket. + At the cost of the server expiring the billing code immediately. + + This has the effect of "wiping" the billing code, and starting over. + The new ticket can be applied to any other profile now. + """ + # extract billing code: + billing_code = profile.subscription.billing_code + + if not billing_code: + return Result(valid=False, error_type=ResultError.MISSING_DATA, message="You lack a valid subscription on the profile slot is being respawned") + + if not which_ticket: + return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing which ticket slot is being respawned") + + if not isinstance(which_ticket, int): + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Ticket slot must be a number.") + + ticket_result = prepare_tickets( + how_many_profiles=1, + ticket_observer=ticket_observer, + connection_observer=connection_observer, + which_ticket=which_ticket, + billing_code=billing_code + ) + + if not isinstance(ticket_result, dict): + return Result(valid=False, error_type=ResultError.UNKNOWN, message=f"Error! The prepare_tickets function returned an invalid format of {type(ticket_result)} when it should be a dict. This is being recieved in the respawn_ticket function.") + + valid = ticket_result.get('valid', False) + + if not valid: + notification = f"Error in Respawn Prep!" + ticket_observer.notify("preparing", subject=notification) + return prep_error_result(ticket_result) + + logger.info("Got a valid result from the respawn's prepare_tickets. Now wiping the profile's subscription.") + + # Now that the billing code has been wiped by the server, wipe it locally: + profile.subscription = None + profile.save() + + notification = f"Respawn Done for Profile {profile.id}!" + ticket_observer.notify("preparing", subject=notification) + return Result(valid=True, message=f"It worked. Respawn Done for Profile {profile.id}! It's completely wiped and you now have a valid ticket at slot {which_ticket}") + + +def prep_error_result(ticket_result: dict): + message = ticket_result.get('message', False) + error_code = ticket_result.get('error_code', False) + + if message == "missing_data": + return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing Input Data. This is an error with the introduction functions.") + + elif message == "cant_save": + return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="Can't save the data. Please check your file permissions and free space.") + + elif message == "verification_failed": + how_many_failed = ticket_result.get('how_many_failed', False) + return Result(valid=False, error_type=ResultError.ENCRYPTION, message=f"Verification of the encryption failed for {how_many_failed} profiles.") + + elif error_code == "failed": + return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="There was an error with setting up ticket tracker or some local filesystem problem.") + + else: + if message: + error_msg = message + elif error_code: + error_msg = f"An error occured: {error_code}." + else: + error_msg = "An unknown error occured." + + return Result(valid=False, error_type=ResultError.UNKNOWN, message=error_msg) diff --git a/core/errors/exceptions.py b/core/errors/exceptions.py index f0b50e5..b7cd80c 100644 --- a/core/errors/exceptions.py +++ b/core/errors/exceptions.py @@ -1,3 +1,6 @@ +# MissingData, FailedToSave, InvalidData, CriticalFailure defined here + + from core.models.Result import Result, ResultError from typing import Optional diff --git a/core/models/Result.py b/core/models/Result.py index 4c7280c..9978cfc 100644 --- a/core/models/Result.py +++ b/core/models/Result.py @@ -27,6 +27,7 @@ class ResultError(Enum): INTERFACE = "interface" TIMEOUT = "timeout" INVALID_API_REPLY = "invalid_api_reply" + ENCRYPTION = "encryption" LEAK_ISSUE = "leak_issue" NEED_SYNC = "need_sync" UNKNOWN = "unknown" diff --git a/core/services/crypto/make_commitments.py b/core/services/crypto/make_commitments.py index 1840838..d92e2ca 100644 --- a/core/services/crypto/make_commitments.py +++ b/core/services/crypto/make_commitments.py @@ -1,4 +1,7 @@ from core.services.crypto.TicketCustomer import TicketCustomer +from core.errors.logger import logger + +from typing import Optional """ We are making both an unblinded and blinded commitment pair. @@ -15,12 +18,14 @@ def make_ONE_commitment_pair( # First, make the original unblinded commitment. it's saved inside the profile object: did_unblinded_save = profile_object.make_unblinded_commitment(which_ticket) + logger.info(f"did_unblinded_save {did_unblinded_save}") # that `profile_object` object is holding the unblinded commitment, # so it can be directly used to blind it (without having to serialize then deserialize it). # Then BLIND it, so it can be sent to the billing server: blind_commitment = profile_object.blind_commitment(which_ticket) + logger.info(f"blind_commitment is {blind_commitment}") # we need to make sure we actually saved the data, # because it's the only way to unblind it later: @@ -33,15 +38,30 @@ def make_ONE_commitment_pair( return blind_commitment -def make_ALL_commitments(how_many_profiles_to_make: int) -> list | None: +def make_ALL_commitments(how_many_profiles_to_make: int, which_ticket: Optional[int] = None) -> list | None: # Setup the entire class object of "profile_object" for using all these other functions, profile_object = TicketCustomer() # setup loop: - which_ticket = 0 list_of_all_blinded_data = [] failed_to_save = [] + ########################################################## + # SINGLE TICKET PREP (respawn flow) + ########################################################## + if which_ticket and how_many_profiles_to_make == 1: + logger.info("Doing a single ticket prep.") + blinded_string = make_ONE_commitment_pair(profile_object, which_ticket) + profile_object.reset() + list_of_all_blinded_data.append(blinded_string) + return list_of_all_blinded_data + + ########################################################## + # BULK TICKET PREP (regular flow) + ########################################################## + logger.info(f"Doing a regular bulk ticket prep for {how_many_profiles_to_make} tickets") + which_ticket = 0 + # loop up to the number of profiles requested: while which_ticket < how_many_profiles_to_make: which_ticket = which_ticket + 1 diff --git a/core/services/keys_and_verifications/wireguard_keys.py b/core/services/keys_and_verifications/wireguard_keys.py index 270988c..ac1e280 100644 --- a/core/services/keys_and_verifications/wireguard_keys.py +++ b/core/services/keys_and_verifications/wireguard_keys.py @@ -7,6 +7,7 @@ from core.services.WebServiceApiService import WebServiceApiService from core.controllers.ConnectionController import ConnectionController from core.observers.ConnectionObserver import ConnectionObserver from core.Constants import Constants +from core.errors.logger import logger from typing import Union, Optional import base64 diff --git a/core/services/networking/systemwide/encrypted_proxy/close_singbox.py b/core/services/networking/systemwide/encrypted_proxy/close_singbox.py index 9d512ac..83cd79e 100644 --- a/core/services/networking/systemwide/encrypted_proxy/close_singbox.py +++ b/core/services/networking/systemwide/encrypted_proxy/close_singbox.py @@ -68,7 +68,7 @@ def get_process_id_from_state(current_state: Optional[SystemState]) -> Result: def _shut_down_by_known_process_id(process_id: int) -> Result: graceful_close = singbox.stop(process_id) if not graceful_close.valid: - logger.error(f"[{function_name}] Could not gracefully close it. So we'll {KILL_WAIT_TIME} seconds and kill it.") + logger.error(f"Could not gracefully close it. So we'll {KILL_WAIT_TIME} seconds and kill it.") still_active = pid_tools.is_running(pid=process_id, process_name="sing-box") diff --git a/core/services/networking/systemwide/general_tools/interface_tools.py b/core/services/networking/systemwide/general_tools/interface_tools.py index e6a0db6..12a7762 100644 --- a/core/services/networking/systemwide/general_tools/interface_tools.py +++ b/core/services/networking/systemwide/general_tools/interface_tools.py @@ -1,5 +1,7 @@ from core.models.Result import Result, ResultError from core.utils.run_commands import run_generic_command +from core.errors.logger import logger + import subprocess import re diff --git a/core/services/prepare_tickets/save_ALL_blind_sigs.py b/core/services/prepare_tickets/save_ALL_blind_sigs.py index d21900a..88888f0 100644 --- a/core/services/prepare_tickets/save_ALL_blind_sigs.py +++ b/core/services/prepare_tickets/save_ALL_blind_sigs.py @@ -2,7 +2,7 @@ from core.utils.save_data import save_data from core.errors.logger import logger import json - +from typing import Optional # save each of the server's signatures replies to disk, def save_ONE_blind_signature(each_blind_signature: dict, which_ticket: int) -> bool: @@ -10,14 +10,15 @@ def save_ONE_blind_signature(each_blind_signature: dict, which_ticket: int) -> b return did_it_save -def save_ALL_blind_sigs(ALL_signed_blind_signatures: list) -> bool: - # setup basic counter & flag for the loop below: - which_ticket = 0 +def save_ALL_blind_sigs(ALL_signed_blind_signatures: list, which_ticket: Optional[int] = None) -> bool: + + if which_ticket is None: + # setup basic counter & flag for the loop below: + which_ticket = 1 + did_they_ALL_save = True for each_blind_signature in ALL_signed_blind_signatures: - which_ticket = which_ticket + 1 - """ As a list of JSONs, each signature is loaded seperately, so that if the format is off for a single ticket, it doesn't collapse all the tickets. @@ -44,4 +45,8 @@ def save_ALL_blind_sigs(ALL_signed_blind_signatures: list) -> bool: error_msg = f"Unable to save the blind signature for {which_ticket}! Skipping it and moving on.." logger.error(error_msg, exc_info=True) + finally: + which_ticket = which_ticket + 1 + + return did_they_ALL_save diff --git a/core/services/prepare_tickets/send_blind_commitments.py b/core/services/prepare_tickets/send_blind_commitments.py index 3e0af80..93f2349 100644 --- a/core/services/prepare_tickets/send_blind_commitments.py +++ b/core/services/prepare_tickets/send_blind_commitments.py @@ -8,23 +8,39 @@ if TYPE_CHECKING: # from core.services.networking.api_requests.step1_get_or_post import send_data_to_server from core.services.networking.httpx import connect from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType -from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems - +# from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems from core.services.networking.make_url import make_url from core.services.helpers.get_which_billing_key import get_which_billing_key + +# models & their controllers +from core.models.session.SessionProfile import SessionProfile +from core.models.system.SystemProfile import SystemProfile +from core.models.Subscription import Subscription # not sure if needed +from core.controllers.ProfileController import ProfileController + # utils from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file # constants & errors from core.Constants import Constants -from core.errors.exceptions import * +from core.errors.exceptions import InvalidData, ServerSideError from core.errors.logger import logger +from typing import Optional -def prep_payload(list_of_all_blinded_data: list) -> dict: +billing_folder = Constants.HV_TICKETING_CONFIG_HOME +billing_path = f"{billing_folder}/billing_choices.json" + +# Bulk Group of Tickets: +def prep_payload_for_bulk(list_of_all_blinded_data: list) -> dict: + """ + Purpose: + Prepare the POST payload for a BULK group ticket prep + + Rank: + Mini-Helper + """ # get temp billing id from local storage: - billing_folder = Constants.HV_TICKETING_CONFIG_HOME - billing_path = f"{billing_folder}/billing_choices.json" temp_billing_code = get_value_from_json_file(billing_path, "temp_billing_code") # prep the JSON payload, it doesn't need the public key, since the server can look that up, @@ -36,20 +52,63 @@ def prep_payload(list_of_all_blinded_data: list) -> dict: return payload +# Single Ticket: +def prep_single_ticket_payload(billing_code: str, list_of_all_blinded_data: list) -> dict: + """ + Purpose: + Prepare the POST payload for a SINGLE ticket's respawn prep + + Rank: + Mini-Helper + """ + # get ticketing plan data: + which_key = get_value_from_json_file(billing_path, "which_key") + + if not which_key or not billing_code: + logger.error(f"Missing pre-reqs for sending the blind commitments to the server. profile's billing_code is {billing_code} and which_key is {which_key}") + return None + + # prep the JSON payload, + payload = { + "billing_code": billing_code, + "which_key": which_key, + "blinded_data": list_of_all_blinded_data, + } + + logger.info("We have a valid payload of pre-reqs for sending the blind commitments") + return payload + + def send_blind_commitments( list_of_all_blinded_data: list, ticket_observer: TicketObserver, connection_observer: ConnectionObserver, + billing_code: Optional[int] = None ) -> list | None: + """ + Purpose: + 1) Oversee payload prep + 2) Send the data to the server API + 3) Handle the response - try: - payload = prep_payload(list_of_all_blinded_data) - - # send it: + Rank: + Coordinator + """ + # BULK GROUP: + if billing_code is None: + payload = prep_payload_for_bulk(list_of_all_blinded_data) which_endpoint = "sign" - url = make_url(which_endpoint) - # api_reply_object = send_data_to_server(payload, url, connection_observer) + # SINGLE TICKET: + else: + payload = prep_single_ticket_payload(billing_code=billing_code, list_of_all_blinded_data=list_of_all_blinded_data) + which_endpoint = "respawn" + if not payload: + return False + + # send it: + url = make_url(which_endpoint) + try: api_reply_object = connect.single_endpoint( method="post", url=url, @@ -63,18 +122,6 @@ def send_blind_commitments( ticket_observer.notify("error", subject=api_reply_object.message) return None - # if not api_reply_object.valid: - # final_error_msg = f"[SEND BLIND COMMITMENTS] First Post request failed {api_reply_object.message}" - # logger.error(final_error_msg) - # api_reply_object = solve_api_problems( - # api_reply_object=api_reply_object, - # get_or_post="post", - # url=url, - # payload=payload, - # connection_observer=connection_observer, - # client_observer=None - # ) - # assuming it worked, extract the data: reply = api_reply_object.data @@ -116,6 +163,6 @@ def send_blind_commitments( except ServerSideError as e: logger.error(f"ServerSideError: {e}", exc_info=True) return None - except: - logger.error(f"Generic Python Error", exc_info=True) + except Exception as e: + logger.error(f"Generic Python Error {e}", exc_info=True) return None diff --git a/core/services/prepare_tickets/ticket_prep_orchestrator.py b/core/services/prepare_tickets/ticket_prep_orchestrator.py index 7461ec6..4d3bae4 100644 --- a/core/services/prepare_tickets/ticket_prep_orchestrator.py +++ b/core/services/prepare_tickets/ticket_prep_orchestrator.py @@ -8,27 +8,68 @@ if TYPE_CHECKING: from core.services.crypto.make_commitments import make_ALL_commitments from core.services.prepare_tickets.unblind_all_tickets import unblind_ALL_tickets from core.services.prepare_tickets.setup_ticket_tracker import setup_ticket_tracker +from core.services.prepare_tickets import ticket_tracker from core.services.prepare_tickets.send_blind_commitments import send_blind_commitments from core.services.prepare_tickets.validate_blind_signatures import validate_blind_signatures from core.services.prepare_tickets.save_ALL_blind_sigs import save_ALL_blind_sigs +from core.utils.basic_operations.does_file_exist import does_file_exist # errors & constants from core.Constants import Constants -from core.errors.exceptions import * +from core.errors.exceptions import MissingData, CriticalFailure from core.errors.logger import logger +from typing import Optional + +""" +There's two potential paths here, + +Path 1) Bulk preparation. +Called by TicketPrepController's prepare_tickets +which_ticket and billing_code are None + +Path 2) Single "respawn", +TicketPrepController's respawn_billing_code_into_ticket +with which_ticket and billing_code. (and how_many_profiles is one) +""" def ticket_prep_orchestrator( how_many_profiles: int, ticket_observer: TicketObserver, connection_observer: ConnectionObserver, + which_ticket: Optional[int] = None, + billing_code: Optional[int] = None ) -> dict: + """ + Purpose: + Orchestrate the entire ticket preparation operation. + + Rank: + Feature's Primary Cross-Module Orchestrator + + Called by: + TicketPrepController's prepare_tickets = For Bulk Tickets + TicketPrepController's respawn_billing_code_into_ticket = For a Single Ticket + + Returns: + Dicts. Shouldn't raise errors on it's own. + """ + if not billing_code and which_ticket: + logger.error("Can't have no billing_code, if you have a ticket to replace") + return { + "valid": False, + "message": "missing_data", + } + try: + ##################################################### + # MAKE BLIND COMMITMENTS + ##################################################### # make all commitments, but get the blinded ones: - list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles) + list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket) # did we actually save them? if not, try again, if list_of_all_blinded_commitments is None: - list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles) + list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket) # still failed to save?! if list_of_all_blinded_commitments is None: return { @@ -36,13 +77,16 @@ def ticket_prep_orchestrator( "message": "cant_save", } + ##################################################### + # SEND BLIND TO GET SIGNED + ##################################################### # assuming we actually saved the unblinding factors, notification = "Sending Blinded Package to the Server.." ticket_observer.notify("preparing", subject=notification) # then send the entire blinded list to the server to sign: blind_signatures = send_blind_commitments( - list_of_all_blinded_commitments, ticket_observer, connection_observer + list_of_all_blinded_commitments, ticket_observer, connection_observer, billing_code ) # Recieve signatures: @@ -54,19 +98,23 @@ def ticket_prep_orchestrator( return { "valid": False, "message": f"The server's blind signature was blank or invalid. It said: {blind_signatures}", + "error_code": "invalid_reply" } else: + ##################################################### + # EVALUATE AND SAVE THE SERVER'S REPLIES + ##################################################### # regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again, notification = "Saving the Server's Blind Replies" ticket_observer.notify("preparing", subject=notification) - did_they_ALL_save = save_ALL_blind_sigs(blind_signatures) + did_they_ALL_save = save_ALL_blind_sigs(blind_signatures, which_ticket) # verify the server's blind signatures against the public key, notification = "Evaluating the Server's Blind Replies" ticket_observer.notify("preparing", subject=notification) failed_validations = validate_blind_signatures( - blind_signatures, ticket_observer, connection_observer + blind_signatures, ticket_observer, connection_observer, which_ticket ) logger.debug(f"failed_validations is {failed_validations}") @@ -86,17 +134,42 @@ def ticket_prep_orchestrator( "failed_validations": failed_validations, } + ##################################################### + # UNBLIND + ##################################################### # Unblind the signatures & combine with unblinded commitment: notification = f"Unblinding Signatures & Preparing Tickets..." ticket_observer.notify("preparing", subject=notification) did_prep_work = unblind_ALL_tickets( - blind_signatures, ticket_observer, connection_observer + ALL_signed_blind_signatures=blind_signatures, + ticket_observer=ticket_observer, + connection_observer=connection_observer, + which_ticket=which_ticket ) - # make a json to keep track of which tickets are used: - setup_tracker = setup_ticket_tracker(how_many_profiles) + ##################################################### + # TICKET TRACKER + ##################################################### + # I'm checking how many profiles here, because if it's multiple tickets then, + # we wipe the file if it already exists. But for one ticket, we just update it + if how_many_profiles > 1: + # make a json to keep track of which tickets are used: + setup_tracker = setup_ticket_tracker(how_many_profiles) + if not setup_tracker: + return {"valid": False, "message": "failed"} + else: + # for a single ticket, we only want to setup the ticket tracker if it doesn't exist yet: + if not does_file_exist(Constants.TICKET_TRACKER_PATH): + setup_tracker = setup_ticket_tracker(how_many_profiles) + if not setup_tracker: + return {"valid": False, "message": "failed"} + # assuming ticket tracker exists, let's update it: + wiped_sub = ticket_tracker.wipe_one_ticket_sub(which_ticket) - if did_prep_work and setup_tracker: + ##################################################### + # FINAL EVALUATION + ##################################################### + if did_prep_work: # this means it unblinded, & setup the tracker without erroring out, # but it does NOT mean that verification of the blind sigs worked. return {"valid": True, "message": "worked"} diff --git a/core/services/prepare_tickets/ticket_tracker.py b/core/services/prepare_tickets/ticket_tracker.py index b3519fb..bd4f251 100644 --- a/core/services/prepare_tickets/ticket_tracker.py +++ b/core/services/prepare_tickets/ticket_tracker.py @@ -1,17 +1,18 @@ -from core.utils.basic_operations.write_or_read_from_json import read_entire_json +from core.utils.basic_operations.write_or_read_from_json import read_entire_json, update_json from core.errors.exceptions import * from core.errors.logger import logger from core.Constants import Constants + import json import os # folders: -billing_folder = Constants.HV_TICKETING_CONFIG_HOME -ticket_tracker_path = f"{billing_folder}/ticket_tracker.json" +# billing_folder = Constants.HV_TICKETING_CONFIG_HOME +# ticket_tracker_path = f"{billing_folder}/ticket_tracker.json" def get_all_unused_tickets() -> dict: - data = read_entire_json(ticket_tracker_path) + data = read_entire_json(Constants.TICKET_TRACKER_PATH) if data == False or data == "" or data == None: return {"valid": False, "message": "invalid_data"} @@ -33,7 +34,7 @@ def get_all_unused_tickets() -> dict: def is_given_ticket_able_to_be_read(which_ticket: int) -> bool: - data = read_entire_json(ticket_tracker_path) + data = read_entire_json(Constants.TICKET_TRACKER_PATH) # the ticket # was originally an int, but needs to be a string to do lookups, which_ticket = str(which_ticket) # type: ignore @@ -46,17 +47,17 @@ def is_given_ticket_able_to_be_read(which_ticket: int) -> bool: def does_ticket_tracker_exist() -> tuple: - if os.path.exists(ticket_tracker_path): - return (True, ticket_tracker_path) + if os.path.exists(Constants.TICKET_TRACKER_PATH): + return (True, Constants.TICKET_TRACKER_PATH) else: - return (False, ticket_tracker_path) + return (False, Constants.TICKET_TRACKER_PATH) def get_data_for_a_single_ticket(which_ticket_as_int: int) -> tuple: - data = read_entire_json(ticket_tracker_path) + data = read_entire_json(Constants.TICKET_TRACKER_PATH) if data == False or data == "" or data == None: - error_msg = f"Entire file of tickets does not exist. Check the location {ticket_tracker_path}" + error_msg = f"Entire file of tickets does not exist. Check the location {Constants.TICKET_TRACKER_PATH}" logger.error(error_msg, exc_info=True) print(error_msg) raise ValueError(error_msg) @@ -72,5 +73,19 @@ def get_data_for_a_single_ticket(which_ticket_as_int: int) -> tuple: return status, location, subscription else: raise ValueError( - f"Key '{which_ticket}' does not exist in JSON file {ticket_tracker_path}" + f"Key '{which_ticket}' does not exist in JSON file {Constants.TICKET_TRACKER_PATH}" ) + + +def wipe_one_ticket_sub(which_ticket: int) -> bool: + wipe_payload = { + "status": "unused", + "location": None, + "subscription": None + } + + return update_json( + filepath=Constants.TICKET_TRACKER_PATH, + key_to_add=which_ticket, + value_to_update=wipe_payload + ) \ No newline at end of file diff --git a/core/services/prepare_tickets/unblind_all_tickets.py b/core/services/prepare_tickets/unblind_all_tickets.py index e3af34a..f4d8f00 100644 --- a/core/services/prepare_tickets/unblind_all_tickets.py +++ b/core/services/prepare_tickets/unblind_all_tickets.py @@ -9,13 +9,14 @@ from core.services.crypto.TicketCustomer import TicketCustomer # utils from core.utils.save_data import save_data # observers -from core.observers.BaseObserver import BaseObserver -from core.models.Event import Event +# from core.observers.BaseObserver import BaseObserver +# from core.models.Event import Event # errors -from core.errors.exceptions import * +from core.errors.exceptions import MissingData, FailedToSave, InvalidData from core.errors.logger import logger # generic import json +from typing import Optional def prep_ONE_unblinded_ticket( @@ -49,6 +50,7 @@ def unblind_ALL_tickets( ALL_signed_blind_signatures: list, ticket_observer: TicketObserver, connection_observer: ConnectionObserver, + which_ticket: Optional[int] = None ) -> bool: try: if ALL_signed_blind_signatures == False or ALL_signed_blind_signatures == None: @@ -59,8 +61,16 @@ def unblind_ALL_tickets( # Setup the entire class object of "profile_object" for using all these other functions, profile_object = TicketCustomer() - # setup basic counter for the loop below: - which_ticket = 0 + # if it's a single ticket, make sure we only have one ticket in the list: + if which_ticket is not None: + if not isinstance(which_ticket, int): + return False + quantity_tickets = len(ALL_signed_blind_signatures) + if quantity_tickets != 1: + return False + else: + # setup counter for the loop below because it's not an individual ticket: + which_ticket = 1 """ LOOP STARTS: @@ -70,8 +80,6 @@ def unblind_ALL_tickets( Finally, saving the unblinded signature and unblinded commitment together. """ for each_blind_signature in ALL_signed_blind_signatures: - which_ticket = which_ticket + 1 - # We type check, instead of just blindly loading the json, because this function used by two different modules with different formats. if isinstance(each_blind_signature, str): blind_signature = json.loads(each_blind_signature) @@ -114,10 +122,12 @@ def unblind_ALL_tickets( exc_info=True, ) # continue because this is only one ticket, don't want to ruin the others. + which_ticket = which_ticket + 1 continue notification = f"Finished ticket {which_ticket}'s prep" ticket_observer.notify("preparing", subject=notification) + which_ticket = which_ticket + 1 # finished the loop: return True diff --git a/core/services/prepare_tickets/validate_blind_signatures.py b/core/services/prepare_tickets/validate_blind_signatures.py index c26b401..eb1494b 100644 --- a/core/services/prepare_tickets/validate_blind_signatures.py +++ b/core/services/prepare_tickets/validate_blind_signatures.py @@ -17,10 +17,10 @@ from core.models.Event import Event from core.Constants import Constants from core.errors.exceptions import * from core.errors.logger import logger -import traceback # generic +import traceback import json - +from typing import Optional def make_sure_we_have_inputs( ALL_signed_blind_signatures: list, @@ -93,6 +93,7 @@ def validate_blind_signatures( ALL_signed_blind_signatures: list, ticket_observer: TicketObserver, connection_observer: ConnectionObserver, + which_ticket: Optional[int] = None, ): try: # Setup the entire class object of "profile_object" for using all these other functions, @@ -112,7 +113,10 @@ def validate_blind_signatures( # end: confirm inputs # setup basic counters, flags, and lists for the loop below: - which_ticket = 0 + + if which_ticket is None: + which_ticket = 1 + did_we_answer_the_key_question_yet = "no" list_of_failed_verifications = [] @@ -122,8 +126,6 @@ def validate_blind_signatures( Looping through each signature, verifying it against the single public key. """ for each_blind_signature in ALL_signed_blind_signatures: - which_ticket = which_ticket + 1 - # prep from server's format: blind_signature = json.loads(each_blind_signature) @@ -132,6 +134,9 @@ def validate_blind_signatures( which_ticket, blind_signature, public_key ) + # bump loop for next round: + which_ticket = which_ticket + 1 + if validity_data["valid"] == True: # verification went fine: notification = f"Verified {which_ticket}'s blind signature" diff --git a/core/services/using_tickets/use_ticket_orchestrator.py b/core/services/using_tickets/use_ticket_orchestrator.py index 9fdf014..215c8a2 100644 --- a/core/services/using_tickets/use_ticket_orchestrator.py +++ b/core/services/using_tickets/use_ticket_orchestrator.py @@ -8,14 +8,18 @@ from core.services.using_tickets.send_unblinded import send_unblinded_ticket_to_ from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType from core.utils.basic_operations.write_or_read_from_json import ( - update_value_in_json_with_two_values, + update_value_in_json_with_two_values ) from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file from core.errors.exceptions import * from core.errors.logger import logger from core.Constants import Constants + import traceback +# prep the ticket tracker: +billing_folder = Constants.HV_TICKETING_CONFIG_HOME +ticket_tracker_path = f"{billing_folder}/ticket_tracker.json" # Coordinates using the ticket anonymously. # To do so, it sends to the server & updates the 'ticker_tracker' JSON with new data: @@ -25,10 +29,6 @@ def use_ticket_orchestrator( connection_observer: ConnectionObserver, ) -> dict | ApiResponse: - # prep the ticket tracker: - billing_folder = Constants.HV_TICKETING_CONFIG_HOME - ticket_tracker_path = f"{billing_folder}/ticket_tracker.json" - # send to server: api_reply_object = send_unblinded_ticket_to_server( which_ticket, which_location, connection_observer @@ -122,3 +122,4 @@ def make_sure_sub_saved( billing_code, f"{billing_folder}/emergency_code_for_profile_{which_ticket_as_str}.txt", ) +