Medical dispenser - Don’t trust the user  - Dr. Ryland Grace hacked his ship's medication system by changing the clock. We'll build three Python dispensers—from vulnerable to bulletproof—showing why trusting system time is dangerous.

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)