littleshop/docs/CHANNEL_LOCK_DESIGN.md
SysAdmin 7806bb2392 Docs: Channel Lock & PIN Protection feature design
- 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>
2025-10-06 04:39:29 +01:00

26 KiB
Raw Blame History

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 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


Document Version: 1.0 Author: SilverLabs Development Team Review Date: October 6, 2025 Next Review: November 6, 2025 (post-implementation)