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

488 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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