# 🔐 ПЛАН РЕАЛИЗАЦИИ БЕЗОПАСНОЙ СИСТЕМЫ 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() 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* *Статус: Готов к реализации*