Skip to content

Instantly share code, notes, and snippets.

@kodyabbott
Created February 27, 2025 04:21
Show Gist options
  • Save kodyabbott/b9210818df8bf50977dfecdbbf35fd20 to your computer and use it in GitHub Desktop.
Save kodyabbott/b9210818df8bf50977dfecdbbf35fd20 to your computer and use it in GitHub Desktop.
Simplified OPAQUE PAKE implementation (educational purposes only)
"""
OPAQUE Protocol Implementation Sketch (Educational Use Only)
Based on draft-irtf-cfrg-opaque (The OPAQUE Augmented PAKE Protocol)
This is a simplified implementation for educational purposes,
not suitable for production use.
"""
import os
import hmac
import hashlib
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
# Configuration
HASH_LEN = 32 # SHA-256 output length
OPRF_SEED_LEN = 32
NONCE_LEN = 32
# Simplified implementation for educational purposes
class OPAQUE:
def __init__(self):
# In a real implementation, this would be securely stored
self.server_private_key = os.urandom(32)
def register_user(self, username, password):
"""Register a new user with the OPAQUE protocol"""
# 1. Server generates a random OPRF seed for this user
oprf_seed = os.urandom(OPRF_SEED_LEN)
# 2. Client and server engage in OPRF protocol
# (In reality, this would be an interactive protocol)
blind, blinded_element = self._client_blind(password)
evaluated_element = self._server_evaluate(oprf_seed, blinded_element)
oprf_output = self._client_finalize(password, blind, evaluated_element)
# 3. Client derives envelope encryption key
envelope_key = self._derive_key(oprf_output, b"EnvelopeKey")
# 4. Client creates an "envelope" that protects their authentication data
# In a real implementation, this would include client credentials
auth_data = os.urandom(32) # Simulated auth data
envelope = self._create_envelope(envelope_key, auth_data)
# 5. Server stores user record
user_record = {
"username": username,
"oprf_seed": oprf_seed,
"envelope": envelope,
# No password-equivalent material is stored
}
return user_record
def authenticate(self, username, password, user_record):
"""Authenticate a user with the OPAQUE protocol"""
# 1. Retrieve user record
oprf_seed = user_record["oprf_seed"]
envelope = user_record["envelope"]
# 2. Client and server engage in OPRF protocol
blind, blinded_element = self._client_blind(password)
evaluated_element = self._server_evaluate(oprf_seed, blinded_element)
oprf_output = self._client_finalize(password, blind, evaluated_element)
# 3. Client derives envelope encryption key
envelope_key = self._derive_key(oprf_output, b"EnvelopeKey")
# 4. Client attempts to open the envelope
try:
auth_data = self._open_envelope(envelope_key, envelope)
# If we reach here, authentication succeeded
# 5. Derive session key (both parties can derive the same key)
session_key = self._derive_key(oprf_output, b"SessionKey")
return True, session_key
except Exception:
# Authentication failed
return False, None
# OPRF operations (simplified for clarity)
def _client_blind(self, password):
"""Client blinds the password"""
# Use a deterministic blind derived from password for this simplified demo
# In a real implementation, this would be random and use proper group operations
blind = hashlib.sha256(b"blind_seed" + password.encode()).digest()
blinded_element = hmac.new(blind, password.encode(), hashlib.sha256).digest()
return blind, blinded_element
def _server_evaluate(self, oprf_seed, blinded_element):
"""Server evaluates the OPRF on the blinded element"""
# In a real implementation, this would use a proper group operation
return hmac.new(oprf_seed, blinded_element, hashlib.sha256).digest()
def _client_finalize(self, password, blind, evaluated_element):
"""Client finalizes the OPRF computation"""
# In a real implementation, this would unblind using proper group operations
# This is a simplified version - using password as additional input to ensure
# the same password produces the same result
oprf_output = hmac.new(blind, evaluated_element + password.encode(), hashlib.sha256).digest()
return oprf_output
# Key derivation and envelope operations
def _derive_key(self, input_keying_material, info):
"""Derive a key using HKDF"""
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=info,
)
return hkdf.derive(input_keying_material)
def _create_envelope(self, envelope_key, auth_data):
"""Create an encrypted envelope containing auth data"""
nonce = os.urandom(NONCE_LEN)
# In a real implementation, this would use authenticated encryption
# This is simplified for clarity
ciphertext = bytes([a ^ b for a, b in zip(auth_data, envelope_key)])
auth_tag = hmac.new(envelope_key, nonce + ciphertext, hashlib.sha256).digest()
return {
"nonce": nonce,
"ciphertext": ciphertext,
"auth_tag": auth_tag
}
def _open_envelope(self, envelope_key, envelope):
"""Open and verify an envelope"""
nonce = envelope["nonce"]
ciphertext = envelope["ciphertext"]
auth_tag = envelope["auth_tag"]
# Verify the envelope's authenticity
expected_tag = hmac.new(envelope_key, nonce + ciphertext, hashlib.sha256).digest()
if not hmac.compare_digest(auth_tag, expected_tag):
raise ValueError("Invalid envelope authentication tag")
# Decrypt the data
auth_data = bytes([a ^ b for a, b in zip(ciphertext, envelope_key)])
return auth_data
# Example usage
if __name__ == "__main__":
opaque = OPAQUE()
# User registration
username = "alice"
password = "secure-password"
user_record = opaque.register_user(username, password)
print(f"User {username} registered successfully")
# Authentication with correct password
success, session_key = opaque.authenticate(username, password, user_record)
print(f"Authentication {'successful' if success else 'failed'}")
# Authentication with incorrect password
wrong_password = "wrong-password"
success, session_key = opaque.authenticate(username, wrong_password, user_record)
print(f"Authentication with wrong password: {'successful' if success else 'failed'}")
@kodyabbott
Copy link
Author

In the implementation I provided, you can see how it follows the structure outlined in the RFC, though in a simplified manner:

Setup

The RFC describes server setup with server_private_key, server_public_key, and an oprf_seed. In my code:

def __init__(self):
    # In a real implementation, this would be securely stored
    self.server_private_key = os.urandom(32)

Then for each user:

# 1. Server generates a random OPRF seed for this user
oprf_seed = os.urandom(OPRF_SEED_LEN)

Registration

The RFC describes registration needing server authentication, exchange of messages, and resulting in:

  1. Client receiving an export_key
  2. Server storing a record

In my implementation:

def register_user(self, username, password):
    """Register a new user with the OPAQUE protocol"""
    # 1. Server generates a random OPRF seed for this user
    oprf_seed = os.urandom(OPRF_SEED_LEN)
    
    # 2. Client and server engage in OPRF protocol
    # (In reality, this would be an interactive protocol)
    blind, blinded_element = self._client_blind(password)
    evaluated_element = self._server_evaluate(oprf_seed, blinded_element)
    oprf_output = self._client_finalize(password, blind, evaluated_element)
    
    # 3. Client derives envelope encryption key
    envelope_key = self._derive_key(oprf_output, b"EnvelopeKey")
    
    # 4. Client creates an "envelope" that protects their authentication data
    # In a real implementation, this would include client credentials
    auth_data = os.urandom(32)  # Simulated auth data
    envelope = self._create_envelope(envelope_key, auth_data)
    
    # 5. Server stores user record
    user_record = {
        "username": username,
        "oprf_seed": oprf_seed,
        "envelope": envelope,
        # No password-equivalent material is stored
    }
    
    return user_record

The major simplification in my code is that the registration protocol exchange (RegistrationRequest, RegistrationResponse, RegistrationRecord) is collapsed into a single function call rather than showing the actual message passing between client and server.

In a full implementation, you would see:

  1. The client sending a blinded password value (RegistrationRequest)
  2. The server responding with its evaluation (RegistrationResponse)
  3. The client creating and sending back an envelope (RegistrationRecord)

My implementation also doesn't explicitly return an export_key to the client, which is mentioned in the RFC as a potential output for encryption of additional data.

For a more directly RFC-compliant implementation, you would need to:

  1. Implement the actual request/response protocol
  2. Include the AKE key exchange functionality
  3. Return the export_key
  4. Follow the exact message formats specified in Section 5.1

@kodyabbott
Copy link
Author

Here are key elements from the RFC that are missing or simplified in my code:

  1. Online Authenticated Key Exchange (3.3): The full OPAQUE protocol includes a complete authenticated key exchange phase, which is only partially implemented in my code (it creates a session key but doesn't include the full AKE protocol).

  2. Envelope Structure (4.1.1): The RFC specifies a particular structure for the envelope, whereas my code uses a simplified version.

  3. 3DH Protocol (6.4): The code doesn't implement the three Diffie-Hellman exchanges that are part of the full OPAQUE specification.

  4. Message Formats (5.1, 6.1): The proper wire formats for messages aren't implemented.

  5. Key Schedule Functions (6.4.2): The specific key derivation schedule is simplified.

The implementation I provided is more of a conceptual sketch showing the core OPAQUE principles (OPRF + envelope encryption) rather than a complete implementation of the RFC. It demonstrates the password-hardening and zero-knowledge aspects but doesn't include the full authenticated key exchange.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment