Medical dispenser - Don’t trust the user
Remember that scene in Project Hail Mary [spoiler alert] by Andy Weir where Dr. Ryland Grace outsmarts the ship's computer? He's in pain, asks for painkillers, and the computer refuses because he needs to wait three hours between doses. His solution? Simply tell the computer to advance its clock by three hours and ask again. It works, and Grace gets his medication, smugly noting "What a stupid system."
Reading this brought back memories from the late '90s when I'd set my PC's date backward to squeeze a few more days from trial software. Sound familiar? We all thought we were clever hackers. Turns out, we were just exploiting the same fundamental flaw that Grace discovered on his interstellar journey: trusting system time for critical decisions is like trusting a student's "my dog ate my homework" excuse.
When Time Manipulation Gets Serious
While tricking trial software was harmless teenage rebellion, this vulnerability has caused real disasters. In 2012, a major trading firm lost $440 million in 45 minutes [source] partly because their system logic relied on timing assumptions that failed catastrophically. More seriously, several medical devices have been recalled when researchers discovered that changing system time could bypass safety limits on drug dosages or radiation exposure.
The problem isn't that computers are stupid - it's that programmers sometimes forget that time isn't as immutable as we'd like to believe.
Building Our Space Medication Dispenser
Let's create a medication dispenser that would make Dr. Ryland Grace work harder for his painkillers. We'll start with the naive approach that the Hail Mary's computer probably used, then progressively make it more hack-resistant.
The Vulnerable Approach: Trusting System Time
from datetime import datetime, timedelta
import json
class NaiveMedicationDispenser:
"""The Hail Mary's original dispenser - easily hackable"""
def __init__(self):
self.dose_history = []
self.minimum_interval = timedelta(hours=3)
self.max_daily_doses = 6
def request_dose(self, current_time=None):
# Trust whatever time is given (or system time)
if current_time is None:
current_time = datetime.now()
# Check if enough time has passed
if self.dose_history:
last_dose = self.dose_history[-1]
time_since_last = current_time - last_dose
if time_since_last < self.minimum_interval:
wait_time = self.minimum_interval - time_since_last
return f"ACCESS DENIED: Wait {wait_time.seconds // 60} more minutes"
# Check daily limit (also vulnerable!)
today_doses = [d for d in self.dose_history
if d.date() == current_time.date()]
if len(today_doses) >= self.max_daily_doses:
return "ACCESS DENIED: Daily limit reached"
# Dispense the medication
self.dose_history.append(current_time)
return "DISPENSED: One dose of space aspirin delivered"
# Grace's hack in action:
dispenser = NaiveMedicationDispenser()
print(dispenser.request_dose(datetime.now())) # First dose works
print(dispenser.request_dose(datetime.now())) # Denied - too soon
# The "clever" hack:
future_time = datetime.now() + timedelta(hours=3, minutes=1)
print(dispenser.request_dose(future_time)) # Works! Computer is "stupid"
See the problem? We're trusting external input for something safety-critical. It's like having a security door that asks visitors to self-report whether they're authorized.
Better Approach: Monotonic Time (In-Memory Only)
Python's time.monotonic() provides a clock that cannot go backwards and isn't affected by system time changes. Think of it as a stopwatch that starts when the program runs and just keeps counting up, immune to Grace's shenanigans.
Important limitation: Monotonic time is only reliable within a single program session. It resets when the program restarts, so you can't persist these values to disk. For persistent enforcement, you'll need the secure approach shown later.
import time
from collections import deque
class MonotonicMedicationDispenser:
"""Uses monotonic time - can't be tricked by system clock changes
Note: This only works within a single program session.
Monotonic time resets on restart, so don't persist these values!
"""
def __init__(self):
self.dose_times = deque()
self.minimum_interval_seconds = 3 * 3600 # 3 hours
self.daily_window_seconds = 24 * 3600
self.max_daily_doses = 6
def request_dose(self):
current_monotonic = time.monotonic()
# Check interval from last dose
if self.dose_times:
time_since_last = current_monotonic - self.dose_times[-1]
if time_since_last < self.minimum_interval_seconds:
wait_seconds = self.minimum_interval_seconds - time_since_last
return f"ACCESS DENIED: Wait {wait_seconds // 60:.0f} more minutes"
# Check doses in last 24 hours (sliding window)
cutoff_time = current_monotonic - self.daily_window_seconds
recent_doses = [t for t in self.dose_times if t > cutoff_time]
if len(recent_doses) >= self.max_daily_doses:
oldest_expiry = recent_doses[0] + self.daily_window_seconds
wait_seconds = oldest_expiry - current_monotonic
return f"ACCESS DENIED: Daily limit. Next dose in {wait_seconds // 3600:.1f} hours"
# Dispense and record
self.dose_times.append(current_monotonic)
return "DISPENSED: Medication delivered safely"
# Grace tries his trick:
dispenser = MonotonicMedicationDispenser()
print(dispenser.request_dose()) # First dose works
# Even if Grace changes system time here, it won't help
print(dispenser.request_dose()) # Still denied - monotonic time doesn't care
The beauty of monotonic time is that it's simple and reliable for in-memory checks during a single session. Grace can set the system clock to the year 3000 or back to 1970, but monotonic time just keeps counting forward. However, if the program restarts, all bets are off—which is why truly critical systems need something more robust.
Ultimate Approach: Secure Event Log with Proper Cryptography
For truly critical systems (like actual medical devices or spacecraft), we need something that survives restarts and detects tampering. This version uses proper HMAC authentication, stores real timestamps, and maintains an append-only audit trail.
import hmac
import hashlib
import json
import time
import os
from datetime import datetime, timezone
from pathlib import Path
class SecureMedicationDispenser:
"""Uses cryptographic event chain with proper HMAC authentication
This version:
- Uses HMAC for proper message authentication
- Stores UTC timestamps for persistence across restarts
- Uses monotonic time for in-session interval enforcement
- Maintains tamper-evident event log
- Loads secret from environment or generates securely
"""
def __init__(self, log_file="medication_log.json"):
self.log_file = Path(log_file)
self.minimum_interval_seconds = 3 * 3600
self.max_daily_doses = 6
# Load or generate secure device secret
self.device_secret = self._get_or_create_secret()
# Track monotonic time for current session only
self.session_start_monotonic = time.monotonic()
self.session_start_utc = datetime.now(timezone.utc)
# Load existing event log
self.event_log = self._load_log()
def _get_or_create_secret(self):
"""Get secret from environment or generate securely"""
secret = os.environ.get('MEDICATION_DISPENSER_SECRET')
if secret:
return secret.encode('utf-8')
# For demo purposes, generate random secret
# In production: use hardware security module or secure key storage
secret = os.urandom(32)
print("Warning: Using randomly generated secret. Set MEDICATION_DISPENSER_SECRET environment variable for production.")
return secret
def _compute_hmac(self, message):
"""Compute HMAC-SHA256 of message using device secret"""
return hmac.new(
self.device_secret,
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
def _load_log(self):
"""Load and verify existing event log"""
if not self.log_file.exists():
return []
try:
with open(self.log_file, 'r') as f:
events = json.load(f)
# Verify chain integrity
for i, event in enumerate(events):
# Reconstruct the message that was signed
event_copy = event.copy()
stored_hmac = event_copy.pop('hmac')
event_string = json.dumps(event_copy, sort_keys=True)
expected_hmac = self._compute_hmac(event_string)
if expected_hmac != stored_hmac:
raise ValueError(f"HMAC verification failed at event {i}")
return events
except Exception as e:
print(f"Error loading log: {e}")
return []
def _save_log(self):
"""Persist event log to disk"""
with open(self.log_file, 'w') as f:
json.dump(self.event_log, f, indent=2)
def _create_event(self, event_type, success, reason=""):
"""Create a tamper-evident event record with proper HMAC"""
# Use UTC time for persistence across restarts
utc_now = datetime.now(timezone.utc)
event = {
'index': len(self.event_log),
'type': event_type,
'success': success,
'reason': reason,
'utc_timestamp': utc_now.isoformat(),
'previous_hmac': self.event_log[-1]['hmac'] if self.event_log else "genesis"
}
# Create HMAC of event data (excluding the HMAC itself)
event_string = json.dumps(event, sort_keys=True)
event['hmac'] = self._compute_hmac(event_string)
return event
def _get_utc_now(self):
"""Get current UTC time as timezone-aware datetime"""
return datetime.now(timezone.utc)
def request_dose(self):
"""Request medication with complete audit trail"""
utc_now = self._get_utc_now()
# Get successful dose events
successful_doses = [e for e in self.event_log
if e['type'] == 'DOSE_REQUEST' and e['success']]
# Check interval constraint using most recent dose
if successful_doses:
last_dose_time = datetime.fromisoformat(
successful_doses[-1]['utc_timestamp']
)
elapsed = (utc_now - last_dose_time).total_seconds()
if elapsed < self.minimum_interval_seconds:
event = self._create_event(
'DOSE_REQUEST', False,
f"Interval violation: {elapsed:.0f}s < {self.minimum_interval_seconds}s"
)
self.event_log.append(event)
self._save_log()
return f"DENIED: Wait {(self.minimum_interval_seconds - elapsed) // 60:.0f} minutes"
# Check Daily limit = last 24 hours, not calendar day midnight reset
cutoff = utc_now.timestamp() - (24 * 3600)
recent_doses = [
e for e in successful_doses
if datetime.fromisoformat(e['utc_timestamp']).timestamp() > cutoff
]
if len(recent_doses) >= self.max_daily_doses:
event = self._create_event(
'DOSE_REQUEST', False,
f"Daily limit reached: {len(recent_doses)}/{self.max_daily_doses}"
)
self.event_log.append(event)
self._save_log()
return "DENIED: Daily limit reached"
# Success - dispense medication
event = self._create_event('DOSE_REQUEST', True, "Dose dispensed")
self.event_log.append(event)
self._save_log()
return "DISPENSED: Medication delivered"
def audit_log(self, num_events=5):
"""Show recent events for debugging or medical review"""
for event in self.event_log[-num_events:]:
status = "✓" if event['success'] else "✗"
timestamp = datetime.fromisoformat(event['utc_timestamp'])
local_time = timestamp.astimezone()
print(f"{status} {local_time.strftime('%Y-%m-%d %H:%M:%S %Z')}: {event['reason']}")
def verify_integrity(self):
"""Verify the entire event chain hasn't been tampered with"""
try:
for i, event in enumerate(self.event_log):
event_copy = event.copy()
stored_hmac = event_copy.pop('hmac')
event_string = json.dumps(event_copy, sort_keys=True)
expected_hmac = self._compute_hmac(event_string)
if expected_hmac != stored_hmac:
return False, f"HMAC mismatch at event {i}"
return True, "Event log integrity verified"
except Exception as e:
return False, f"Verification error: {e}"
Testing the Secure Dispenser
# TEST SCRIPT - Run this to see it in action!
if __name__ == "__main__":
print("=== TESTING SECURE MEDICATION DISPENSER ===\n")
# Create a dispenser with shorter intervals for testing
dispenser = SecureMedicationDispenser("test_medication_log.json")
dispenser.minimum_interval_seconds = 10 # Override for quick testing
print("Test 1: First dose (should work)")
print(f"Result: {dispenser.request_dose()}\n")
print("Test 2: Immediate second dose (should fail)")
print(f"Result: {dispenser.request_dose()}\n")
print("Test 3: Grace tries to change system time...")
print("(Note: This won't help - we use UTC timestamps with HMAC!)")
print(f"Result: {dispenser.request_dose()}\n")
print("Waiting 10 seconds for next valid dose...")
time.sleep(10)
print("Test 4: After waiting (should work)")
print(f"Result: {dispenser.request_dose()}\n")
print("\n=== AUDIT LOG ===")
dispenser.audit_log()
print("\n=== TESTING DAILY LIMIT ===")
print("Simulating reaching daily limit...")
# Create new dispenser with 1-second intervals for quick testing
quick_dispenser = SecureMedicationDispenser("test_daily_limit.json")
quick_dispenser.minimum_interval_seconds = 1
quick_dispenser.max_daily_doses = 3 # Lower limit for testing
for i in range(5):
result = quick_dispenser.request_dose()
print(f"Dose attempt {i+1}: {result}")
if i < 4: # Don't wait after last attempt
time.sleep(1.1)
print("\n=== AUDIT LOG FOR DAILY LIMIT TEST ===")
quick_dispenser.audit_log()
print("\n=== VERIFY TAMPER DETECTION ===")
is_valid, message = quick_dispenser.verify_integrity()
print(f"Integrity check: {message}")
print("\nEvent chain (each HMAC depends on previous):")
for event in quick_dispenser.event_log[-3:]:
print(f"Event {event['index']}: HMAC={event['hmac'][:16]}...")
# Cleanup test files
import os
for f in ["test_medication_log.json", "test_daily_limit.json"]:
if os.path.exists(f):
os.remove(f)
This approach combines several security best practices:
- HMAC authentication instead of simple hashing - proper cryptographic message authentication
- UTC timestamps that persist across restarts and can be audited
- Secure key management using environment variables rather than hardcoded secrets
- Full-length cryptographic digests (256 bits, not truncated to 64 bits)
- Tamper-evident event chain where each event includes the HMAC of the previous event
- Persistent storage that survives program restarts
The Real Lesson
When Andy Weir wrote this scene, he probably knew exactly what he was doing. As a programmer himself, he understood that this vulnerability is everywhere in software.
The progression from naive datetime checking to monotonic time to cryptographically-secured event sourcing mirrors how the software industry has evolved its thinking about security and reliability. We've learned (sometimes painfully) that in critical systems, you can't trust anything that can be externally manipulated.
For space exploration software, where Grace's life literally depends on proper functioning, these patterns aren't just best practices - they're the difference between a successful mission and disaster. But even in our earthbound Python projects, applying these principles makes our code more robust, secure, and professional.
Key Takeaways
- Never trust system time for security decisions - it can be manipulated
- Use monotonic time for in-memory interval checking - but remember it resets on restart
- Use proper HMAC for authentication - not string concatenation with SHA256
- Store UTC timestamps for persistence - monotonic time values don't survive restarts
- Never hardcode secrets - use environment variables or secure key storage
- Don't truncate cryptographic digests - keep the full 256 bits of security
Next time you're building something time-sensitive - whether it's rate limiting for an API, session timeout handling, or even a game's cooldown timer - remember Grace and his painkillers. Ask yourself: "Could a desperate astronaut break this?" If the answer is yes, it's time to implement proper security measures.
And let's face it - if Grace had physical access to the computer's hardware and Rocky's engineering help, there's only so much software can do to stop him (Rockty's voice: Bad, bad, bad)
import hmac
import hashlib
import json
import time
import os
from datetime import datetime, timezone
from pathlib import Path
class SecureMedicationDispenser:
"""Uses cryptographic event chain with proper HMAC authentication
This version:
- Uses HMAC for proper message authentication
- Stores UTC timestamps for persistence across restarts
- Uses monotonic time for in-session interval enforcement
- Maintains tamper-evident event log
- Loads secret from environment or generates securely
"""
def __init__(self, log_file="medication_log.json"):
self.log_file = Path(log_file)
self.minimum_interval_seconds = 3 * 3600
self.max_daily_doses = 6
# Load or generate secure device secret
self.device_secret = self._get_or_create_secret()
# Track monotonic time for current session only
self.session_start_monotonic = time.monotonic()
self.session_start_utc = datetime.now(timezone.utc)
# Load existing event log
self.event_log = self._load_log()
def _get_or_create_secret(self):
"""Get secret from environment or generate securely"""
secret = os.environ.get('MEDICATION_DISPENSER_SECRET')
if secret:
return secret.encode('utf-8')
# For demo purposes, generate random secret
# In production: use hardware security module or secure key storage
secret = os.urandom(32)
print("Warning: Using randomly generated secret. Set MEDICATION_DISPENSER_SECRET environment variable for production.")
return secret
def _compute_hmac(self, message):
"""Compute HMAC-SHA256 of message using device secret"""
return hmac.new(
self.device_secret,
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
def _load_log(self):
"""Load and verify existing event log"""
if not self.log_file.exists():
return []
try:
with open(self.log_file, 'r') as f:
events = json.load(f)
# Verify chain integrity
for i, event in enumerate(events):
# Reconstruct the message that was signed
event_copy = event.copy()
stored_hmac = event_copy.pop('hmac')
event_string = json.dumps(event_copy, sort_keys=True)
expected_hmac = self._compute_hmac(event_string)
if expected_hmac != stored_hmac:
raise ValueError(f"HMAC verification failed at event {i}")
return events
except Exception as e:
print(f"Error loading log: {e}")
return []
def _save_log(self):
"""Persist event log to disk"""
with open(self.log_file, 'w') as f:
json.dump(self.event_log, f, indent=2)
def _create_event(self, event_type, success, reason=""):
"""Create a tamper-evident event record with proper HMAC"""
# Use UTC time for persistence across restarts
utc_now = datetime.now(timezone.utc)
event = {
'index': len(self.event_log),
'type': event_type,
'success': success,
'reason': reason,
'utc_timestamp': utc_now.isoformat(),
'previous_hmac': self.event_log[-1]['hmac'] if self.event_log else "genesis"
}
# Create HMAC of event data (excluding the HMAC itself)
event_string = json.dumps(event, sort_keys=True)
event['hmac'] = self._compute_hmac(event_string)
return event
def _get_utc_now(self):
"""Get current UTC time as timezone-aware datetime"""
return datetime.now(timezone.utc)
def request_dose(self):
"""Request medication with complete audit trail"""
utc_now = self._get_utc_now()
# Get successful dose events
successful_doses = [e for e in self.event_log
if e['type'] == 'DOSE_REQUEST' and e['success']]
# Check interval constraint using most recent dose
if successful_doses:
last_dose_time = datetime.fromisoformat(
successful_doses[-1]['utc_timestamp']
)
elapsed = (utc_now - last_dose_time).total_seconds()
if elapsed < self.minimum_interval_seconds:
event = self._create_event(
'DOSE_REQUEST', False,
f"Interval violation: {elapsed:.0f}s < {self.minimum_interval_seconds}s"
)
self.event_log.append(event)
self._save_log()
return f"DENIED: Wait {(self.minimum_interval_seconds - elapsed) // 60:.0f} minutes"
# Check Daily limit = last 24 hours, not calendar day midnight reset
cutoff = utc_now.timestamp() - (24 * 3600)
recent_doses = [
e for e in successful_doses
if datetime.fromisoformat(e['utc_timestamp']).timestamp() > cutoff
]
if len(recent_doses) >= self.max_daily_doses:
event = self._create_event(
'DOSE_REQUEST', False,
f"Daily limit reached: {len(recent_doses)}/{self.max_daily_doses}"
)
self.event_log.append(event)
self._save_log()
return "DENIED: Daily limit reached"
# Success - dispense medication
event = self._create_event('DOSE_REQUEST', True, "Dose dispensed")
self.event_log.append(event)
self._save_log()
return "DISPENSED: Medication delivered"
def audit_log(self, num_events=5):
"""Show recent events for debugging or medical review"""
for event in self.event_log[-num_events:]:
status = "✓" if event['success'] else "✗"
timestamp = datetime.fromisoformat(event['utc_timestamp'])
local_time = timestamp.astimezone()
print(f"{status} {local_time.strftime('%Y-%m-%d %H:%M:%S %Z')}: {event['reason']}")
def verify_integrity(self):
"""Verify the entire event chain hasn't been tampered with"""
try:
for i, event in enumerate(self.event_log):
event_copy = event.copy()
stored_hmac = event_copy.pop('hmac')
event_string = json.dumps(event_copy, sort_keys=True)
expected_hmac = self._compute_hmac(event_string)
if expected_hmac != stored_hmac:
return False, f"HMAC mismatch at event {i}"
return True, "Event log integrity verified"
except Exception as e:
return False, f"Verification error: {e}"
# TEST SCRIPT - Run this to see it in action!
if __name__ == "__main__":
print("=== TESTING SECURE MEDICATION DISPENSER ===\n")
# Create a dispenser with shorter intervals for testing
dispenser = SecureMedicationDispenser("test_medication_log.json")
dispenser.minimum_interval_seconds = 10 # Override for quick testing
print("Test 1: First dose (should work)")
print(f"Result: {dispenser.request_dose()}\n")
print("Test 2: Immediate second dose (should fail)")
print(f"Result: {dispenser.request_dose()}\n")
print("Test 3: Grace tries to change system time...")
print("(Note: This won't help - we use UTC timestamps with HMAC!)")
print(f"Result: {dispenser.request_dose()}\n")
print("Waiting 10 seconds for next valid dose...")
time.sleep(10)
print("Test 4: After waiting (should work)")
print(f"Result: {dispenser.request_dose()}\n")
print("\n=== AUDIT LOG ===")
dispenser.audit_log()
print("\n=== TESTING DAILY LIMIT ===")
print("Simulating reaching daily limit...")
# Create new dispenser with 1-second intervals for quick testing
quick_dispenser = SecureMedicationDispenser("test_daily_limit.json")
quick_dispenser.minimum_interval_seconds = 1
quick_dispenser.max_daily_doses = 3 # Lower limit for testing
for i in range(5):
result = quick_dispenser.request_dose()
print(f"Dose attempt {i+1}: {result}")
if i < 4: # Don't wait after last attempt
time.sleep(1.1)
print("\n=== AUDIT LOG FOR DAILY LIMIT TEST ===")
quick_dispenser.audit_log()
print("\n=== VERIFY TAMPER DETECTION ===")
is_valid, message = quick_dispenser.verify_integrity()
print(f"Integrity check: {message}")
print("\nEvent chain (each HMAC depends on previous):")
for event in quick_dispenser.event_log[-3:]:
print(f"Event {event['index']}: HMAC={event['hmac'][:16]}...")
# Cleanup test files
import os
for f in ["test_medication_log.json", "test_daily_limit.json"]:
if os.path.exists(f):
os.remove(f)