security: round 3 hardening (CSRF double-submit, TRX MITM, container hardening)

This commit is contained in:
ZOMBIIIIIII
2026-05-12 01:47:58 +03:00
parent c8bc40af97
commit 8dc0855827
37 changed files with 1852 additions and 318 deletions

View File

@@ -2,15 +2,15 @@
"openapi": "3.0.0",
"info": {
"title": "CryptoWallet API",
"version": "4.0.0",
"description": "Multi-chain crypto wallet API (non-custodial). Клиент сам деривит mnemonic и шлёт публичные адреса; сервер хранит только адреса и строит unsigned tx для отправки. Auth via JWT (cookie/Bearer), issued by external auth-service (BITOK)."
"version": "5.0.0",
"description": "Multi-chain custodial wallet API (ETH/BSC/BTC/TRX/SOL). Сервер генерит mnemonic, шифрует AES-256-GCM (master-key из HashiCorp Vault), хранит её и сам подписывает транзакции. Auth via JWT (cookie/Bearer), issued by external auth-service (BITOK)."
},
"servers": [
{ "url": "/api", "description": "API root" }
],
"tags": [
{ "name": "System", "description": "Health & service info" },
{ "name": "Wallets", "description": "User wallet records" },
{ "name": "Wallets", "description": "Custodial wallet lifecycle" },
{ "name": "Wallet Ops", "description": "Per-chain balance / transactions / send" },
{ "name": "BTC", "description": "Bitcoin RPC proxy (Blockstream)" },
{ "name": "TRON", "description": "TRON RPC proxy (TronGrid)" },
@@ -32,10 +32,6 @@
"error": { "type": "string" }
}
},
"SuccessEmpty": {
"type": "object",
"properties": { "success": { "type": "boolean", "example": true } }
},
"HealthResponse": {
"type": "object",
"properties": {
@@ -52,26 +48,7 @@
"properties": {
"chain": { "$ref": "#/components/schemas/Chain" },
"address": { "type": "string" },
"derivationPath": { "type": "string", "description": "BIP32 path, например m/44'/60'/0'/0/0" }
}
},
"WalletInput": {
"type": "object",
"required": ["chain", "address", "derivationPath"],
"properties": {
"chain": { "$ref": "#/components/schemas/Chain" },
"address": { "type": "string", "maxLength": 64, "description": "Публичный адрес (chain-specific checksum-валидируется)" },
"derivationPath": { "type": "string", "maxLength": 64, "description": "BIP32 m/.. (например m/44'/60'/0'/0/0)" }
}
},
"CreateWalletsRequest": {
"type": "object",
"required": ["wallets"],
"properties": {
"wallets": {
"type": "array", "minItems": 1, "maxItems": 20,
"items": { "$ref": "#/components/schemas/WalletInput" }
}
"derivationPath": { "type": "string", "description": "BIP32 path" }
}
},
"WalletsResponse": {
@@ -81,6 +58,31 @@
"data": { "type": "array", "items": { "$ref": "#/components/schemas/Wallet" } }
}
},
"MnemonicResponse": {
"type": "object",
"properties": {
"success": { "type": "boolean", "example": true },
"data": {
"type": "object",
"properties": {
"mnemonic": { "type": "string", "description": "BIP39 mnemonic (12 words)" }
}
}
}
},
"TxBroadcastResponse": {
"type": "object",
"properties": {
"success": { "type": "boolean", "example": true },
"data": {
"type": "object",
"properties": {
"txid": { "type": "string", "description": "Идентификатор отправленной транзакции" },
"chain": { "$ref": "#/components/schemas/Chain" }
}
}
}
},
"BalanceResponse": {
"type": "object",
"properties": {
@@ -125,17 +127,7 @@
"properties": {
"to": { "type": "string", "description": "Recipient address" },
"amount": { "type": "string", "description": "Amount в smallest units" },
"token": { "type": "string", "nullable": true, "description": "Например USDT для TRC20/ERC20/BEP20. Без token = native (TRX/ETH/BNB/BTC)" }
}
},
"UnsignedTxResponse": {
"type": "object",
"properties": {
"success": { "type": "boolean" },
"data": {
"type": "object",
"description": "Unsigned tx — формат зависит от chain (kind: btc | tron | evm | solana). Клиент подписывает приватом и broadcast'ит через соответствующий /api/{btc,tron}/broadcast endpoint или RPC своей цепи."
}
"token": { "type": "string", "nullable": true, "description": "USDT для TRC20/ERC20/BEP20. Без token = native." }
}
}
}
@@ -167,17 +159,44 @@
"/wallets/create": {
"post": {
"summary": "Upsert wallets для авторизованного юзера",
"description": "Клиент сам генерит mnemonic и деривит публичные адреса (BIP44 для ETH/BSC/BTC/TRX/SOL). Тело — массив `{chain, address, derivationPath}`. На конфликт (user_id, chain) сервер обновляет address+derivationPath. Mnemonic клиенту не нужно слать — сервер её не хранит.",
"summary": "Создать custodial-кошелёк (server-side mnemonic)",
"description": "**Тело запроса не требуется.** Сервер генерит BIP39 mnemonic (12 слов), деривит адреса для 5 chains (BIP44: ETH m/44'/60'/0'/0/0, BTC m/84'/0'/0'/0/0, TRX m/44'/195'/0'/0/0, SOL m/44'/501'/0'/0', BSC = ETH path), шифрует mnemonic AES-256-GCM (master-key из HashiCorp Vault) и атомарно сохраняет. **Возвращает ТОЛЬКО адреса** — mnemonic клиенту не отдаётся. Чтобы потом увидеть seed — отдельный endpoint POST /wallets/mnemonic/reveal. Идемпотентность: 409 если у юзера уже есть кошелёк.",
"tags": ["Wallets"],
"responses": {
"201": { "description": "Wallet created (returns addresses only)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletsResponse" } } } },
"401": { "description": "Not authenticated" },
"409": { "description": "Wallet already exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
"503": { "description": "Crypto service not ready" }
}
}
},
"/wallets/mnemonic/reveal": {
"post": {
"summary": "Раскрыть mnemonic (settings-screen)",
"description": "Расшифровывает и возвращает 12-словную BIP39 мнемонику юзера. POST + CSRF + body-confirmation. Rate-limit 5/час per-user. Каждый запрос пишется в audit-log.",
"tags": ["Wallets"],
"requestBody": {
"required": true,
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateWalletsRequest" } } }
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["confirm"],
"properties": {
"confirm": { "type": "string", "enum": ["I_UNDERSTAND_SEED_IS_SECRET"] }
}
}
}
}
},
"responses": {
"201": { "description": "Created/updated (вернёт сохранённые адреса)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletsResponse" } } } },
"400": { "description": "Invalid input (chain/address/derivationPath)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
"401": { "description": "Not authenticated" }
"200": { "description": "Mnemonic revealed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MnemonicResponse" } } } },
"400": { "description": "Missing/invalid confirm token" },
"401": { "description": "Not authenticated" },
"404": { "description": "Wallet not created yet" },
"429": { "description": "Rate limit (5/hour) exceeded" },
"503": { "description": "Crypto service not ready" }
}
}
},
@@ -212,8 +231,8 @@
"/wallets/{chain}/send": {
"post": {
"summary": "Build unsigned send transaction (non-custodial)",
"description": "Возвращает unsigned tx. Клиент подписывает приватным ключом и broadcast'ит через /api/{btc,tron}/broadcast или RPC своей цепи.",
"summary": "Custodial send: server signs + broadcasts",
"description": "Юзер на клиенте жмёт 'подтвердить' → клиент шлёт {to, amount, token?}. Сервер расшифровывает мнемонику, деривит chain privkey, подписывает, broadcast'ит. Возвращает txid. Защита: TRX MITM check, EVM gas cap 500 gwei, SOL confirmTransaction, BTC timeout + safety multiplier.",
"tags": ["Wallet Ops"],
"parameters": [{ "name": "chain", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/Chain" } }],
"requestBody": {
@@ -221,10 +240,11 @@
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } }
},
"responses": {
"200": { "description": "Unsigned tx", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnsignedTxResponse" } } } },
"200": { "description": "Broadcast successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TxBroadcastResponse" } } } },
"400": { "description": "Invalid input" },
"404": { "description": "Wallet not found" },
"502": { "description": "Upstream RPC error" }
"404": { "description": "Wallet/mnemonic not found" },
"502": { "description": "Broadcast failed (insufficient balance / RPC error / unsupported)" },
"503": { "description": "Crypto service not ready" }
}
}
},