Smart Contract Two-Factor Authentication
A comprehensive technical guide to understanding and implementing blockchain-based 2FA with honeypot security mechanisms.
Introduction
HSC-2FA (Honeytoken Driven Smart Contract 2FA) is a proof-of-concept demonstration that combines two powerful security concepts:
- Two-Factor Authentication (2FA): Requiring two different forms of identification to access a resource
- Honeytokens: Decoy credentials that trigger alerts when used, detecting unauthorized access attempts
By leveraging blockchain technology, HSC-2FA creates a tamper-proof authentication system where every attempt is permanently recorded and immediately detectable if a honeytoken is selected.
Key Features
- Blockchain-Enforced 2FA: Authentication flows execute via Ethereum smart contracts
- Honeytoken Security: Randomized decoy options mixed with real choices in each session
- CLI-First Design: Pure terminal interface for sysadmins and engineers
- Instant Alerts: Real-time notifications when honeytokens are selected
Component Responsibilities
| Component | Technology | Purpose |
|---|---|---|
| Smart Contract | Solidity | Token management, challenge issuance, authentication validation |
| CLI Interface | Python + Web3.py | User interaction, transaction signing, result display |
| Alert Monitor | Python | Event listening, notification dispatch |
| Configuration | Python | Network settings, contract addresses, credentials |
Smart Contract Implementation
The heart of HSC-2FA is the Solidity smart contract (auth.sol) that handles all authentication logic on-chain.
Contract Structure
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract Auth {
using ECDSA for bytes32;
// Storage mappings for token management
mapping(address => bytes32[]) private realTokens;
mapping(address => bytes32[]) private honeyTokens;
mapping(address => bytes32[]) private currentChallenge;
// Events for tracking authentication attempts
event HoneytokenAlert(address indexed user, bytes32 token);
event AuthenticationSuccess(address indexed user, bytes32 token);
event TokensSet(address indexed user, bool isReal);
event ChallengeIssued(address indexed user, bytes32[] tokens);
// Contract admin for authorization
address public admin;
}Key Data Structures
The contract uses three storage mappings to manage tokens:
// Real authentication tokens (valid credentials)
mapping(address => bytes32[]) private realTokens;
// Honeytoken/decoy tokens (trigger alerts when selected)
mapping(address => bytes32[]) private honeyTokens;
// Current challenge for each user (shuffled combination)
mapping(address => bytes32[]) private currentChallenge;Token Management Functions
Setting Real Tokens (Admin Function)
function setRealTokens(address user, bytes32[] memory tokens) public onlyAdmin {
realTokens[user] = tokens;
emit TokensSet(user, true);
}This function allows the admin to assign valid authentication tokens to a user. The tokens are stored as bytes32 arrays for efficient on-chain storage and comparison.
Setting Honeytokens (Admin Function)
function setHoneyTokens(address user, bytes32[] memory tokens) public onlyAdmin {
honeyTokens[user] = tokens;
emit TokensSet(user, false);
}Similar to real tokens, honeytokens are stored separately but managed through the same interface.
Challenge Issuance
The issueChallenge function creates a shuffled challenge containing both real tokens and honeytokens:
function issueChallenge(address user) public returns (bytes32[] memory) {
// Fetch both token types
bytes32[] memory rTokens = realTokens[user];
bytes32[] memory hTokens = honeyTokens[user];
// Combine into single array
bytes32[] memory combined = new bytes32[](rTokens.length + hTokens.length);
for (uint i = 0; i < rTokens.length; i++) {
combined[i] = rTokens[i];
}
for (uint j = 0; j < hTokens.length; j++) {
combined[rTokens.length + j] = hTokens[j];
}
// Shuffle to prevent identification
bytes32[] memory shuffled = shuffleTokens(combined);
currentChallenge[user] = shuffled;
emit ChallengeIssued(user, shuffled);
return shuffled;
}Token Shuffling Algorithm
The shuffle function uses blockchain randomness to mix tokens:
function shuffleTokens(bytes32[] memory combined) internal view returns (bytes32[] memory) {
uint length = combined.length;
for (uint i = 0; i < length; i++) {
// Use block data for pseudo-randomness
uint n = i + uint(keccak256(abi.encodePacked(
block.timestamp,
block.prevrandao,
i
))) % (length - i);
// Swap elements
(combined[i], combined[n]) = (combined[n], combined[i]);
}
return combined;
}Security Note: This shuffling uses block data which is not truly random. Production systems should use Chainlink VRF or similar oracle services for verifiable randomness.
Authentication Process
The authenticate function validates the user’s selection:
function authenticate(bytes32 selectedToken, bytes memory signature) public {
// Step 1: Verify signature ownership
address signer = recoverSigner(selectedToken, signature);
if (signer != msg.sender) {
revert SignatureMismatch();
}
// Step 2: Verify token is in current challenge
bytes32[] memory challengeTokens = currentChallenge[signer];
if (!tokenExists(selectedToken, challengeTokens)) {
revert TokenNotInChallenge();
}
// Step 3: Determine if real token or honeytoken
if (tokenExists(selectedToken, honeyTokens[signer])) {
emit HoneytokenAlert(signer, selectedToken);
} else if (tokenExists(selectedToken, realTokens[signer])) {
emit AuthenticationSuccess(signer, selectedToken);
} else {
revert TokenNotRecognized();
}
}Signature Recovery
The contract includes helper functions for ECDSA signature verification:
function recoverSigner(bytes32 message, bytes memory sig) public pure returns (address) {
bytes32 ethSignedMessageHash = getEthSignedMessageHash(message);
return ethSignedMessageHash.recover(sig);
}
function getEthSignedMessageHash(bytes32 message) public pure returns (bytes32) {
return message.toEthSignedMessageHash();
}These use OpenZeppelin’s ECDSA library for secure signature verification following Ethereum’s signature standard.
CLI Authentication Flow
The Python-based CLI (cli/auth.py) provides the user interface for authentication. Here’s how it works:
Initialization and Configuration
import json
from web3 import Web3
from eth_account import Account
# Load contract ABI from JSON file
with open(ABI_PATH, 'r') as abi_file:
contract_abi = json.load(abi_file)
# Establish Web3 connection to Ethereum network
w3 = Web3(Web3.HTTPProvider(PROVIDER_URL))
# Create contract instance
contract = w3.eth.contract(address=CONTRACT_ADDRESS, abi=contract_abi)User Authentication Flow
def main():
# Step 1: Collect user credentials
user_addr = input("Enter your Ethereum address: ").strip()
pk = load_private_key() # Securely prompt for private key
acct = Account.from_key(pk)
# Verify key matches address
if acct.address.lower() != user_addr.lower():
print("Private key does not match provided address.")
sys.exit(1)Fetching and Displaying Challenge
# Step 2: Request challenge from smart contract
print("Fetching authentication challenge...")
tx = contract.functions.issueChallenge(user_addr).call()
token_opts = [w3.toHex(token) for token in tx]
# Step 3: Display options to user
selected_token_hex = select_option(token_opts)The user sees a numbered list of token options (hex strings). They don’t know which are real tokens and which are honeytokens:
[Options]
1. 0x8f4a...3b2c
2. 0x7d1e...9a4f
3. 0x2c9a...5e1d
Select option (number):
Signing the Selection
Once the user selects a token, it’s signed using their private key:
# Convert hex to bytes
selected_token_bytes = bytes.fromhex(selected_token_hex[2:])
# Create message hash and sign
message_hash = w3.solidityKeccak(['bytes32'], [selected_token_bytes])
signed = acct.sign_message(
Web3.solidityKeccak(['string'], [f"\x19Ethereum Signed Message:\n32{selected_token_bytes.hex()}"])
)
signature = signed.signatureSubmitting Authentication
# Build and sign the transaction
tx = contract.functions.authenticate(
selected_token_bytes,
signature
).build_transaction({
'from': user_addr,
'nonce': w3.eth.get_transaction_count(user_addr),
'gas': 200000,
'gasPrice': w3.eth.gas_price,
})
signed_tx = acct.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)Processing Results
# Wait for transaction receipt
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
# Check for authentication success event
logs = contract.events.AuthenticationSuccess().process_receipt(receipt)
if logs:
print("[SUCCESS] Authentication successful! Welcome.")
else:
# Check for honeytoken alert
logs = contract.events.HoneytokenAlert().process_receipt(receipt)
if logs:
print("[ALERT] Decoy/honeytoken selected! Security team notified.")
sys.exit(10) # Exit with specific code for alertAlert and Monitoring System
The alert system (alerts/monitor.py) listens for blockchain events and triggers notifications:
Event Monitoring Loop
def main():
print("[Monitor] Listening for contract events...")
last_block = w3.eth.block_number
while True:
# Poll for new blocks
latest_block = w3.eth.block_number
if latest_block > last_block:
# Check each new block for events
for block in range(last_block + 1, latest_block + 1):
# Query HoneytokenAlert events
events = contract.events.HoneytokenAlert().get_logs(
fromBlock=block,
toBlock=block
)
for event in events:
handle_honeytoken_alert(event)
# Query AuthenticationSuccess events
events = contract.events.AuthenticationSuccess().get_logs(
fromBlock=block,
toBlock=block
)
for event in events:
handle_auth_success(event)
last_block = latest_block
time.sleep(5) # Polling intervalHoneytoken Alert Handler
def handle_honeytoken_alert(event):
user = event['args']['user']
token = event['args']['token']
body = f"Honeytoken ALERT!\nUser: {user}\nToken: {Web3.toHex(token)}"
print("[Monitor] Honeytoken alert detected.")
# Send email notification
send_email_alert(
subject="Honeytoken Triggered!",
body=body,
recipient=NOTIFY_EMAIL,
smtp_server=SMTP_SERVER,
smtp_port=SMTP_PORT,
smtp_user=SMTP_USER,
smtp_password=SMTP_PASSWORD
)
# Send Slack notification
send_slack_alert(SLACK_WEBHOOK, body)Security Considerations
Important Warnings
⚠️ Production Use: This project is a proof-of-concept. Never use in production without:
- Implementing verifiable random number generation (Chainlink VRF)
- Using hardware wallets for key management
- Conducting professional security audits
Token Security
-
Private Key Management: Always keep private keys secure. Prefer hardware wallets for maximum security.
-
Honeytoken Rotation: Regularly rotate honeytoken options to prevent attackers from mapping out valid tokens over time.
-
Randomness Limitations: The current shuffling uses
block.timestampandblock.prevrandao, which are not truly random. An attacker with significant mining power could influence the shuffle.
Network Security
- Provider URLs: Use reputable RPC providers (Infura, Alchemy) with API key authentication
- HTTPS: Always use encrypted connections to blockchain nodes
- Firewall: Restrict access to monitoring and admin interfaces
Notification Security
- Credential Storage: Never commit SMTP passwords or Slack webhooks to version control
- Environment Variables: Use environment variables for sensitive configuration
- Access Control: Limit notification recipients to security personnel
Configuration
Python Dependencies
# requirements.txt
# Blockchain && Cryptography
web3>=6.0.0
eth-account>=0.9.0
# Notifications
requests>=2.25.0
# Environment and CLI
python-dotenv>=0.21.0
colorama>=0.4.4
tabulate>=0.8.9
Contract Configuration
The cli/config.py file contains network and contract settings:
# Contract deployment address on the blockchain
CONTRACT_ADDRESS = "0xYourContractAddressHere"
# Ethereum node provider (Infura, Alchemy, or local)
PROVIDER_URL = "https://goerli.infura.io/v3/your-infura-key"
# Path to compiled contract ABI
ABI_PATH = "./AuthABI.json"Alert Configuration
In alerts/monitor.py, configure notification settings:
# Email notification settings
NOTIFY_EMAIL = "admin@example.com"
SMTP_SERVER = "smtp.example.com"
SMTP_PORT = 465
SMTP_USER = "your@email.com"
SMTP_PASSWORD = "your-smtp-password"
# Slack webhook URL
SLACK_WEBHOOK = "https://hooks.slack.com/services/XXXX/YYYY/ZZZZ"Conclusion
HSC-2FA demonstrates an innovative approach to two-factor authentication by combining:
- Blockchain immutability for tamper-proof audit trails
- Honeytoken technology for detecting unauthorized access attempts
- Smart contract automation for trustless authentication
- Real-time alerting for immediate incident response
While currently a proof-of-concept, this architecture provides a foundation for building more sophisticated decentralized authentication systems. The key innovations—randomized challenges and immediate alerting—could be adapted to various security-sensitive applications.
Future Enhancements
Potential improvements for production deployment:
- Chainlink VRF for verifiable randomness
- Multi-sig support for enterprise deployments
- Rate limiting to prevent brute-force attacks
- Time-based one-time password (TOTP) integration
- Hardware wallet support (Ledger, Trezor)