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 profile slot ids more closely with which ticket slot id is being used for it, but they are not yet officially tied. In the future, they should be.

This commit is contained in:
SimplifiedPrivacy 2026-08-14 19:36:22 -04:00
parent 1695e0314e
commit 31488f790e
17 changed files with 379 additions and 83 deletions

View file

@ -1,5 +1,10 @@
# Major Change Log: # 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.
<br/>
# Reduce Redundancy # Reduce Redundancy
### Aug 13, 2026 ### 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. Isolated download file modules to their own module, then had both the install application versions (browsers), and install dependencies both use that same module.

View file

@ -51,9 +51,12 @@ class Constants:
HV_PROFILE_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles' HV_PROFILE_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles'
HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_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_CONFIG_HOME: Final[str] = f"{HV_CONFIG_HOME}/ticketing"
HV_TICKETING_DATA_HOME: Final[str] = f"{HV_DATA_HOME}/ticket_data" 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_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications'
HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents' HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents'

View file

@ -15,7 +15,7 @@ from core.errors.logger import logger
from core.errors.exceptions import ServerSideError, NetworkingError from core.errors.exceptions import ServerSideError, NetworkingError
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist 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.api_requests.step1_get_or_post import send_data_to_server
from core.services.networking.httpx import connect from core.services.networking.httpx import connect
@ -56,6 +56,9 @@ def initiate_payment(
if tickets_exist_already: if tickets_exist_already:
invoice_data_object.add_error_code("already_exists") invoice_data_object.add_error_code("already_exists")
return invoice_data_object 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() billing_id = do_we_have_billing_id()
if billing_id: if billing_id:

View file

@ -11,6 +11,13 @@ from core.services.helpers.get_how_many_profiles_were_ordered import (
get_how_many_profiles_were_ordered, get_how_many_profiles_were_ordered,
) )
from core.observers.BaseObserver import BaseObserver 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: Goal:
@ -26,19 +33,28 @@ 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. 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( def prepare_tickets(
how_many_profiles: int, how_many_profiles: int,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None,
billing_code: Optional[int] = None
) -> dict: ) -> 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: # make sure it's a number:
if not isinstance(how_many_profiles, int): if not isinstance(how_many_profiles, int):
ticket_observer.notify("failed_input", None) ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"} return {"valid": False, "error_code": "failed_input"}
# make sure that "how_many_profiles" is actually the number of profiles ordered # 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): # (which is based on locally saved data from the previous step):
how_many_ordered = get_how_many_profiles_were_ordered() how_many_ordered = get_how_many_profiles_were_ordered()
if how_many_profiles != how_many_ordered: if how_many_profiles != how_many_ordered:
@ -56,7 +72,11 @@ def prepare_tickets(
# ok now we have the pre-reqs, let's use this high level orchestrator, # ok now we have the pre-reqs, let's use this high level orchestrator,
prep_results = ticket_prep_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: # rest of this function is evaluating the results:
@ -83,3 +103,85 @@ def prepare_tickets(
notification = f"Error with Ticket Preparation or Verification!" notification = f"Error with Ticket Preparation or Verification!"
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
return prep_results 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)

View file

@ -1,3 +1,6 @@
# MissingData, FailedToSave, InvalidData, CriticalFailure defined here
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from typing import Optional from typing import Optional

View file

@ -27,6 +27,7 @@ class ResultError(Enum):
INTERFACE = "interface" INTERFACE = "interface"
TIMEOUT = "timeout" TIMEOUT = "timeout"
INVALID_API_REPLY = "invalid_api_reply" INVALID_API_REPLY = "invalid_api_reply"
ENCRYPTION = "encryption"
LEAK_ISSUE = "leak_issue" LEAK_ISSUE = "leak_issue"
NEED_SYNC = "need_sync" NEED_SYNC = "need_sync"
UNKNOWN = "unknown" UNKNOWN = "unknown"

View file

@ -1,4 +1,7 @@
from core.services.crypto.TicketCustomer import TicketCustomer 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. 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: # First, make the original unblinded commitment. it's saved inside the profile object:
did_unblinded_save = profile_object.make_unblinded_commitment(which_ticket) 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, # 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). # 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: # Then BLIND it, so it can be sent to the billing server:
blind_commitment = profile_object.blind_commitment(which_ticket) 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, # we need to make sure we actually saved the data,
# because it's the only way to unblind it later: # because it's the only way to unblind it later:
@ -33,15 +38,30 @@ def make_ONE_commitment_pair(
return blind_commitment 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, # Setup the entire class object of "profile_object" for using all these other functions,
profile_object = TicketCustomer() profile_object = TicketCustomer()
# setup loop: # setup loop:
which_ticket = 0
list_of_all_blinded_data = [] list_of_all_blinded_data = []
failed_to_save = [] 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: # loop up to the number of profiles requested:
while which_ticket < how_many_profiles_to_make: while which_ticket < how_many_profiles_to_make:
which_ticket = which_ticket + 1 which_ticket = which_ticket + 1

View file

@ -7,6 +7,7 @@ from core.services.WebServiceApiService import WebServiceApiService
from core.controllers.ConnectionController import ConnectionController from core.controllers.ConnectionController import ConnectionController
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver
from core.Constants import Constants from core.Constants import Constants
from core.errors.logger import logger
from typing import Union, Optional from typing import Union, Optional
import base64 import base64

View file

@ -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: def _shut_down_by_known_process_id(process_id: int) -> Result:
graceful_close = singbox.stop(process_id) graceful_close = singbox.stop(process_id)
if not graceful_close.valid: 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") still_active = pid_tools.is_running(pid=process_id, process_name="sing-box")

View file

@ -1,5 +1,7 @@
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.utils.run_commands import run_generic_command from core.utils.run_commands import run_generic_command
from core.errors.logger import logger
import subprocess import subprocess
import re import re

View file

@ -2,7 +2,7 @@ from core.utils.save_data import save_data
from core.errors.logger import logger from core.errors.logger import logger
import json import json
from typing import Optional
# save each of the server's signatures replies to disk, # save each of the server's signatures replies to disk,
def save_ONE_blind_signature(each_blind_signature: dict, which_ticket: int) -> bool: 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 return did_it_save
def save_ALL_blind_sigs(ALL_signed_blind_signatures: list) -> bool: 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: # setup basic counter & flag for the loop below:
which_ticket = 0 which_ticket = 1
did_they_ALL_save = True did_they_ALL_save = True
for each_blind_signature in ALL_signed_blind_signatures: for each_blind_signature in ALL_signed_blind_signatures:
which_ticket = which_ticket + 1
""" """
As a list of JSONs, each signature is loaded seperately, 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. 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.." error_msg = f"Unable to save the blind signature for {which_ticket}! Skipping it and moving on.."
logger.error(error_msg, exc_info=True) logger.error(error_msg, exc_info=True)
finally:
which_ticket = which_ticket + 1
return did_they_ALL_save return did_they_ALL_save

View file

@ -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.api_requests.step1_get_or_post import send_data_to_server
from core.services.networking.httpx import connect from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType 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.networking.make_url import make_url
from core.services.helpers.get_which_billing_key import get_which_billing_key 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 # utils
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
# constants & errors # constants & errors
from core.Constants import Constants from core.Constants import Constants
from core.errors.exceptions import * from core.errors.exceptions import InvalidData, ServerSideError
from core.errors.logger import logger from core.errors.logger import logger
from typing import Optional
def prep_payload(list_of_all_blinded_data: list) -> dict:
# get temp billing id from local storage:
billing_folder = Constants.HV_TICKETING_CONFIG_HOME billing_folder = Constants.HV_TICKETING_CONFIG_HOME
billing_path = f"{billing_folder}/billing_choices.json" 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:
temp_billing_code = get_value_from_json_file(billing_path, "temp_billing_code") 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, # 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 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( def send_blind_commitments(
list_of_all_blinded_data: list, list_of_all_blinded_data: list,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
billing_code: Optional[int] = None
) -> list | None: ) -> list | None:
"""
Purpose:
1) Oversee payload prep
2) Send the data to the server API
3) Handle the response
try: Rank:
payload = prep_payload(list_of_all_blinded_data) Coordinator
"""
# BULK GROUP:
if billing_code is None:
payload = prep_payload_for_bulk(list_of_all_blinded_data)
which_endpoint = "sign"
# 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: # send it:
which_endpoint = "sign"
url = make_url(which_endpoint) url = make_url(which_endpoint)
# api_reply_object = send_data_to_server(payload, url, connection_observer) try:
api_reply_object = connect.single_endpoint( api_reply_object = connect.single_endpoint(
method="post", method="post",
url=url, url=url,
@ -63,18 +122,6 @@ def send_blind_commitments(
ticket_observer.notify("error", subject=api_reply_object.message) ticket_observer.notify("error", subject=api_reply_object.message)
return None 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: # assuming it worked, extract the data:
reply = api_reply_object.data reply = api_reply_object.data
@ -116,6 +163,6 @@ def send_blind_commitments(
except ServerSideError as e: except ServerSideError as e:
logger.error(f"ServerSideError: {e}", exc_info=True) logger.error(f"ServerSideError: {e}", exc_info=True)
return None return None
except: except Exception as e:
logger.error(f"Generic Python Error", exc_info=True) logger.error(f"Generic Python Error {e}", exc_info=True)
return None return None

View file

@ -8,27 +8,68 @@ if TYPE_CHECKING:
from core.services.crypto.make_commitments import make_ALL_commitments 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.unblind_all_tickets import unblind_ALL_tickets
from core.services.prepare_tickets.setup_ticket_tracker import setup_ticket_tracker 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.send_blind_commitments import send_blind_commitments
from core.services.prepare_tickets.validate_blind_signatures import validate_blind_signatures 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.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 # errors & constants
from core.Constants import 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 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( def ticket_prep_orchestrator(
how_many_profiles: int, how_many_profiles: int,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None,
billing_code: Optional[int] = None
) -> dict: ) -> 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: try:
#####################################################
# MAKE BLIND COMMITMENTS
#####################################################
# make all commitments, but get the blinded ones: # 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, # did we actually save them? if not, try again,
if list_of_all_blinded_commitments is None: 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?! # still failed to save?!
if list_of_all_blinded_commitments is None: if list_of_all_blinded_commitments is None:
return { return {
@ -36,13 +77,16 @@ def ticket_prep_orchestrator(
"message": "cant_save", "message": "cant_save",
} }
#####################################################
# SEND BLIND TO GET SIGNED
#####################################################
# assuming we actually saved the unblinding factors, # assuming we actually saved the unblinding factors,
notification = "Sending Blinded Package to the Server.." notification = "Sending Blinded Package to the Server.."
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
# then send the entire blinded list to the server to sign: # then send the entire blinded list to the server to sign:
blind_signatures = send_blind_commitments( 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: # Recieve signatures:
@ -54,19 +98,23 @@ def ticket_prep_orchestrator(
return { return {
"valid": False, "valid": False,
"message": f"The server's blind signature was blank or invalid. It said: {blind_signatures}", "message": f"The server's blind signature was blank or invalid. It said: {blind_signatures}",
"error_code": "invalid_reply"
} }
else: 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, # 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" notification = "Saving the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification) 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, # verify the server's blind signatures against the public key,
notification = "Evaluating the Server's Blind Replies" notification = "Evaluating the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
failed_validations = validate_blind_signatures( 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}") logger.debug(f"failed_validations is {failed_validations}")
@ -86,17 +134,42 @@ def ticket_prep_orchestrator(
"failed_validations": failed_validations, "failed_validations": failed_validations,
} }
#####################################################
# UNBLIND
#####################################################
# Unblind the signatures & combine with unblinded commitment: # Unblind the signatures & combine with unblinded commitment:
notification = f"Unblinding Signatures & Preparing Tickets..." notification = f"Unblinding Signatures & Preparing Tickets..."
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
did_prep_work = unblind_ALL_tickets( 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
) )
#####################################################
# 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: # make a json to keep track of which tickets are used:
setup_tracker = setup_ticket_tracker(how_many_profiles) 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, # this means it unblinded, & setup the tracker without erroring out,
# but it does NOT mean that verification of the blind sigs worked. # but it does NOT mean that verification of the blind sigs worked.
return {"valid": True, "message": "worked"} return {"valid": True, "message": "worked"}

View file

@ -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.exceptions import *
from core.errors.logger import logger from core.errors.logger import logger
from core.Constants import Constants from core.Constants import Constants
import json import json
import os import os
# folders: # folders:
billing_folder = Constants.HV_TICKETING_CONFIG_HOME # billing_folder = Constants.HV_TICKETING_CONFIG_HOME
ticket_tracker_path = f"{billing_folder}/ticket_tracker.json" # ticket_tracker_path = f"{billing_folder}/ticket_tracker.json"
def get_all_unused_tickets() -> dict: 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: if data == False or data == "" or data == None:
return {"valid": False, "message": "invalid_data"} 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: 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, # the ticket # was originally an int, but needs to be a string to do lookups,
which_ticket = str(which_ticket) # type: ignore 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: def does_ticket_tracker_exist() -> tuple:
if os.path.exists(ticket_tracker_path): if os.path.exists(Constants.TICKET_TRACKER_PATH):
return (True, ticket_tracker_path) return (True, Constants.TICKET_TRACKER_PATH)
else: 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: 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: 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) logger.error(error_msg, exc_info=True)
print(error_msg) print(error_msg)
raise ValueError(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 return status, location, subscription
else: else:
raise ValueError( 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
) )

View file

@ -9,13 +9,14 @@ from core.services.crypto.TicketCustomer import TicketCustomer
# utils # utils
from core.utils.save_data import save_data from core.utils.save_data import save_data
# observers # observers
from core.observers.BaseObserver import BaseObserver # from core.observers.BaseObserver import BaseObserver
from core.models.Event import Event # from core.models.Event import Event
# errors # errors
from core.errors.exceptions import * from core.errors.exceptions import MissingData, FailedToSave, InvalidData
from core.errors.logger import logger from core.errors.logger import logger
# generic # generic
import json import json
from typing import Optional
def prep_ONE_unblinded_ticket( def prep_ONE_unblinded_ticket(
@ -49,6 +50,7 @@ def unblind_ALL_tickets(
ALL_signed_blind_signatures: list, ALL_signed_blind_signatures: list,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None
) -> bool: ) -> bool:
try: try:
if ALL_signed_blind_signatures == False or ALL_signed_blind_signatures == None: 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, # Setup the entire class object of "profile_object" for using all these other functions,
profile_object = TicketCustomer() profile_object = TicketCustomer()
# setup basic counter for the loop below: # if it's a single ticket, make sure we only have one ticket in the list:
which_ticket = 0 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: LOOP STARTS:
@ -70,8 +80,6 @@ def unblind_ALL_tickets(
Finally, saving the unblinded signature and unblinded commitment together. Finally, saving the unblinded signature and unblinded commitment together.
""" """
for each_blind_signature in ALL_signed_blind_signatures: 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. # 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): if isinstance(each_blind_signature, str):
blind_signature = json.loads(each_blind_signature) blind_signature = json.loads(each_blind_signature)
@ -114,10 +122,12 @@ def unblind_ALL_tickets(
exc_info=True, exc_info=True,
) )
# continue because this is only one ticket, don't want to ruin the others. # continue because this is only one ticket, don't want to ruin the others.
which_ticket = which_ticket + 1
continue continue
notification = f"Finished ticket {which_ticket}'s prep" notification = f"Finished ticket {which_ticket}'s prep"
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
which_ticket = which_ticket + 1
# finished the loop: # finished the loop:
return True return True

View file

@ -17,10 +17,10 @@ from core.models.Event import Event
from core.Constants import Constants from core.Constants import Constants
from core.errors.exceptions import * from core.errors.exceptions import *
from core.errors.logger import logger from core.errors.logger import logger
import traceback
# generic # generic
import traceback
import json import json
from typing import Optional
def make_sure_we_have_inputs( def make_sure_we_have_inputs(
ALL_signed_blind_signatures: list, ALL_signed_blind_signatures: list,
@ -93,6 +93,7 @@ def validate_blind_signatures(
ALL_signed_blind_signatures: list, ALL_signed_blind_signatures: list,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None,
): ):
try: try:
# Setup the entire class object of "profile_object" for using all these other functions, # 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 # end: confirm inputs
# setup basic counters, flags, and lists for the loop below: # 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" did_we_answer_the_key_question_yet = "no"
list_of_failed_verifications = [] list_of_failed_verifications = []
@ -122,8 +126,6 @@ def validate_blind_signatures(
Looping through each signature, verifying it against the single public key. Looping through each signature, verifying it against the single public key.
""" """
for each_blind_signature in ALL_signed_blind_signatures: for each_blind_signature in ALL_signed_blind_signatures:
which_ticket = which_ticket + 1
# prep from server's format: # prep from server's format:
blind_signature = json.loads(each_blind_signature) blind_signature = json.loads(each_blind_signature)
@ -132,6 +134,9 @@ def validate_blind_signatures(
which_ticket, blind_signature, public_key which_ticket, blind_signature, public_key
) )
# bump loop for next round:
which_ticket = which_ticket + 1
if validity_data["valid"] == True: if validity_data["valid"] == True:
# verification went fine: # verification went fine:
notification = f"Verified {which_ticket}'s blind signature" notification = f"Verified {which_ticket}'s blind signature"

View file

@ -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.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.utils.basic_operations.write_or_read_from_json import ( 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.utils.basic_operations.write_string_to_text_file import write_string_to_text_file
from core.errors.exceptions import * from core.errors.exceptions import *
from core.errors.logger import logger from core.errors.logger import logger
from core.Constants import Constants from core.Constants import Constants
import traceback 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. # Coordinates using the ticket anonymously.
# To do so, it sends to the server & updates the 'ticker_tracker' JSON with new data: # 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, connection_observer: ConnectionObserver,
) -> dict | ApiResponse: ) -> 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: # send to server:
api_reply_object = send_unblinded_ticket_to_server( api_reply_object = send_unblinded_ticket_to_server(
which_ticket, which_location, connection_observer which_ticket, which_location, connection_observer
@ -122,3 +122,4 @@ def make_sure_sub_saved(
billing_code, billing_code,
f"{billing_folder}/emergency_code_for_profile_{which_ticket_as_str}.txt", f"{billing_folder}/emergency_code_for_profile_{which_ticket_as_str}.txt",
) )