From d3634a57284c0dbf70e2b02bf2090c4de913a8cd Mon Sep 17 00:00:00 2001 From: Youngjoon Lee <5462944+youngjoon-lee@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:49:53 +0900 Subject: [PATCH] use boxplot for timing attack sucess rate distribution --- mixnet/v2/sim/adversary.py | 14 ++++++--- mixnet/v2/sim/analysis.py | 63 +++++++++++++++++++------------------- mixnet/v2/sim/node.py | 19 +++++++++++- 3 files changed, 58 insertions(+), 38 deletions(-) diff --git a/mixnet/v2/sim/adversary.py b/mixnet/v2/sim/adversary.py index 6284246..3029b35 100644 --- a/mixnet/v2/sim/adversary.py +++ b/mixnet/v2/sim/adversary.py @@ -24,7 +24,8 @@ class Adversary: self.msg_pools_per_window.append(defaultdict(lambda: deque())) self.msgs_received_per_window = [] # list[dict[receiver, set[sender])]] self.msgs_received_per_window.append(defaultdict(set)) - self.final_msgs_received = defaultdict(dict) # dict[receiver, dict[window, sender]] + # dict[receiver, dict[window, list[(sender, origin_id)]]] + self.final_msgs_received = defaultdict(lambda: defaultdict(list)) # self.node_states = defaultdict(dict) self.env.process(self.update_observation_window()) @@ -33,10 +34,13 @@ class Adversary: self.message_sizes.append(len(msg)) def observe_receiving_node(self, sender: "Node", receiver: "Node", msg: SphinxPacket | bytes): - self.msg_pools_per_window[-1][receiver].append(self.env.now) - self.msgs_received_per_window[-1][receiver].add(sender) - if receiver.operated_by_adversary and not isinstance(msg, SphinxPacket): - self.final_msgs_received[receiver][len(self.msg_pools_per_window) - 1] = sender + cur_window = len(self.msg_pools_per_window) - 1 + self.msg_pools_per_window[cur_window][receiver].append(self.env.now) + self.msgs_received_per_window[cur_window][receiver].add(sender) + + origin_id = receiver.inspect_message(msg) + if origin_id is not None: + self.final_msgs_received[receiver][cur_window].append((sender, origin_id)) # if node not in self.node_states[self.env.now]: # self.node_states[self.env.now][node] = NodeState.RECEIVING diff --git a/mixnet/v2/sim/analysis.py b/mixnet/v2/sim/analysis.py index 433ca5d..e8bcd6f 100644 --- a/mixnet/v2/sim/analysis.py +++ b/mixnet/v2/sim/analysis.py @@ -22,6 +22,7 @@ COL_EXPECTED = "Expected" COL_MSG_SIZE = "Message Size" COL_EGRESS = "Egress" COL_INGRESS = "Ingress" +COL_SUCCESS_RATE = "Success Rate (%)" class Analysis: @@ -67,7 +68,7 @@ class Analysis: plt.xlabel(COL_TIME) plt.ylabel("Bandwidth (KiB/s)") plt.ylim(bottom=0) - # Customize the legend to show only 'egress' and 'ingress' regardless of node_id + # Customize the legend to show only "egress" and "ingress" regardless of node_id handles, labels = plt.gca().get_legend_handles_labels() by_label = dict(zip(labels, handles)) plt.legend(by_label.values(), by_label.keys()) @@ -194,40 +195,38 @@ class Analysis: def timing_attack(self, hops_between_layers: int): hops_to_observe = hops_between_layers * (self.config.mixnet.num_mix_layers + 1) - suspected_senders = Counter() - for receiver, windows_and_senders in self.sim.p2p.adversary.final_msgs_received.items(): - for window, sender in windows_and_senders.items(): - suspected_senders.update(self.timing_attack_with(receiver, window, hops_to_observe, sender)) + success_rates = [] + for receiver, windows_and_msgs in self.sim.p2p.adversary.final_msgs_received.items(): + for window, senders_and_origins in windows_and_msgs.items(): + for sender, origin_id in senders_and_origins: + suspected_origins = self.timing_attack_with(receiver, window, hops_to_observe, sender) + suspected_origin_ids = {node.id for node in suspected_origins.keys()} + if origin_id in suspected_origin_ids: + success_rate = 1 / len(suspected_origin_ids) * 100.0 + else: + success_rate = 0.0 + success_rates.append(success_rate) - suspected_senders = ({node.id: count for node, count in suspected_senders.items()}) - print(f"suspected nodes count: {len(suspected_senders)}") - - # Create the bar plot for original sender counts - original_senders = ({node.id: count for node, count in self.sim.p2p.measurement.original_senders.items()}) - plt.figure(figsize=(12, 8)) - plt.bar(list(original_senders.keys()), list(original_senders.values())) - plt.xlabel("Node ID") - plt.ylabel("Counts") - plt.title("Original Sender Counts") - plt.xlim(-1, self.config.mixnet.num_nodes) - plt.show() - - # Create the bar plot for suspected sender counts - keys = list(suspected_senders.keys()) - values = list(suspected_senders.values()) - # Create the bar plot - plt.figure(figsize=(12, 8)) - plt.bar(keys, values) - plt.xlabel("Node ID") - plt.ylabel("Counts") - plt.title("Suspected Sender Counts") - plt.xlim(-1, self.config.mixnet.num_nodes) + df = pd.DataFrame(success_rates, columns=[COL_SUCCESS_RATE]) + print(df.describe()) + plt.figure(figsize=(6, 6)) + plt.boxplot(df[COL_SUCCESS_RATE], vert=True, patch_artist=True, boxprops=dict(facecolor="lightblue"), + medianprops=dict(color="orange")) + mean = df[COL_SUCCESS_RATE].mean() + median = df[COL_SUCCESS_RATE].median() + plt.axhline(mean, color="red", linestyle="--", linewidth=1, label=f"Mean: {mean:.2f}%") + plt.axhline(median, color="orange", linestyle="-", linewidth=1, label=f"Median: {median:.2f}%") + plt.ylim(-5, 105) + plt.title("Timing attack success rate distribution") + plt.legend() + plt.grid(True) plt.show() def timing_attack_with(self, receiver: "Node", window: int, remaining_hops: int, sender: "Node" = None) -> Counter: assert remaining_hops >= 1 - # Start inspecting senders who sent messages that were arrived in the receiver at the given window + # Start inspecting senders who sent messages that were arrived in the receiver at the given window. + # If the specific sender is given, inspect only that sender to maximize the success rate. if sender is not None: senders = {sender} else: @@ -238,7 +237,7 @@ class Analysis: return Counter(senders) # A result to be returned after inspecting all senders who sent messages to the receiver - suspected_senders = Counter() + suspected_origins = Counter() # Inspect each sender who sent messages to the receiver for sender in senders: @@ -248,9 +247,9 @@ class Analysis: for prev_window in range(window - 1, window - 1 - window_range, -1): if prev_window < 0: break - suspected_senders.update(self.timing_attack_with(sender, prev_window, remaining_hops - 1)) + suspected_origins.update(self.timing_attack_with(sender, prev_window, remaining_hops - 1)) - return suspected_senders + return suspected_origins @staticmethod def print_nodes_per_hop(nodes_per_hop, starting_window: int): diff --git a/mixnet/v2/sim/node.py b/mixnet/v2/sim/node.py index 9833d75..3bd9379 100644 --- a/mixnet/v2/sim/node.py +++ b/mixnet/v2/sim/node.py @@ -106,11 +106,28 @@ class Node: final_msg = msg[:msg.rfind(self.PADDING_SEPARATOR)] self.log("Received final message: %s" % final_msg) + def inspect_message(self, msg: SphinxPacket | bytes) -> int | None: + """ + Inspects the message if the node is operated by adversary. + @param msg: SphinxPacket or final unwrapped message + @return: Origin Node ID, or None if the node is not operated by adversary + """ + if self.operated_by_adversary and isinstance(msg, bytes): + origin_id, _ = Node.parse_payload(msg) + return origin_id + return None + def build_payload(self) -> bytes: - payload = bytes(f"{self.id}-{self.payload_id}", "utf-8") + payload = bytes(f"{self.id}-{self.payload_id}-", "utf-8") self.payload_id += 1 return payload + bytes(self.config.mixnet.payload_size - len(payload)) + @staticmethod + def parse_payload(payload: bytes) -> (int, int): + parts = payload.split(b"-") + node_id, payload_id = int(parts[0]), int(parts[1]) + return node_id, payload_id + def pad_payload(self, payload: bytes, target_size: int) -> bytes: """ Pad the final msg to the target size (e.g. the same size as a SphinxPacket),