Защита рецептов: SQL DEPO/SPECTRE/QUIK вынесен в пакет fsql под garble

Реализация варианта B из аудита защиты знаний. Прямой garble всего bj-server
ломает html/template (он обращается к полям структур по имени, garble их
переименовывает). Решение — селективная обфускация: GOGARBLE обфусцирует ТОЛЬКО
пакет с секретами, шаблоны и обработчики не затрагиваются.

95 SQL-запросов к DEPO/SPECTRE/QUIK и трансферным таблицам FTV_M2M вынесены из
lkgateway в отдельный пакет internal/lkgateway/fsql как переменные (именно var,
не const: const компилятор вставил бы обратно в необфусцируемый пакет, и
шифрование бы не сработало — проверено). Сами обработчики, типы и параметры
sql.Named остались на месте; пакет держит только строки запросов.

build-release.sh собирает bj-server с `GOGARBLE=.../fsql garble -literals`
(нужен отдельный SDK go1.26 — модуль-кэшный тулчейн garble не подходит); если
garble/go1.26 не найдены — обычная сборка с предупреждением.

Проверено на реальном garble-бинаре (запускается, /admin и страницы на
структурах работают): рецепт чтения чужих систем из бинаря исчез —
OD_RESTS 13→0, OD_WIRING 5→0, OD_TURNS 1→0, OD_BALANCES 3→0, D_B_CONTRACTS 1→0,
OD_CLIENTS_PORTS 10→0, DepoLimits/MoneyLimits →0. Остаток FTV_M2M — упоминания
имён таблиц в операторских embed-шаблонах и логах, не сам рецепт.

Стенд собран обычной сборкой (без garble) — трассировки остаются читаемыми.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-18 20:29:47 +03:00
parent 47f5015c3f
commit 7ce4c2c79f
49 changed files with 605 additions and 332 deletions
+5 -42
View File
@@ -13,6 +13,7 @@ import (
"context"
"database/sql"
"fmt"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
"strings"
"time"
)
@@ -54,9 +55,7 @@ func depoClientIDs(ctx context.Context, s FancyMSSQLSettings, contract string) (
defer conn.Close()
cctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
rows, err := conn.QueryContext(cctx, `SELECT DISTINCT c.ID, ISNULL(c.NUMBER,''), ISNULL(c.FACE,0)
FROM OD_CLIENTS c JOIN OD_CLIENTS_PORTS p ON p.CLIENT = c.ID
WHERE p.PORT_NUM = @p1 OR p.PORT_NUM LIKE @p2`, contract, likeEscape(contract)+" %")
rows, err := conn.QueryContext(cctx, fsql.Hist01, contract, likeEscape(contract)+" %")
if err != nil {
return nil, 0, nil, err
}
@@ -137,32 +136,7 @@ func lkOperations(ctx context.Context, s FancyMSSQLSettings, contract, from, to
}
fromT, toT := parseRange(from, to)
// Три вида операций в одном списке: зачисление, списание, перевод между счетами.
q := fmt.Sprintf(`SELECT TOP %d * FROM (
SELECT d.R_DATE AS RD, d.PROC_DATE AS PD, ISNULL(d.NUM,'') AS NUM, d.STATE AS ST, N'зачисление' AS KIND,
z.DEPO AS ACC, ISNULL(v.ISIN,'') AS ISIN, ISNULL(v.NAME,'') AS SEC, CAST(ISNULL(z.AMOUNT,0) AS float) AS QTY,
LTRIM(RTRIM(ISNULL(z.COMMENT,'') + ' ' + ISNULL(z.REJECT_REASON,''))) AS NOTE
FROM U_OP_INV_ZACH z JOIN OD_DOCS d ON d.ID = z.DOC LEFT JOIN OD_VALUES v ON v.ID = z.SHARE
WHERE z.DEPO IN (%s)
UNION ALL
SELECT d.R_DATE, d.PROC_DATE, ISNULL(d.NUM,''), d.STATE, N'списание',
p.DEPO, ISNULL(v.ISIN,''), ISNULL(v.NAME,''), -CAST(ISNULL(p.AMOUNT,0) AS float),
LTRIM(RTRIM(ISNULL(p.COMMENT,'') + ' ' + ISNULL(p.REJECT_REASON,'')))
FROM U_OP_INV_SPIS p JOIN OD_DOCS d ON d.ID = p.DOC LEFT JOIN OD_VALUES v ON v.ID = p.SHARE
WHERE p.DEPO IN (%s)
UNION ALL
SELECT d.R_DATE, d.PROC_DATE, ISNULL(d.NUM,''), d.STATE, N'перевод: списание',
t.D_DEPO, ISNULL(v.ISIN,''), ISNULL(v.NAME,''), -CAST(ISNULL(t.AMOUNT,0) AS float),
LTRIM(RTRIM(ISNULL(t.COMMENT,'') + ' ' + ISNULL(t.REJECT_REASON,'')))
FROM U_OP_INV_PEREVOD t JOIN OD_DOCS d ON d.ID = t.DOC LEFT JOIN OD_VALUES v ON v.ID = t.SHARE
WHERE t.D_DEPO IN (%s)
UNION ALL
SELECT d.R_DATE, d.PROC_DATE, ISNULL(d.NUM,''), d.STATE, N'перевод: зачисление',
t.K_DEPO, ISNULL(v.ISIN,''), ISNULL(v.NAME,''), CAST(ISNULL(t.AMOUNT,0) AS float),
LTRIM(RTRIM(ISNULL(t.COMMENT,'') + ' ' + ISNULL(t.REJECT_REASON,'')))
FROM U_OP_INV_PEREVOD t JOIN OD_DOCS d ON d.ID = t.DOC LEFT JOIN OD_VALUES v ON v.ID = t.SHARE
WHERE t.K_DEPO IN (%s)) x
WHERE x.RD >= @p1 AND x.RD < DATEADD(day, 1, @p2)
ORDER BY x.RD DESC, x.NUM DESC`, limit, inClause(ids), inClause(ids), inClause(ids), inClause(ids))
q := fmt.Sprintf(fsql.Hist02, limit, inClause(ids), inClause(ids), inClause(ids), inClause(ids))
rows, err := conn.QueryContext(cctx, q, fromT, toT)
if err != nil {
return nil, err
@@ -226,17 +200,7 @@ func lkIncome(ctx context.Context, s FancyMSSQLSettings, contract, from, to stri
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
fromT, toT := parseRange(from, to)
q := fmt.Sprintf(`SELECT TOP 200 h.P_DATE, h.INCOME_KIND, ISNULL(v.ISIN,''), ISNULL(v.NAME,''),
CAST(ISNULL(l.AMOUNT,0) AS float), CAST(ISNULL(l.SUMMA_TOTAL,0) AS float),
CAST(ISNULL(l.TAX_HOLDED,0) AS float), CAST(ISNULL(l.TOTAL_TO_PAY,0) AS float),
ISNULL(l.STATUS,''), ISNULL(l.DEPO,0)
FROM U_OP_INF_INCOME_DOL l
JOIN OD_DOLS o ON o.ID = l.DOL
JOIN U_OP_INF_INCOME h ON h.DOC = o.DOC
LEFT JOIN OD_VALUES v ON v.ID = h.SHARE
WHERE (l.DEPO IN (%s) OR l.INCOME_FACE = @p3)
AND h.P_DATE >= @p1 AND h.P_DATE < DATEADD(day, 1, @p2)
ORDER BY h.P_DATE DESC`, inClause(ids))
q := fmt.Sprintf(fsql.Hist03, inClause(ids))
rows, err := conn.QueryContext(cctx, q, fromT, toT, face)
if err != nil {
return nil, 0, err
@@ -266,8 +230,7 @@ func lkIncome(ctx context.Context, s FancyMSSQLSettings, contract, from, to stri
}
// Удержанный налог по лицу за период — по журналу НДФЛ депозитария.
if face > 0 {
_ = conn.QueryRowContext(cctx, `SELECT CAST(ISNULL(SUM(CAST(SUMMA AS float)),0) AS float) FROM OD_NDFL
WHERE FACE = @p1 AND R_DATE >= @p2 AND R_DATE < DATEADD(day, 1, @p3)`,
_ = conn.QueryRowContext(cctx, fsql.Hist04,
sql.Named("p1", face), sql.Named("p2", fromT), sql.Named("p3", toT)).Scan(&taxTotal)
}
return rowsOut, taxTotal, nil