17 KiB
🔐 ПЛАН РЕАЛИЗАЦИИ БЕЗОПАСНОЙ СИСТЕМЫ 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
- ⚠️ Нужно проверить копирование звездочек
🎯 ЦЕЛИ РЕАЛИЗАЦИИ:
- Безопасность: Зашифровать существующий API ключ
- Функциональность: Исправить сохранение в handleSave
- UX: Защитить от копирования звездочек
- Производительность: Кэш расшифровки на 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
Статус: Готов к реализации