- Created comprehensive design document (docs/CHANNEL_LOCK_DESIGN.md) - Complete UX flows with PIN setup, unlock, and auto-lock modes - Technical implementation details with code examples - Security architecture (PBKDF2, brute force protection) - Database schema changes and migration plan - Testing strategy and implementation checklist - Added to ROADMAP.md as Phase 3, Item #1 (HIGH PRIORITY) - Target: October 2025 implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
26 KiB
TeleBot Channel Lock & PIN Protection Design
Status: 📋 Documented - Ready for Implementation Priority: 🔒 HIGH - Security & Privacy Enhancement Target: Q4 2025 (October 2025) Estimated Implementation: 2-3 hours core functionality + 1-2 hours UX polish Last Updated: October 6, 2025
Executive Summary
Complete channel lock system for TeleBot that provides enterprise-grade privacy protection through a 4-digit PIN. When enabled, the entire Telegram conversation is locked with a single security gate, requiring PIN entry to access any functionality beyond the unlock screen.
Key Benefits
✅ Single Security Boundary - One gate protects all features, not selective locks ✅ Screenshot Safe - Locked screen shows no sensitive data ✅ Shared Device Safe - Complete protection for shared/public devices ✅ Mobile Banking UX - Familiar pattern from banking/crypto wallet apps ✅ No Data Loss - All data preserved in session, just visibility controlled ✅ Privacy by Design - User controls when and how data is visible ✅ Simple Implementation - Early gate check, minimal code complexity
Architecture Overview
Core Concept
┌─────────────────────────────────────────┐
│ ALL TELEGRAM UPDATES │
│ (Messages, Callbacks, Commands) │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────┐
│ SECURITY GATE │
│ Is session locked? │
└─────────┬───────────┘
│
┌─────────┴──────────┐
│ │
▼ ▼
🔒 LOCKED ✅ UNLOCKED
Show unlock Process normally
screen only (full features)
State Model
public enum SessionState
{
Guest, // No PIN set, full access (backward compatible with existing users)
Unlocked, // PIN set, currently unlocked (full access)
Locked // PIN set, currently locked (GATE BLOCKS ALL ACTIONS)
}
User Experience Design
1. First-Time Setup (Optional)
┌─────────────────────────────────────┐
│ 🔐 Secure Your Orders │
├─────────────────────────────────────┤
│ Protect your personal information │
│ and order history with a 4-digit │
│ PIN code. │
│ │
│ • Lock chat automatically │
│ • Protect payment details │
│ • Secure order history │
│ │
│ [✅ Set Up PIN] [⏭️ Skip] │
└─────────────────────────────────────┘
Design Decisions:
- Completely optional - existing users continue with Guest mode
- Clear value proposition - explain what PIN protects
- No friction - skip option for users who don't need it
- Re-prompt after 3 orders or 7 days (gentle nudge)
2. PIN Setup Flow
Step 1: Enter New PIN
┌─────────────────────────────────────┐
│ 🔢 Create Your PIN │
├─────────────────────────────────────┤
│ PIN: ● ● ● ● │
│ │
│ [1] [2] [3] │
│ [4] [5] [6] │
│ [7] [8] [9] │
│ [⬅️ Clear] [0] [✅ Next] │
└─────────────────────────────────────┘
Step 2: Confirm PIN
┌─────────────────────────────────────┐
│ 🔢 Confirm Your PIN │
├─────────────────────────────────────┤
│ Enter your PIN again to confirm │
│ │
│ PIN: ● ● ● ● │
│ │
│ [Numeric keypad as above] │
└─────────────────────────────────────┘
Step 3: Success
┌─────────────────────────────────────┐
│ ✅ PIN Protection Enabled │
├─────────────────────────────────────┤
│ Your chat is now protected with │
│ a secure PIN. │
│ │
│ Auto-lock: 🏠 On Main Menu │
│ │
│ [⚙️ Settings] [🏠 Main Menu] │
└─────────────────────────────────────┘
3. Locked State (Primary UX)
┌─────────────────────────────────────┐
│ 🔒 Channel Locked │
├─────────────────────────────────────┤
│ Enter your 4-digit PIN to continue │
│ │
│ Locked at: 14:32 │
│ │
│ [🔓 Unlock] │
└─────────────────────────────────────┘
Important: This is the ONLY message shown when locked. No other features accessible.
4. Unlock Flow
┌─────────────────────────────────────┐
│ 🔢 Enter PIN │
├─────────────────────────────────────┤
│ PIN: ● ● ○ ○ │
│ │
│ Attempts remaining: 3 │
│ │
│ [1] [2] [3] │
│ [4] [5] [6] │
│ [7] [8] [9] │
│ [⬅️ Clear] [0] [✅ Submit] │
└─────────────────────────────────────┘
After Successful Unlock:
┌─────────────────────────────────────┐
│ ✅ Unlocked │
├─────────────────────────────────────┤
│ Welcome back! │
│ │
│ [Returns to normal menu] │
└─────────────────────────────────────┘
Failed Attempt:
┌─────────────────────────────────────┐
│ ❌ Incorrect PIN │
├─────────────────────────────────────┤
│ PIN: ○ ○ ○ ○ │
│ │
│ Attempts remaining: 2 │
│ ⚠️ Account locks after 3 failures │
│ │
│ [Numeric keypad] │
└─────────────────────────────────────┘
Account Locked (after 3 failures):
┌─────────────────────────────────────┐
│ 🚫 Account Temporarily Locked │
├─────────────────────────────────────┤
│ Too many failed attempts. │
│ │
│ Try again in: 5 minutes │
│ │
│ [❓ Forgot PIN?] │
└─────────────────────────────────────┘
5. Auto-Lock Modes
⚙️ Security Settings
├── 🔐 PIN Protection: ✅ Enabled
│ ├── 📌 Change PIN
│ └── ❌ Disable PIN (requires current PIN)
│
├── 🔒 Auto-Lock: 🏠 On Main Menu ▼
│ │
│ ├── ✋ Manual Only
│ │ └── Lock only when "🔒 Lock" button clicked
│ │
│ ├── 🏠 On Main Menu (Recommended)
│ │ └── Auto-lock when returning to main menu
│ │
│ ├── ⏰ After Inactivity
│ │ └── Lock after [30 ▼] minutes idle
│ │
│ ├── 🚪 On Exit
│ │ └── Lock on any "back to menu" action
│ │
│ └── 🔐 Always (Paranoid Mode)
│ └── Lock after EVERY single action
│
├── 🗑️ Clear All Data (requires PIN)
│ └── Permanently delete order history, cart, settings
│
└── ℹ️ Security Info
├── PIN Last Changed: Oct 6, 2025
├── Last Locked: 2 hours ago
└── Failed Attempts Today: 0
Technical Implementation
1. Database Schema Changes
public class UserSession
{
// Existing fields...
public Guid Id { get; set; }
public long TelegramUserId { get; set; }
public Cart Cart { get; set; }
public List<ConversationMessage> Conversation { get; set; }
// NEW: Security & Lock State
public SessionState State { get; set; } = SessionState.Guest;
public string? PinHash { get; set; } // PBKDF2 hash, null = no PIN
public DateTime? LastActivityAt { get; set; }
public DateTime? LockedAt { get; set; }
public int FailedPinAttempts { get; set; }
public DateTime? LockoutUntil { get; set; }
public SecuritySettings Security { get; set; } = new();
public string? PinEntry { get; set; } // Temporary, in-memory only (not persisted)
}
public class SecuritySettings
{
public bool PinEnabled { get; set; }
public AutoLockMode AutoLock { get; set; } = AutoLockMode.OnMainMenu;
public int AutoLockMinutes { get; set; } = 30;
public DateTime? PinCreatedAt { get; set; }
public DateTime? PinLastChangedAt { get; set; }
}
public enum AutoLockMode
{
Manual, // Only lock when user clicks button
OnMainMenu, // Lock when returning to main menu (default)
OnInactivity, // Lock after X minutes idle
OnExit, // Lock on any "back to menu" action
Always // Lock after every single action (paranoid mode)
}
2. Security Gate Implementation
// CallbackHandler.cs - Primary entry point for all user interactions
public async Task Handle(ITelegramBotClient bot, CallbackQuery callbackQuery, CancellationToken ct)
{
var telegramUser = callbackQuery.From;
var session = await _sessionManager.GetOrCreateSessionAsync(telegramUser.Id);
// UPDATE ACTIVITY TIMESTAMP
session.LastActivityAt = DateTime.UtcNow;
// SECURITY GATE - Check lock state BEFORE any other processing
if (session.State == SessionState.Locked && !IsUnlockAction(callbackQuery.Data))
{
await ShowUnlockScreen(bot, callbackQuery);
return; // STOP ALL PROCESSING - gate closed
}
// Check if account is in lockout period
if (session.LockoutUntil.HasValue && DateTime.UtcNow < session.LockoutUntil.Value)
{
await ShowLockoutScreen(bot, callbackQuery, session.LockoutUntil.Value);
return;
}
// If unlocked or this is unlock action, continue normal flow
await ProcessCallback(bot, callbackQuery, session);
// CHECK AUTO-LOCK CONDITIONS after action completes
await CheckAndApplyAutoLock(session);
}
private bool IsUnlockAction(string? callbackData)
{
return callbackData?.StartsWith("unlock") == true ||
callbackData?.StartsWith("pin_entry:") == true ||
callbackData == "show_pin_pad" ||
callbackData == "pin_clear" ||
callbackData?.StartsWith("pin_digit:") == true;
}
3. PIN Management
// PIN Hashing (secure storage)
public string HashPin(string pin)
{
using var rfc2898 = new Rfc2898DeriveBytes(
pin,
saltSize: 32,
iterations: 100_000,
HashAlgorithmName.SHA256
);
var hash = Convert.ToBase64String(rfc2898.GetBytes(32));
var salt = Convert.ToBase64String(rfc2898.Salt);
return $"{salt}:{hash}"; // Store both salt and hash
}
// PIN Verification
public bool VerifyPin(string enteredPin, string storedHash)
{
var parts = storedHash.Split(':');
if (parts.Length != 2) return false;
var salt = Convert.FromBase64String(parts[0]);
var hash = Convert.FromBase64String(parts[1]);
using var rfc2898 = new Rfc2898DeriveBytes(
enteredPin,
salt,
iterations: 100_000,
HashAlgorithmName.SHA256
);
var enteredHash = rfc2898.GetBytes(32);
return CryptographicOperations.FixedTimeEquals(hash, enteredHash);
}
// PIN Entry Handling
private async Task HandlePinDigit(ITelegramBotClient bot, CallbackQuery query, UserSession session, string digit)
{
if (session.PinEntry == null)
session.PinEntry = "";
if (digit == "clear")
{
session.PinEntry = "";
}
else if (digit == "submit" && session.PinEntry.Length == 4)
{
await HandlePinSubmit(bot, query, session);
return;
}
else if (session.PinEntry.Length < 4)
{
session.PinEntry += digit;
}
// Refresh PIN pad with updated dots
await ShowPinPad(bot, query, session);
}
private async Task HandlePinSubmit(ITelegramBotClient bot, CallbackQuery query, UserSession session)
{
if (VerifyPin(session.PinEntry, session.PinHash!))
{
// SUCCESS
session.State = SessionState.Unlocked;
session.FailedPinAttempts = 0;
session.PinEntry = null;
await bot.EditMessageTextAsync(
query.Message!.Chat.Id,
query.Message.MessageId,
"✅ *Unlocked*\n\nWelcome back!",
parseMode: ParseMode.Markdown,
replyMarkup: MenuBuilder.MainMenu()
);
}
else
{
// FAILURE
session.FailedPinAttempts++;
session.PinEntry = null;
if (session.FailedPinAttempts >= 3)
{
// LOCKOUT
session.LockoutUntil = DateTime.UtcNow.AddMinutes(5);
await ShowLockoutScreen(bot, query, session.LockoutUntil.Value);
}
else
{
await ShowPinPad(bot, query, session, isError: true);
}
}
await _sessionManager.SaveSessionAsync(session);
}
4. Auto-Lock Logic
private async Task CheckAndApplyAutoLock(UserSession session)
{
// Don't lock if PIN not enabled or already locked
if (!session.Security.PinEnabled || session.State == SessionState.Locked)
return;
bool shouldLock = session.Security.AutoLock switch
{
AutoLockMode.Manual => false, // Never auto-lock
AutoLockMode.OnMainMenu =>
session.CurrentMenu == "main" || session.LastAction == "menu",
AutoLockMode.OnInactivity =>
(DateTime.UtcNow - session.LastActivityAt.GetValueOrDefault()).TotalMinutes
>= session.Security.AutoLockMinutes,
AutoLockMode.OnExit =>
session.CurrentMenu == "main" ||
session.LastAction == "back" ||
session.LastAction == "menu",
AutoLockMode.Always => true, // Lock after every action
_ => false
};
if (shouldLock)
{
session.State = SessionState.Locked;
session.LockedAt = DateTime.UtcNow;
session.PinEntry = null; // Clear any temporary PIN entry
await _sessionManager.SaveSessionAsync(session);
_logger.LogInformation(
"Session {SessionId} auto-locked (mode: {Mode})",
session.Id,
session.Security.AutoLock
);
}
}
5. UI Components
// MenuBuilder.cs additions
public InlineKeyboardMarkup UnlockScreen()
{
return new InlineKeyboardMarkup(new[]
{
new[]
{
InlineKeyboardButton.WithCallbackData("🔓 Unlock", "show_pin_pad")
}
});
}
public InlineKeyboardMarkup PinPad(UserSession session, bool isError = false)
{
var maskLength = session.PinEntry?.Length ?? 0;
var mask = new string('●', maskLength) + new string('○', 4 - maskLength);
return new InlineKeyboardMarkup(new[]
{
new[]
{
InlineKeyboardButton.WithCallbackData("1", "pin_digit:1"),
InlineKeyboardButton.WithCallbackData("2", "pin_digit:2"),
InlineKeyboardButton.WithCallbackData("3", "pin_digit:3")
},
new[]
{
InlineKeyboardButton.WithCallbackData("4", "pin_digit:4"),
InlineKeyboardButton.WithCallbackData("5", "pin_digit:5"),
InlineKeyboardButton.WithCallbackData("6", "pin_digit:6")
},
new[]
{
InlineKeyboardButton.WithCallbackData("7", "pin_digit:7"),
InlineKeyboardButton.WithCallbackData("8", "pin_digit:8"),
InlineKeyboardButton.WithCallbackData("9", "pin_digit:9")
},
new[]
{
InlineKeyboardButton.WithCallbackData("⬅️ Clear", "pin_clear"),
InlineKeyboardButton.WithCallbackData("0", "pin_digit:0"),
InlineKeyboardButton.WithCallbackData("✅ Submit", "pin_submit")
}
});
}
// Add lock button to main menu when PIN enabled
public InlineKeyboardMarkup MainMenu(UserSession? session = null)
{
var buttons = new List<InlineKeyboardButton[]>
{
// ... existing menu items
};
// Add lock button if PIN is enabled
if (session?.Security.PinEnabled == true && session.State == SessionState.Unlocked)
{
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData("🔒 Lock Chat", "lock_channel")
});
}
return new InlineKeyboardMarkup(buttons);
}
Security Considerations
1. PIN Storage
- ✅ PBKDF2 with 100,000 iterations (OWASP recommended)
- ✅ 32-byte random salt per PIN
- ✅ SHA-256 hash algorithm
- ✅ Constant-time comparison (prevents timing attacks)
- ❌ Never store PIN in plaintext
2. Brute Force Protection
- ✅ 3 attempts before 5-minute lockout
- ✅ Progressive lockout (future: 5min → 15min → 1hr → 24hr)
- ✅ Failed attempt logging for audit trail
- ✅ Rate limiting on unlock endpoint
3. Session Security
- ✅ PinEntry stored in memory only, never persisted to database
- ✅ Auto-clear PinEntry on lock/unlock/failure
- ✅ Session state synchronized across all interactions
- ✅ Activity timestamp updated on every interaction
4. Data Protection
- ✅ All sensitive data (orders, payments, addresses) hidden when locked
- ✅ No data exposed in locked state messages
- ✅ Session data preserved but inaccessible
- ✅ Optional "Clear All Data" requires PIN verification
Migration & Backward Compatibility
Existing Users (Guest Mode)
- No disruption - continue using bot without PIN
- Optional prompt after 3 orders or 7 days: "Would you like to secure your orders with a PIN?"
- Can enable PIN anytime from Settings menu
Database Migration
-- Add new columns to UserSessions table
ALTER TABLE UserSessions ADD COLUMN State INTEGER DEFAULT 0; -- 0 = Guest
ALTER TABLE UserSessions ADD COLUMN PinHash TEXT NULL;
ALTER TABLE UserSessions ADD COLUMN LastActivityAt TEXT NULL;
ALTER TABLE UserSessions ADD COLUMN LockedAt TEXT NULL;
ALTER TABLE UserSessions ADD COLUMN FailedPinAttempts INTEGER DEFAULT 0;
ALTER TABLE UserSessions ADD COLUMN LockoutUntil TEXT NULL;
ALTER TABLE UserSessions ADD COLUMN SecuritySettings TEXT NULL; -- JSON
-- Add indexes for performance
CREATE INDEX IX_UserSessions_State ON UserSessions(State);
CREATE INDEX IX_UserSessions_LockoutUntil ON UserSessions(LockoutUntil);
Testing Strategy
Unit Tests
- ✅ PIN hashing and verification
- ✅ Auto-lock mode logic
- ✅ Lockout calculation
- ✅ PIN entry validation (4 digits only)
Integration Tests
- ✅ Full unlock flow (locked → pin entry → unlocked)
- ✅ Failed attempt lockout enforcement
- ✅ Auto-lock triggers (menu, inactivity, exit)
- ✅ State transitions (Guest → Unlocked → Locked)
E2E Tests (Playwright)
- ✅ First-time PIN setup flow
- ✅ Lock and unlock via menu
- ✅ Auto-lock on main menu return
- ✅ Failed attempts and lockout
- ✅ PIN change workflow
- ✅ Disable PIN and return to Guest mode
Implementation Checklist
Phase 1: Core Functionality (2-3 hours)
- Add
SessionState,PinHash, security fields toUserSessionmodel - Create database migration for new columns
- Implement
HashPin()andVerifyPin()methods - Add security gate at top of
CallbackHandler.Handle() - Create
ShowUnlockScreen()method - Build PIN pad UI in
MenuBuilder.PinPad() - Implement
HandlePinDigit()andHandlePinSubmit() - Add lockout logic (3 attempts → 5 min timeout)
- Implement manual lock button in main menu
Phase 2: Auto-Lock (1 hour)
- Create
CheckAndApplyAutoLock()method - Implement all 5 auto-lock modes
- Add activity timestamp tracking
- Test auto-lock triggers
Phase 3: Settings & Management (1 hour)
- Create Security Settings menu
- Implement PIN setup flow (new users)
- Implement Change PIN flow
- Implement Disable PIN flow (requires current PIN)
- Add auto-lock mode selector
- Add "Clear All Data" with PIN verification
Phase 4: UX Polish (1-2 hours)
- Add first-time setup prompt (optional, after 3 orders)
- Improve error messages and feedback
- Add security info display (last locked, failed attempts)
- Add countdown timer for lockout screen
- Implement "Forgot PIN?" recovery flow (contact support)
- Add haptic/visual feedback for PIN entry
Phase 5: Testing & Deployment (1 hour)
- Write unit tests for PIN security
- Write integration tests for lock flows
- Manual E2E testing on Telegram
- Security review (penetration testing)
- Deploy to staging environment
- User acceptance testing
- Deploy to production with feature flag
Future Enhancements
Short Term (Q1 2026)
- Biometric Unlock: Support Telegram's WebApp biometric API
- PIN Complexity: Option for 6-digit PIN or alphanumeric password
- Session Management: "Lock all devices" from any session
- Audit Log: View all unlock events and failed attempts
Medium Term (Q2 2026)
- 2FA Support: Optional TOTP/authenticator app second factor
- Trusted Devices: Remember devices, require PIN only on new devices
- Emergency Contacts: Designate trusted contact for PIN recovery
- Secure Backup: Encrypted backup of order history with recovery phrase
Long Term (Q3-Q4 2026)
- Hardware Security: Support for hardware security keys (YubiKey, etc.)
- Zero-Knowledge Encryption: End-to-end encrypted order data
- Multi-Account: Separate PINs for business vs personal shopping
- Compliance: FIPS 140-2 certification for payment card industry
Success Metrics
Security Metrics
- ✅ Zero unauthorized access incidents
- ✅ < 0.1% lockout rate (balance security vs UX)
- ✅ 100% PIN hash security (PBKDF2 with strong parameters)
User Adoption
- 🎯 Target: 60% of active users enable PIN within 30 days
- 🎯 Target: < 5% disable PIN after enabling
- 🎯 Target: > 90% user satisfaction with lock/unlock UX
Performance
- ✅ < 100ms PIN verification time
- ✅ < 500ms lock/unlock state transition
- ✅ Zero impact on unlocked session performance
Open Questions
-
Forgot PIN Flow: Contact support vs security questions vs recovery phrase?
- Recommendation: Contact support with identity verification (Telegram username + recent order ID)
-
PIN Complexity: Force 4 digits or allow stronger PINs?
- Recommendation: Default 4 digits, optional 6-digit or alphanumeric in settings
-
Session Expiry: Should locked sessions expire and clear data after X days?
- Recommendation: 90-day expiry with warning email/notification at 80 days
-
Multi-Device: If user has bot open on phone and desktop, lock both?
- Recommendation: Yes, state is server-side, lock applies to all clients
-
PIN Recovery: Allow self-service PIN reset via email?
- Recommendation: Phase 1 = support only, Phase 2 = email with order verification
References
- OWASP Password Storage Cheat Sheet
- Telegram Bot API Security Best Practices
- NIST Digital Identity Guidelines
- PBKDF2 Implementation Guide
Document Version: 1.0 Author: SilverLabs Development Team Review Date: October 6, 2025 Next Review: November 6, 2025 (post-implementation)