use boxplot for timing attack sucess rate distribution

This commit is contained in:
Youngjoon Lee 2024-06-10 17:49:53 +09:00
parent eb864dda33
commit d3634a5728
No known key found for this signature in database
GPG Key ID: 09B750B5BD6F08A2
3 changed files with 58 additions and 38 deletions

View File

@ -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

View File

@ -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):

View File

@ -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),