# 🔐 ПЛАН РЕАЛИЗАЦИИ БЕЗОПАСНОЙ СИСТЕМЫ API КЛЮЧЕЙ

> **Дата:** 2025-09-18  
> **Проект:** SFERA  
> **Статус:** ГОТОВ К РЕАЛИЗАЦИИ  

## 📊 РЕЗУЛЬТАТЫ ДИАГНОСТИКИ

### ✅ ТЕКУЩЕЕ СОСТОЯНИЕ СИСТЕМЫ:

**Данные в БД:**
- 1 активный API ключ Wildberries (длина 398 символов)
- Организация "Rennel" типа SELLER
- Ключ создан: 2025-09-17T15:19:35.423Z
- **ПРОБЛЕМА:** Хранится в открытом виде в поле `apiKey`

**UI Состояние:**
- ✅ Звездочки отображаются корректно: `••••••••••••••••••••`
- ✅ Логика переключения работает (редактирование очищает поле)
- ✅ Индикатор статуса показывает "API ключ настроен"
- ❌ **КРИТИЧНО:** handleSave НЕ СОХРАНЯЕТ API ключи

**Безопасность:**
- ❌ Ключи хранятся в открытом виде в БД
- ❌ ENCRYPTION_KEY отсутствует в .env
- ⚠️ Нужно проверить копирование звездочек

### 🎯 ЦЕЛИ РЕАЛИЗАЦИИ:

1. **Безопасность:** Зашифровать существующий API ключ
2. **Функциональность:** Исправить сохранение в handleSave
3. **UX:** Защитить от копирования звездочек
4. **Производительность:** Кэш расшифровки на 2 часа

---

## 🛡️ ПОЭТАПНЫЙ ПЛАН РЕАЛИЗАЦИИ

### 📋 ЭТАП 1: ТЕСТИРОВАНИЕ КОПИРОВАНИЯ (10 мин)
**Цель:** Убедиться что звездочки не раскрывают реальный ключ

**Задачи:**
- [ ] Запустить dev сервер
- [ ] Открыть /seller/settings в браузере
- [ ] Протестировать копирование поля с звездочками
- [ ] При необходимости добавить атрибуты блокировки

**Ожидаемый результат:** Звездочки нельзя скопировать как реальный ключ

**Критерии успеха:**
- ✅ При копировании получаются звездочки, а не настоящий ключ
- ✅ Невозможно выделить реальный ключ из поля

---

### 🔧 ЭТАП 2: ИСПРАВЛЕНИЕ СОХРАНЕНИЯ (30 мин)
**Цель:** handleSave начинает сохранять API ключи в БД

**Подготовка:**
```bash
# Backup текущего состояния
cp src/components/dashboard/user-settings.tsx src/components/dashboard/user-settings.tsx.backup
```

**Задачи:**
- [ ] Найти где обрезана функция handleSave (после строки 698)
- [ ] Добавить импорт ADD_MARKETPLACE_API_KEY в user-settings.tsx
- [ ] Добавить логику сохранения API ключей после сохранения профиля
- [ ] Протестировать сохранение через UI

**Код изменений:**
```typescript
// В импортах добавить:
import { UPDATE_USER_PROFILE, UPDATE_ORGANIZATION_BY_INN, ADD_MARKETPLACE_API_KEY } from '@/graphql/mutations'

// В handleSave добавить после сохранения профиля:
// Сохраняем API ключи маркетплейсов
if (formData.wildberriesApiKey && formData.wildberriesApiKey !== '••••••••••••••••••••') {
  await apolloClient.mutate({
    mutation: ADD_MARKETPLACE_API_KEY,
    variables: {
      input: {
        marketplace: 'WILDBERRIES',
        apiKey: formData.wildberriesApiKey,
        validateOnly: false
      }
    }
  })
}

if (formData.ozonApiKey && formData.ozonApiKey !== '••••••••••••••••••••') {
  await apolloClient.mutate({
    mutation: ADD_MARKETPLACE_API_KEY,
    variables: {
      input: {
        marketplace: 'OZON',
        apiKey: formData.ozonApiKey,
        clientId: formData.ozonClientId,
        validateOnly: false
      }
    }
  })
}

// Обновляем кэш с новыми API ключами
await apolloClient.refetchQueries({
  include: [GET_ME]
})
```

**Тестирование:**
- [ ] Ввести новый WB ключ → должен сохраниться в БД
- [ ] Проверить что ключ виден в GraphQL ответе
- [ ] Проверить работу статистики с новым ключом

**Rollback план:** Восстановить из backup файла

---

### 🔐 ЭТАП 3: СОЗДАНИЕ КРИПТОСЕРВИСА (45 мин)
**Цель:** Безопасное шифрование с кэшем на 2 часа

**Подготовка:**
```bash
# Генерация мастер-ключа
openssl rand -hex 32

# Добавление в .env
echo "ENCRYPTION_KEY=сгенерированный_ключ" >> .env
```

**Задачи:**
- [ ] Создать `/src/services/crypto.service.ts`
- [ ] Реализовать AES-256-GCM шифрование
- [ ] Добавить кэш с TTL 2 часа
- [ ] Написать юнит-тесты

**Реализация CryptoService:**
```typescript
// src/services/crypto.service.ts
import crypto from 'crypto'

export class CryptoService {
  private algorithm = 'aes-256-gcm'
  private key: Buffer
  
  // Кэш на 2 часа
  private cache = new Map<string, { value: string; expires: number }>()
  
  constructor() {
    const masterKey = process.env.ENCRYPTION_KEY
    if (!masterKey || masterKey.length !== 64) {
      throw new Error('ENCRYPTION_KEY must be 64 hex characters')
    }
    this.key = Buffer.from(masterKey, 'hex')
  }
  
  encrypt(text: string): { encrypted: string; iv: string; tag: string } {
    const iv = crypto.randomBytes(16)
    const cipher = crypto.createCipheriv(this.algorithm, this.key, iv)
    
    let encrypted = cipher.update(text, 'utf8', 'hex')
    encrypted += cipher.final('hex')
    
    return {
      encrypted,
      iv: iv.toString('hex'),
      tag: cipher.getAuthTag().toString('hex')
    }
  }
  
  decrypt(encrypted: string, iv: string, tag: string): string {
    // Проверяем кэш (2 часа TTL)
    const cacheKey = `${encrypted}-${iv}`
    const cached = this.cache.get(cacheKey)
    if (cached && cached.expires > Date.now()) {
      return cached.value
    }
    
    // Расшифровываем
    const decipher = crypto.createDecipheriv(
      this.algorithm,
      this.key,
      Buffer.from(iv, 'hex')
    )
    
    decipher.setAuthTag(Buffer.from(tag, 'hex'))
    
    let decrypted = decipher.update(encrypted, 'hex', 'utf8')
    decrypted += decipher.final('utf8')
    
    // Сохраняем в кэш на 2 часа
    this.cache.set(cacheKey, {
      value: decrypted,
      expires: Date.now() + 2 * 60 * 60 * 1000 // 2 часа
    })
    
    // Очищаем старые записи
    this.cleanCache()
    
    return decrypted
  }
  
  private cleanCache() {
    const now = Date.now()
    for (const [key, item] of this.cache) {
      if (item.expires < now) {
        this.cache.delete(key)
      }
    }
  }
}
```

**Тестирование:**
- [ ] Шифрование/расшифровка работает корректно
- [ ] Кэш сохраняет данные на 2 часа
- [ ] Автоочистка удаляет старые записи

---

### 🗄️ ЭТАП 4: МИГРАЦИЯ БД (30 мин)
**Цель:** Добавить поля для зашифрованных ключей

**Задачи:**
- [ ] Создать Prisma миграцию с новыми полями
- [ ] Применить миграцию к БД
- [ ] Проверить совместимость

**Новая схема:**
```prisma
model ApiKey {
  id             String          @id @default(cuid())
  marketplace    MarketplaceType
  
  // Новые поля для шифрования
  encryptedKey   String?         // Зашифрованный ключ
  encryptionIv   String?         // Вектор инициализации  
  encryptionTag  String?         // Тег аутентификации
  lastUsedAt     DateTime?       // Аудит использования
  
  // Старое поле (временно для совместимости)
  apiKey         String          // УДАЛИТЬ после миграции
  
  clientId       String?
  isActive       Boolean         @default(true)
  createdAt      DateTime        @default(now())
  updatedAt      DateTime        @updatedAt
  validationData Json?
  organizationId String
  organization   Organization    @relation(fields: [organizationId], references: [id])
  
  @@unique([organizationId, marketplace])
  @@map("api_keys")
}
```

**Команды миграции:**
```bash
npx prisma migrate dev --name add_api_keys_encryption
npx prisma generate
```

**Тестирование:**
- [ ] Миграция применяется без ошибок
- [ ] Существующий ключ остается рабочим
- [ ] Новые поля готовы к использованию

**Rollback:** `npx prisma migrate reset` (данные не критичны)

---

### 🔄 ЭТАП 5: ОБНОВЛЕНИЕ РЕЗОЛВЕРОВ (45 мин)
**Цель:** Использовать шифрование в GraphQL

**Задачи:**
- [ ] Обновить addMarketplaceApiKey для шифрования новых ключей
- [ ] Обновить getWildberriesStatistics для расшифровки
- [ ] Добавить fallback на старые ключи
- [ ] Протестировать API

**Изменения в wildberries.ts:**
```typescript
import { CryptoService } from '../../../services/crypto.service'

const cryptoService = new CryptoService()

// В addMarketplaceApiKey:
if (existing) {
  // Шифруем новый ключ
  const encrypted = cryptoService.encrypt(apiKey)
  
  await prisma.apiKey.update({
    where: { id: existing.id },
    data: {
      encryptedKey: encrypted.encrypted,
      encryptionIv: encrypted.iv,
      encryptionTag: encrypted.tag,
      apiKey: null, // Очищаем старое поле
      isActive: true
    }
  })
}

// В getWildberriesStatistics:
let decryptedKey: string

if (apiKey.encryptedKey && apiKey.encryptionIv && apiKey.encryptionTag) {
  // Новый зашифрованный ключ
  decryptedKey = cryptoService.decrypt(
    apiKey.encryptedKey,
    apiKey.encryptionIv,
    apiKey.encryptionTag
  )
} else if (apiKey.apiKey) {
  // Fallback на старый незашифрованный ключ
  decryptedKey = apiKey.apiKey
} else {
  return { success: false, message: 'API ключ не найден' }
}
```

**Тестирование:**
- [ ] Новые ключи шифруются при сохранении
- [ ] Старый ключ работает через fallback
- [ ] Статистика загружается корректно
- [ ] Кэш работает (повторные запросы быстрее)

---

### 📦 ЭТАП 6: МИГРАЦИЯ СУЩЕСТВУЮЩЕГО КЛЮЧА (15 мин)
**Цель:** Перешифровать единственный ключ в БД

**Задачи:**
- [ ] Создать скрипт миграции
- [ ] Выполнить перешифровку
- [ ] Убрать fallback логику
- [ ] Финальное тестирование

**Скрипт миграции:**
```javascript
// scripts/migrate-existing-api-keys.js
import { PrismaClient } from '@prisma/client'
import { CryptoService } from '../src/services/crypto.service.js'

const prisma = new PrismaClient()
const cryptoService = new CryptoService()

async function migrateApiKeys() {
  const keys = await prisma.apiKey.findMany({
    where: {
      apiKey: { not: null },
      encryptedKey: null
    }
  })
  
  console.log(`🔄 Найдено ключей для миграции: ${keys.length}`)
  
  for (const key of keys) {
    if (!key.apiKey) continue
    
    const encrypted = cryptoService.encrypt(key.apiKey)
    
    await prisma.apiKey.update({
      where: { id: key.id },
      data: {
        encryptedKey: encrypted.encrypted,
        encryptionIv: encrypted.iv,
        encryptionTag: encrypted.tag,
        apiKey: null // Очищаем старое поле
      }
    })
    
    console.log(`✅ Мигрирован ключ ${key.id} для ${key.marketplace}`)
  }
  
  console.log('🎉 Миграция завершена')
}

migrateApiKeys().catch(console.error)
```

**Выполнение:**
```bash
node scripts/migrate-existing-api-keys.js
```

**Тестирование:**
- [ ] Существующий ключ перешифрован
- [ ] Статистика продолжает работать  
- [ ] В БД нет незашифрованных ключей
- [ ] Fallback больше не используется

---

## 🧪 ПЛАН ТЕСТИРОВАНИЯ

### После каждого этапа проверять:

**1. Основная функциональность:**
- [ ] Авторизация в кабинет селлера работает
- [ ] Настройки → API ключи открываются
- [ ] Статистика Wildberries загружается

**2. Сохранение ключей:**
- [ ] Ввод нового WB ключа сохраняется
- [ ] Настройки сохраняются без ошибок
- [ ] Ключ появляется в БД

**3. Безопасность:**
- [ ] Звездочки показываются вместо ключей
- [ ] Копирование не раскрывает реальные ключи
- [ ] GraphQL не возвращает расшифрованные ключи
- [ ] Ошибки не логируют реальные ключи

**4. Производительность:**
- [ ] Первый запрос статистики (расшифровка)
- [ ] Повторный запрос (из кэша) быстрее
- [ ] Память не растет при множественных запросах

---

## ⚠️ КРИТИЧЕСКИЕ МОМЕНТЫ

### 🚨 ТОЧКИ ОСТАНОВКИ:
- Статистика перестала загружаться
- Ошибки сохранения в настройках  
- Потеря существующего API ключа
- Любые 500 ошибки в GraphQL
- Ошибки шифрования/расшифровки

### 🔄 ROLLBACK ПЛАНЫ:

**Этап 2:** Восстановить user-settings.tsx из backup  
**Этап 3:** Не применять к продакшену  
**Этап 4:** `npx prisma migrate reset`  
**Этап 5:** Откатить изменения резолверов  
**Этап 6:** Восстановить из backup БД  

### 💾 BACKUP ФАЙЛЫ:
- ✅ `api_keys_backup_1758196936364.json` - данные БД
- ✅ `user-settings.tsx.backup` - будет создан на этапе 2

---

## 🎯 ОЖИДАЕМЫЙ РЕЗУЛЬТАТ

### После завершения всех этапов:

**✅ Безопасность:**
- API ключи зашифрованы в БД алгоритмом AES-256-GCM
- UI показывает только звездочки
- Копирование не раскрывает реальные ключи
- GraphQL не возвращает расшифрованные ключи

**✅ Функциональность:**
- Сохранение API ключей через UI работает
- Статистика Wildberries загружается корректно
- Существующий ключ мигрирован без потерь

**✅ Производительность:**
- Кэш расшифровки на 2 часа
- Автоочистка старых записей кэша
- Быстрые повторные запросы

**✅ Аудит:**
- Поле lastUsedAt отслеживает использование
- Логи не содержат реальных ключей
- Безопасное хранение в production

---

## 📋 ЧЕКЛИСТ ГОТОВНОСТИ

Перед началом убедиться:
- [ ] Проект запускается локально
- [ ] БД доступна и работает
- [ ] Есть доступ к кабинету селлера
- [ ] Backup данных создан
- [ ] ENCRYPTION_KEY сгенерирован

**План готов к исполнению! 🚀**

---

*Документ создан: 2025-09-18*  
*Проект: SFERA Marketplace Platform*  
*Статус: Готов к реализации*