Files
sfera-new/2025-09-18/API_KEYS_SECURITY_IMPLEMENTATION_PLAN.md

17 KiB
Raw Blame History

🔐 ПЛАН РЕАЛИЗАЦИИ БЕЗОПАСНОЙ СИСТЕМЫ 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 ключи в БД

Подготовка:

# 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

Код изменений:

// В импортах добавить:
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 часа

Подготовка:

# Генерация мастер-ключа
openssl rand -hex 32

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

Задачи:

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

Реализация CryptoService:

// 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 миграцию с новыми полями
  • Применить миграцию к БД
  • Проверить совместимость

Новая схема:

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")
}

Команды миграции:

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:

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 логику
  • Финальное тестирование

Скрипт миграции:

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

Выполнение:

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
Статус: Готов к реализации