# 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 ```csharp 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 ```csharp public class UserSession { // Existing fields... public Guid Id { get; set; } public long TelegramUserId { get; set; } public Cart Cart { get; set; } public List 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 ```csharp // 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 ```csharp // 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 ```csharp 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 ```csharp // 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 { // ... 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 ```sql -- 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 to `UserSession` model - [ ] Create database migration for new columns - [ ] Implement `HashPin()` and `VerifyPin()` methods - [ ] Add security gate at top of `CallbackHandler.Handle()` - [ ] Create `ShowUnlockScreen()` method - [ ] Build PIN pad UI in `MenuBuilder.PinPad()` - [ ] Implement `HandlePinDigit()` and `HandlePinSubmit()` - [ ] 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 1. **Forgot PIN Flow**: Contact support vs security questions vs recovery phrase? - **Recommendation**: Contact support with identity verification (Telegram username + recent order ID) 2. **PIN Complexity**: Force 4 digits or allow stronger PINs? - **Recommendation**: Default 4 digits, optional 6-digit or alphanumeric in settings 3. **Session Expiry**: Should locked sessions expire and clear data after X days? - **Recommendation**: 90-day expiry with warning email/notification at 80 days 4. **Multi-Device**: If user has bot open on phone and desktop, lock both? - **Recommendation**: Yes, state is server-side, lock applies to all clients 5. **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](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) - [Telegram Bot API Security Best Practices](https://core.telegram.org/bots/security) - [NIST Digital Identity Guidelines](https://pages.nist.gov/800-63-3/) - [PBKDF2 Implementation Guide](https://cryptobook.nakov.com/mac-and-key-derivation/pbkdf2) --- **Document Version**: 1.0 **Author**: SilverLabs Development Team **Review Date**: October 6, 2025 **Next Review**: November 6, 2025 (post-implementation)