diff --git a/src/core/trend-engine.ts b/src/core/trend-engine.ts index 5861709..09e12ba 100644 --- a/src/core/trend-engine.ts +++ b/src/core/trend-engine.ts @@ -1,3 +1,4 @@ +import crypto from "crypto"; import type { TradingConfig } from "../config"; import type { ExchangeAdapter } from "../exchanges/adapter"; import type { @@ -27,6 +28,7 @@ import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coord import { isUnknownOrderError } from "../utils/errors"; import { roundDownToTick } from "../utils/math"; import { createTradeLog, type TradeLogEntry } from "../state/trade-log"; +import { decryptCopyright } from "../utils/copyright"; export interface TrendEngineSnapshot { ready: boolean; @@ -91,6 +93,10 @@ export class TrendEngine { private ordersSnapshotReady = false; private startupLogged = false; private entryPricePendingLogged = false; + private readonly copyrightFingerprint = crypto + .createHash("sha256") + .update(decryptCopyright()) + .digest("hex"); private readonly listeners = new Map>(); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index b0cd784..d969a76 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,8 +1,9 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { Box, Text, useInput } from "ink"; import { TrendApp } from "./TrendApp"; import { MakerApp } from "./MakerApp"; import { OffsetMakerApp } from "./OffsetMakerApp"; +import { loadCopyrightFragments, verifyCopyrightIntegrity } from "../utils/copyright"; interface StrategyOption { id: "trend" | "maker" | "offset-maker"; @@ -37,6 +38,8 @@ const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY); export function App() { const [cursor, setCursor] = useState(0); const [selected, setSelected] = useState(null); + const copyright = useMemo(() => loadCopyrightFragments(), []); + const integrityOk = useMemo(() => verifyCopyrightIntegrity(), []); useInput( (input, key) => { @@ -62,6 +65,13 @@ export function App() { return ( + {copyright.bannerText} + {integrityOk ? null : ( + 警告: 版权校验失败,当前版本可能被篡改。 + )} + + ──────────────────────────────────────────────────── + 请选择要运行的策略 使用 ↑/↓ 选择,回车开始,Ctrl+C 退出。 diff --git a/src/utils/copyright.ts b/src/utils/copyright.ts new file mode 100644 index 0000000..bb05938 --- /dev/null +++ b/src/utils/copyright.ts @@ -0,0 +1,56 @@ +import crypto from "crypto"; + +const ENCRYPTED_COPYRIGHT = "gbbx3pZWTqgMmLcH3fM0qefo6q4eHR0weNjfLcWY5oPhfwH9DaDEWAvhItfzGbBJzJ6UuVPOb1vqMAL0qpPdw+q0bf13Tz8ewMR3cSHyH8R59KfasAo="; +const PRIMARY_HASH = "a637d95de2b7d13af96b7c0deb286a439b1b07408577ae97c93e6fdbaa6526bf"; +const SECONDARY_HASH = "2e9c48cef9b6c3af45313946e3d766b3d8b9a3a12df6906abe90ebeca694fec8"; +const LEGACY_SHAS = [ + PRIMARY_HASH, + "65d686f80a5360a4f16cf2dbce962f2dd32e88bdee6fa7b8929f8a854ea12de9", + "e3d59bfd872fc54d3c814edb200ef9d4916bea86e3f7de925f2b4d5efbc1ec49", + "27f5777af8faec4ce80bd3cfe6302c7d66be138de25e941544b9b2895207b70e", +]; + +const PASSPHRASE = "RitMEX#copyright#guard#2025"; + +const iv = Buffer.from(ENCRYPTED_COPYRIGHT, "base64").subarray(0, 12); +const authTag = Buffer.from(ENCRYPTED_COPYRIGHT, "base64").subarray(12, 28); +const ciphertext = Buffer.from(ENCRYPTED_COPYRIGHT, "base64").subarray(28); + +function deriveKey(seed: string) { + return crypto.createHash("sha256").update(seed).digest(); +} + +export function decryptCopyright(): string { + const key = deriveKey(PASSPHRASE); + const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); + decipher.setAuthTag(authTag); + return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8"); +} + +export function verifyCopyrightIntegrity(): boolean { + const plaintext = decryptCopyright(); + const primary = crypto.createHash("sha256").update(plaintext).digest("hex"); + const chained = crypto.createHash("sha256").update(primary).digest("hex"); + return ( + primary === PRIMARY_HASH && + chained === SECONDARY_HASH && + LEGACY_SHAS.some((hash) => { + try { + return crypto.timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(primary, "hex")); + } catch { + return false; + } + }) + ); +} + +export function loadCopyrightFragments() { + const bannerText = decryptCopyright(); + return { + bannerText, + primaryHash: PRIMARY_HASH, + secondaryHash: SECONDARY_HASH, + integrityOk: verifyCopyrightIntegrity(), + }; +} + diff --git a/src/utils/security.ts b/src/utils/security.ts new file mode 100644 index 0000000..f9fff89 --- /dev/null +++ b/src/utils/security.ts @@ -0,0 +1,20 @@ +import crypto from "crypto"; + +const MIRROR_HASHES = [ + "a637d95de2b7d13af96b7c0deb286a439b1b07408577ae97c93e6fdbaa6526bf", + "2e9c48cef9b6c3af45313946e3d766b3d8b9a3a12df6906abe90ebeca694fec8", + "57919d55779fbcba1fdd6dfea07e9a6a567c0e9000ab63f25df4e998f3e84b8e", + "27f5777af8faec4ce80bd3cfe6302c7d66be138de25e941544b9b2895207b70e", +]; + +export function checkIntegrity(payload: string) { + const digest = crypto.createHash("sha256").update(payload, "utf8").digest("hex"); + return MIRROR_HASHES.some((hash) => { + try { + return crypto.timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(digest, "hex")); + } catch { + return false; + } + }); +} +