From 9e5ad05c83fa55013cfbcd445248e9e3476c65ee Mon Sep 17 00:00:00 2001 From: LuSrackhall <3647637206@qq.com> Date: Wed, 31 Dec 2025 17:17:21 +0800 Subject: [PATCH 1/7] =?UTF-8?q?build(feat):=20=E5=AE=9E=E7=8E=B0=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E9=81=97=E7=95=99=E7=9A=84=E5=AF=B9=E7=A7=B0=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E6=9E=84=E5=BB=BA=E6=B3=A8=E5=85=A5=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E9=80=9A=E8=BF=87=20-ldflags=20?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E5=AF=86=E9=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将硬编码的对称密钥改为可注入变量,保持开源构建行为不变。 - 更新 SDK 和工具链以支持密钥注入,确保兼容性。 - 增加文档和规格,明确密钥注入的使用方式和影响。 --- BUILD_COMPATIBILITY.md | 17 ++++ BUILD_COMPATIBILITY.zh-CN.md | 18 +++++ .../design.md | 68 ++++++++++++++++ .../proposal.md | 52 +++++++++++++ .../specs/encrypted-outputs/spec.md | 35 +++++++++ .../tasks.md | 42 ++++++++++ ...64\346\227\266\350\257\264\346\230\216.md" | 62 +++++++++++++++ openspec/specs/encrypted-outputs/spec.md | 78 +++++++++++++++++++ openspec/specs/signature-management/spec.md | 4 +- sdk/audioPackage/enc/enc.go | 39 +++++++++- sdk/private_keys.template.env | 24 +++++- sdk/server/server.go | 63 ++++++++++++--- sdk/setup_build_env.sh | 12 +++ sdk/signature/album.go | 27 +++++-- sdk/signature/encryption.go | 48 ++++++++---- tools/ktalbum-tools/commands/extract.go | 14 +++- tools/ktalbum-tools/commands/info.go | 14 +++- tools/ktalbum-tools/utils/header.go | 61 ++++++++++++++- 18 files changed, 630 insertions(+), 48 deletions(-) create mode 100644 openspec/changes/update-build-injected-symmetric-keys/design.md create mode 100644 openspec/changes/update-build-injected-symmetric-keys/proposal.md create mode 100644 openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md create mode 100644 openspec/changes/update-build-injected-symmetric-keys/tasks.md create mode 100644 "openspec/changes/update-build-injected-symmetric-keys/\344\270\264\346\227\266\350\257\264\346\230\216.md" create mode 100644 openspec/specs/encrypted-outputs/spec.md diff --git a/BUILD_COMPATIBILITY.md b/BUILD_COMPATIBILITY.md index 8a8295a7..317e419c 100644 --- a/BUILD_COMPATIBILITY.md +++ b/BUILD_COMPATIBILITY.md @@ -13,6 +13,23 @@ Certain features rely on **build-time injected parameters** (such as signing or This difference is intentional and limited to identity-related behavior. +## Build-Time Injected Keys + +The project supports **build-time key injection** (via Go `-ldflags -X`), so private build identities can override default open-source keys **without changing source code**. + +This mechanism is used by: + +* Signature authorization flow keys (F/K/Y/N) +* Signature management encryption keys (A/B) +* Album export encryption keys (versioned XOR keys: v1/v2) +* Album config encryption seed (FixedSecret used for key derivation) +* Album `signature` field inner encryption key + +For local/private builds, see: + +* `sdk/private_keys.template.env` +* `sdk/setup_build_env.sh` + --- # Encrypted Output Compatibility diff --git a/BUILD_COMPATIBILITY.zh-CN.md b/BUILD_COMPATIBILITY.zh-CN.md index b78b7f21..1e3fcf75 100644 --- a/BUILD_COMPATIBILITY.zh-CN.md +++ b/BUILD_COMPATIBILITY.zh-CN.md @@ -13,6 +13,24 @@ 这种差异是**有意设计的**,且仅限于与身份相关的行为。 +## 构建时注入密钥(Build-Time Injected Keys) + +本项目支持通过 Go 的 `-ldflags -X` 进行**构建时密钥注入**: +私有构建可以在不修改源码的前提下覆盖开源默认密钥(注入值为 XOR 混淆后的 hex,运行时自动解混淆)。 + +该机制覆盖: + +- 授权流密钥(F/K/Y/N) +- 签名管理对称密钥(A/B) +- 专辑导出文件对称密钥(版本化 XOR:v1/v2) +- 专辑配置加密派生 seed(FixedSecret,用于派生 AES key) +- 专辑配置 `signature` 字段内层加密密钥 + +本地/私有构建入口: + +- `sdk/private_keys.template.env` +- `sdk/setup_build_env.sh` + --- # 加密产物的兼容性 diff --git a/openspec/changes/update-build-injected-symmetric-keys/design.md b/openspec/changes/update-build-injected-symmetric-keys/design.md new file mode 100644 index 00000000..57699fe8 --- /dev/null +++ b/openspec/changes/update-build-injected-symmetric-keys/design.md @@ -0,0 +1,68 @@ +# Design: 构建注入式对称密钥适配 + +## Overview + +本设计将历史遗留的对称密钥/secret 统一到与授权流相同的构建注入体系: + +- 默认值保留在源码中(开源构建无需私钥文件即可工作) +- 私有构建通过 `-ldflags -X` 覆盖变量值 +- 覆盖值为 XOR 混淆后的 hex 字符串,运行时自动解混淆为明文 + +## Injection Format + +### Build-Time + +- 通过 Go 链接器 `-X 'package.path.VarName=VALUE'` 注入 +- `VALUE` 推荐为:`tools/key-obfuscator` 输出的 hex(对任意长度字符串可用;长度非 32 时会提示 warning) + +### Runtime + +- 若变量值等于默认常量,则直接按默认明文使用 +- 否则按以下逻辑尝试解混淆: + 1. `hex.DecodeString(value)` + 2. 对每个字节执行 `b ^ xorMask[i%len(xorMask)]` + 3. 转换为 `string` 或(对 32-byte key 场景)截断/补齐到 32 +- 若注入值并非 hex(用户误注入明文),则回退为“直接使用该字符串” + +## Key Inventory and Usage + +### Signature Keys + +- KeyA (`KeyToneSignatureEncryptionKeyA`) + - 用途:加密签名 ID、派生动态密钥(PBKDF2) + - 注入点:`KeyTone/signature.KeyToneSignatureEncryptionKeyA` + +- KeyB (`KeyToneSignatureEncryptionKeyB`) + - 用途:`.ktsign` 导入/导出对称加密 + - 注入点:`KeyTone/signature.KeyToneSignatureEncryptionKeyB` + +### Album Export Keys (XOR) + +- v1/v2:用于 `.ktalbum` 文件体(zip 数据)的 XOR 加/解密 +- 文件头 `Version` 指定密钥版本;解密时校验失败会回退尝试 v1 +- 注入点: + - `KeyTone/server.KeytoneEncryptKeyV1` + - `KeyTone/server.KeytoneEncryptKeyV2` + +### Album Config Seed (FixedSecret) + +- 用途:派生 AES key:`SHA256(secret + last6(sha1(albumUUID)))` +- 注入点:`KeyTone/audioPackage/enc.FixedSecret` +- 说明:该 secret 非固定 32 字节,因此采用“可变长度解混淆”路径(hex->xor->string) + +### Album Signature Field Inner Key + +- 用途:专辑配置 `signature` 字段内层 AES-GCM(外层仍由 albumUUID 派生 key 保护) +- 注入点:`KeyTone/signature.KeyToneAlbumSignatureEncryptionKey` + +## Build Script Integration + +- `sdk/private_keys.template.env` 增加对应 KEY_* 项 +- `sdk/setup_build_env.sh` 增加 `KEYS_TO_PROCESS` 映射,使本地私有构建可以自动生成 `EXTRA_LDFLAGS` + +## Compatibility Notes + +- 未注入:行为与当前开源版本完全一致 +- 注入后: + - 官方/私有构建与社区构建的加密产物可能不互通(符合 BUILD_COMPATIBILITY 设计) + - 若选择覆盖 v1 key,将导致旧 v1 产物在该私有构建中不可解密(预期行为,需在 proposal 中显式告知) diff --git a/openspec/changes/update-build-injected-symmetric-keys/proposal.md b/openspec/changes/update-build-injected-symmetric-keys/proposal.md new file mode 100644 index 00000000..216b8eca --- /dev/null +++ b/openspec/changes/update-build-injected-symmetric-keys/proposal.md @@ -0,0 +1,52 @@ +# Change: 旧有对称加密密钥接入构建注入体系 + +## Why + +项目已在“授权流密钥”中采用构建时注入(`-ldflags -X` + XOR+hex 混淆)将私钥注入到构建结果中,并保持开源构建可用。 + +但项目中仍存在更早实现的对称加密能力,其密钥/secret 直接硬编码在源码中,导致无法与“构建身份”机制统一,且私有构建难以做到与开源构建的加密产物隔离。 + +## What Changes + +- 将以下对称密钥/secret 从 `const` 改为可注入 `var`,默认值保持原硬编码字符串不变: + - 签名管理 KeyA / KeyB + - 专辑配置 `signature` 字段内层加密密钥 + - 专辑导出文件 XOR 密钥(v1/v2) + - 专辑配置加密派生 secret(FixedSecret) +- 统一注入方式:注入值为 XOR 混淆后的 hex 字符串;运行时自动解混淆。 +- 更新构建脚本与模板:`sdk/setup_build_env.sh` 与 `sdk/private_keys.template.env` 增加新 key 项。 +- 更新文档:`BUILD_COMPATIBILITY.md` 补充“Build-Time Injected Keys”列表。 + +## Non-Goals + +- 不改变默认开源构建行为:未提供私钥注入时,仍使用源码默认值。 +- 不更换加密算法(仅调整密钥来源/注入方式)。 + +## Impact + +- Affected code: + - `sdk/signature/encryption.go` + - `sdk/signature/album.go` + - `sdk/audioPackage/enc/enc.go` + - `sdk/server/server.go` + - `sdk/setup_build_env.sh` + - `sdk/private_keys.template.env` + - `tools/ktalbum-tools/utils/header.go` + - `tools/ktalbum-tools/commands/*.go` + - `BUILD_COMPATIBILITY.md` + +- Affected specs: + - New capability: `openspec/specs/encrypted-outputs/spec.md` + +## Review Notes / Audit Trail + +| Key/Secret | Default(源码) | 注入变量(Go -ldflags -X) | 用途摘要 | +| ----------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- | +| 签名 KeyA | `KeyTone2024Signature_KeyA_SecureEncryptionKeyForIDEncryption` | `KeyTone/signature.KeyToneSignatureEncryptionKeyA` | 加密签名ID、派生动态密钥(PBKDF2) | +| 签名 KeyB | `KeyTone2024Signature_KeyB_SuperSecureEncryptionKeyForExportImportOperation` | `KeyTone/signature.KeyToneSignatureEncryptionKeyB` | `.ktsign` 导入/导出加密 | +| 专辑 signature 字段密钥 | `KeyTone2024Album_Signature_Field_EncryptionKey_32Bytes` | `KeyTone/signature.KeyToneAlbumSignatureEncryptionKey` | 专辑配置中 `signature` 字段内层 AES-GCM | +| 专辑导出 XOR v1 | `KeyTone2024SecretKey` | `KeyTone/server.KeytoneEncryptKeyV1` | `.ktalbum` v1 加/解密(兼容) | +| 专辑导出 XOR v2 | `KeyTone2025AlbumSecureEncryptionKeyV2` | `KeyTone/server.KeytoneEncryptKeyV2` | `.ktalbum` v2 加/解密(当前) | +| 专辑配置派生 secret | `LuSrackhall_KeyTone_2024_Signature_66688868686688` | `KeyTone/audioPackage/enc.FixedSecret` | 派生 AES key:`SHA256(secret + last6(sha1(albumUUID)))` | + +注:工具链 `tools/ktalbum-tools` 也提供同等注入点(模块路径不同)。 diff --git a/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md new file mode 100644 index 00000000..fbf7a5d6 --- /dev/null +++ b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md @@ -0,0 +1,35 @@ +# Encrypted Outputs(增量) + +## MODIFIED Requirements + +### Requirement: 构建身份与对称密钥来源 + +Normative: The system SHALL keep default symmetric keys/secrets hardcoded in source for open-source builds, and SHALL allow overriding them at build-time via Go `-ldflags -X` using XOR-obfuscated hex values; at runtime it MUST deobfuscate injected values before use. + +#### Scenario: 未注入私钥时保持原行为 + +- **GIVEN** 构建过程中未提供任何 `-ldflags -X` 覆盖值 +- **WHEN** 系统进行签名管理加解密、专辑导出/导入、专辑配置加解密 +- **THEN** 系统使用源码默认密钥/secret,行为与当前开源版本一致 + +#### Scenario: 注入密钥后自动解混淆 + +- **GIVEN** 构建过程中通过 `-ldflags -X` 注入了 XOR 混淆后的 hex +- **WHEN** 系统在运行时读取该变量并用于加解密 +- **THEN** 系统先执行 `hex -> xorMask -> plaintext` 解混淆,再进行加解密 + +### Requirement: 专辑导出版本化密钥选择 + +Normative: The system SHALL encrypt `.ktalbum` file body using the current version key (v2), store `header.Version=2`, and SHALL select decryption key by `header.Version` with a v1 fallback on checksum mismatch. + +#### Scenario: 导出使用 v2 + +- **GIVEN** 用户导出专辑 +- **WHEN** 系统生成 `.ktalbum` +- **THEN** `header.Version` MUST be `2` and the file body MUST be encrypted with the v2 key + +#### Scenario: 导入按版本解密并回退 + +- **GIVEN** 用户导入 `.ktalbum` +- **WHEN** 系统解密 zip body +- **THEN** 系统按 `header.Version` 选择密钥;若校验失败且版本不是 v1,则 MUST 尝试 v1 回退 diff --git a/openspec/changes/update-build-injected-symmetric-keys/tasks.md b/openspec/changes/update-build-injected-symmetric-keys/tasks.md new file mode 100644 index 00000000..1ac4c48a --- /dev/null +++ b/openspec/changes/update-build-injected-symmetric-keys/tasks.md @@ -0,0 +1,42 @@ +# 旧有对称加密密钥接入构建注入体系 - 任务清单 + +## 状态 + +**当前状态**: ✅ 已实现(代码与文档已同步) + +## 1. 发现与清点(留痕) + +- [x] 在 `sdk/**/*.go` 中定位所有包含硬编码对称密钥/secret 的位置 +- [x] 在 `tools/**/*.go` 中定位所有包含硬编码对称密钥/secret 的位置 +- [x] 逐一标注用途、调用链、数据格式(32-byte key / 可变长度 secret / XOR key) + +## 2. SDK:密钥注入适配 + +- [x] `sdk/signature/encryption.go`:KeyA/KeyB 改为可注入 `var`,默认保持原值 +- [x] `sdk/signature/album.go`:专辑 signature 字段密钥改为可注入 `var` +- [x] `sdk/audioPackage/enc/enc.go`:FixedSecret 改为可注入 `var` 并支持可变长度解混淆 +- [x] `sdk/server/server.go`:专辑导出 XOR v1/v2 key 改为可注入 `var`,使用时解混淆 +- [x] `sdk/server/server.go`:移除/替换不必要的硬编码签名清理密钥,改用 `signature.GetKeyA()` + +## 3. 工具链:ktalbum-tools 适配 + +- [x] `tools/ktalbum-tools/utils/header.go`:增加 v1/v2 key 与注入点、提供 `GetEncryptKeyByVersion` +- [x] `tools/ktalbum-tools/commands/extract.go`:按版本解密 + 校验失败回退 v1 +- [x] `tools/ktalbum-tools/commands/info.go`:按版本解密 + 校验失败回退 v1 + +## 4. 构建入口与模板 + +- [x] `sdk/setup_build_env.sh`:追加 KEY_A/KEY_B/KEY_ALBUM_* 映射 +- [x] `sdk/private_keys.template.env`:追加新增 KEY_* 项 + +## 5. 文档与规格同步 + +- [x] `BUILD_COMPATIBILITY.md`:补充 Build-Time Injected Keys +- [x] 新增能力规格:`openspec/specs/encrypted-outputs/spec.md` +- [x] 变更增量规格:`openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md` + +## 6. 验证 + +- [x] `go build ./...`(sdk) +- [ ] 可选:在本地提供 `sdk/private_keys.env` 并运行 `source sdk/setup_build_env.sh` 后构建,验证注入 keys 生效 +- [ ] 可选:为 `tools/ktalbum-tools` 构建时传入 `-ldflags -X`,验证能解密对应构建身份产物 diff --git "a/openspec/changes/update-build-injected-symmetric-keys/\344\270\264\346\227\266\350\257\264\346\230\216.md" "b/openspec/changes/update-build-injected-symmetric-keys/\344\270\264\346\227\266\350\257\264\346\230\216.md" new file mode 100644 index 00000000..45ef3c40 --- /dev/null +++ "b/openspec/changes/update-build-injected-symmetric-keys/\344\270\264\346\227\266\350\257\264\346\230\216.md" @@ -0,0 +1,62 @@ +# **已完成:把“旧有对称加密密钥”接入与授权流一致的构建注入机制,并同步代码+规格+文档(含 proposal/design/tasks/spec)。** + +sdk 与 ktalbum-tools 均已 `go build safe.` 通过。 + +**1) 我在仓库里定位到的“硬编码对称密钥/secret”与用途(留痕清单)** +- encryption.go + - `KeyToneSignatureEncryptionKeyA`(KeyA):用于加密签名 ID、派生动态密钥(PBKDF2) + - `KeyToneSignatureEncryptionKeyB`(KeyB):用于 `.ktsign` 导入/导出加密 +- album.go + - `KeyToneAlbumSignatureEncryptionKey`:用于专辑配置 `signature` 字段的“内层 AES-GCM”加密 +- enc.go + - `FixedSecret`:用于派生专辑配置 AES key:`SHA256(secret + last6(sha1(albumUUID)))` +- server.go + - `KeytoneEncryptKeyV1` / `KeytoneEncryptKeyV2`:用于 `.ktalbum` zip body 的版本化 XOR 加/解密 + - 另有一处硬编码字符串用于签名图片清理(实际逻辑并不依赖该字符串),已改为使用 `signature.GetKeyA()`,避免继续引入新的“写死密钥源” +- header.go + - 原 `KeytoneEncryptKey = "KeyTone2024SecretKey"`:用于工具解包/读信息时 XOR 解密 `.ktalbum` + +**2) 适配方式(与授权流一致)** +- 默认值仍然保留在源码中(未注入时保持开源构建行为完全不变) +- 支持通过 Go `-ldflags -X` 注入“XOR 混淆后的 hex” +- 运行时对注入值执行 `hex -> xorMask -> plaintext` 解混淆后再参与加/解密 + - 若误注入明文(非 hex),会回退为“直接使用该字符串”,保证不把构建卡死 + +**3) 实际改动位置(代码)** +- SDK(注入点 + 解混淆) + - encryption.go:KeyA/KeyB 从 `const` → `var`,并复用授权流的解混淆策略 + - album.go:专辑 signature 字段密钥从 `const` → `var` + - enc.go:`FixedSecret` 从 `const` → `var`(可变长度解混淆) + - server.go:`KeytoneEncryptKeyV1/V2` 从 `const` → `var`;导出时用 `getEncryptKeyByVersion(2)`(明文)而不是直接用变量值 +- 工具链(同步支持 v1/v2 + 注入) + - header.go:新增 `KeytoneEncryptKeyV1/V2` 与 `GetEncryptKeyByVersion` + - extract.go、info.go:按 header.Version 选 key,校验失败回退尝试 v1(与 SDK 行为一致) +- 构建注入入口 + - setup_build_env.sh:新增 KEY_A/KEY_B/KEY_ALBUM_* 映射 + - private_keys.template.env:新增对应 KEY_* 模板项 + +**4) 文档与规格同步(你要求的 proposal/design/tasks/spec 全部覆盖)** +- 文档: BUILD_COMPATIBILITY.md 增加 “Build-Time Injected Keys” 列表 +- OpenSpec(新能力规格 + 变更留痕) + - spec.md(新 capability:Encrypted Outputs) + - proposal.md + - design.md + - tasks.md + - spec.md +- 既有规格补充: spec.md 明确 KeyB “可构建注入” + +**5) 你审查时最关键的“注入变量名”对照** +- SDK(由 setup_build_env.sh 自动拼 EXTRA_LDFLAGS) + - `KeyTone/signature.KeyToneSignatureEncryptionKeyA` ← `KEY_A` + - `KeyTone/signature.KeyToneSignatureEncryptionKeyB` ← `KEY_B` + - `KeyTone/signature.KeyToneAlbumSignatureEncryptionKey` ← `KEY_ALBUM_SIGNATURE_FIELD` + - `KeyTone/server.KeytoneEncryptKeyV1` ← `KEY_ALBUM_EXPORT_V1` + - `KeyTone/server.KeytoneEncryptKeyV2` ← `KEY_ALBUM_EXPORT_V2` + - `KeyTone/audioPackage/enc.FixedSecret` ← `KEY_ALBUM_CONFIG_SECRET` +- ktalbum-tools(模块路径不同,需要你在 tools 的构建命令里自行 `-ldflags -X`) + - `ktalbum-tools/utils.KeytoneEncryptKeyV1` + - `ktalbum-tools/utils.KeytoneEncryptKeyV2` + +如果你希望我再把“ktalbum-tools 的构建脚本/说明”也做成和 SDK 一样的 `setup_build_env.sh + private_keys.template.env`(避免你手写 `-ldflags -X`),我可以继续补齐,但会新增文件与文档说明(你确认后我再做)。 + +已进行更改。 \ No newline at end of file diff --git a/openspec/specs/encrypted-outputs/spec.md b/openspec/specs/encrypted-outputs/spec.md new file mode 100644 index 00000000..b3cb1005 --- /dev/null +++ b/openspec/specs/encrypted-outputs/spec.md @@ -0,0 +1,78 @@ +# Encrypted Outputs + +## Purpose + +定义 KeyTone 在“加密产物”上的兼容性边界与构建身份(build identity)的关联。 + +本能力覆盖: + +- 专辑导出文件(`.ktalbum`)加密 +- 专辑配置加密(AES-GCM,基于 albumUUID 派生 key) +- 签名导出文件(`.ktsign`)加密 +- 构建时密钥注入(不改源码、开源构建默认可用) + +## Requirements + +### Requirement: 构建时密钥注入 + +Normative: The system SHALL support overriding selected symmetric keys/secrets at build-time via Go `-ldflags -X` using XOR-obfuscated hex values; when not injected, it MUST fall back to the default hardcoded values so open-source builds remain functional. + +#### Scenario: 开源构建未注入 + +- **GIVEN** 用户从公开源码直接构建且未提供私钥文件/注入参数 +- **WHEN** 系统进行涉及加密的功能 +- **THEN** 使用源码默认密钥/secret,并保持可用 + +#### Scenario: 私有构建注入 + +- **GIVEN** 用户提供私钥(例如通过 `sdk/setup_build_env.sh` 生成 `EXTRA_LDFLAGS`) +- **WHEN** 使用 `-ldflags -X` 注入混淆值构建 +- **THEN** 构建产物的加密身份发生变化,加密产物可与未注入构建不兼容(预期) + +### Requirement: `.ktalbum` 版本化 XOR 加密 + +Normative: The system SHALL encrypt the `.ktalbum` zip body using a versioned XOR key; it MUST store the key version in the file header and MUST select the decryption key by header version, with a v1 fallback on checksum mismatch. + +#### Scenario: 导出写入 v2 + +- **GIVEN** 用户导出专辑 +- **WHEN** 系统生成 `.ktalbum` +- **THEN** MUST set `header.Version=2` and encrypt with v2 key + +#### Scenario: 导入按版本选择密钥 + +- **GIVEN** 用户导入 `.ktalbum` +- **WHEN** 系统解析文件头 +- **THEN** MUST decrypt using the key corresponding to `header.Version` + +### Requirement: 专辑配置 AES-GCM 派生密钥 + +Normative: The system SHALL derive a 32-byte AES key using `SHA256(secret + last6(sha1(albumUUID)))`, where `secret` is configurable via build-time injection; the encrypted config bytes MUST be stored as `nonce + ciphertext`. + +#### Scenario: 使用默认 secret 派生 key + +- **GIVEN** 未注入自定义 secret +- **WHEN** 系统派生专辑配置 AES key +- **THEN** 使用默认 secret,派生结果与当前版本一致 + +#### Scenario: 注入 secret 改变派生 key + +- **GIVEN** 构建时注入了自定义 secret +- **WHEN** 系统派生专辑配置 AES key +- **THEN** 派生结果与默认构建不同,导致加密配置不可跨构建身份通用(预期) + +### Requirement: 签名导出 KeyB + +Normative: The system SHALL encrypt `.ktsign` using KeyB, where KeyB SHALL be build-injectable; when not injected, it MUST use the default hardcoded value. + +#### Scenario: KeyB 未注入 + +- **GIVEN** 未注入 KeyB +- **WHEN** 导出签名 +- **THEN** 使用默认 KeyB 加密 `.ktsign` + +#### Scenario: KeyB 注入 + +- **GIVEN** 注入了 KeyB +- **WHEN** 导出签名 +- **THEN** `.ktsign` MUST be encrypted with the injected KeyB diff --git a/openspec/specs/signature-management/spec.md b/openspec/specs/signature-management/spec.md index f6380286..ed1bca8e 100644 --- a/openspec/specs/signature-management/spec.md +++ b/openspec/specs/signature-management/spec.md @@ -78,7 +78,7 @@ Normative: The system SHALL 在删除前显示确认对话框;客户端 MUST ### Requirement: 导出签名 -Normative: The client SHALL 通过 `POST /signature/export` 请求导出;后端 MUST 返回经 KeyB 加密并以十六进制编码的 `.ktsign` 内容,客户端 MUST 在用户确认保存后才提示成功。 +Normative: The client SHALL 通过 `POST /signature/export` 请求导出;后端 MUST 返回经 KeyB(可构建注入;未注入时使用源码默认值)加密并以十六进制编码的 `.ktsign` 内容,客户端 MUST 在用户确认保存后才提示成功。 #### Scenario: 导出成功 @@ -96,7 +96,7 @@ Normative: The client SHALL 通过 `POST /signature/export` 请求导出;后 ### Requirement: 导入签名 -Normative: The client SHALL 上传 `.ktsign` 文件到 `POST /signature/import`;后端 MUST 使用 KeyB 解密并校验字段,当签名已存在时返回 `409` 且 `conflict: true`;覆盖流程 SHALL 通过 `POST /signature/import-confirm` 携带原始加密字符串和 `overwrite` 标识完成导入。 +Normative: The client SHALL 上传 `.ktsign` 文件到 `POST /signature/import`;后端 MUST 使用 KeyB(可构建注入;未注入时使用源码默认值)解密并校验字段,当签名已存在时返回 `409` 且 `conflict: true`;覆盖流程 SHALL 通过 `POST /signature/import-confirm` 携带原始加密字符串和 `overwrite` 标识完成导入。 #### Scenario: 导入成功 diff --git a/sdk/audioPackage/enc/enc.go b/sdk/audioPackage/enc/enc.go index c4b9682d..5f958a47 100644 --- a/sdk/audioPackage/enc/enc.go +++ b/sdk/audioPackage/enc/enc.go @@ -10,8 +10,41 @@ import ( "KeyTone/signature" ) -// FixedSecret is the fixed secret prefix for album config enc/dec per spec. -const FixedSecret = "LuSrackhall_KeyTone_2024_Signature_66688868686688" +// ============================== +// 对称加密种子(用于派生专辑配置 AES 密钥) +// 注意:该 secret 由源码提供默认值(开源构建保持原行为),也可在构建时通过 -ldflags 注入 +// 注入的值应为经过 XOR 混淆后的 Hex 字符串(与授权流一致) +// ============================== + +// xorMask 用于混淆密钥的掩码,必须与授权流一致 +var xorMask = []byte{0x55, 0xAA, 0x33, 0xCC, 0x99, 0x66, 0x11, 0xEE, 0x77, 0xBB, 0x22, 0xDD, 0x88, 0x44, 0xFF, 0x00} + +// DefaultFixedSecret is the fixed secret prefix for album config enc/dec per spec. +const DefaultFixedSecret = "LuSrackhall_KeyTone_2024_Signature_66688868686688" + +// FixedSecret is the build-injectable secret seed (default: DefaultFixedSecret). +// 若被注入,则值应为 XOR 混淆后的 Hex 字符串。 +var FixedSecret = DefaultFixedSecret + +func deobfuscateString(obfuscatedHex string) string { + obfuscated, err := hex.DecodeString(obfuscatedHex) + if err != nil { + // 非 hex(可能是默认明文,或用户错误注入了明文) + return obfuscatedHex + } + realBytes := make([]byte, len(obfuscated)) + for i, b := range obfuscated { + realBytes[i] = b ^ xorMask[i%len(xorMask)] + } + return string(realBytes) +} + +func getFixedSecret() string { + if FixedSecret == DefaultFixedSecret { + return DefaultFixedSecret + } + return deobfuscateString(FixedSecret) +} // DeriveKey derives a 32-byte AES key using SHA256(FixedSecret + last6(sha1(albumUUID))). // Assumption: albumUUID is the directory name of the album folder unless specified otherwise. @@ -26,7 +59,7 @@ func DeriveKey(albumUUID string) []byte { hexStr = pad + hexStr } suffix := hexStr[len(hexStr)-6:] - seed := FixedSecret + suffix + seed := getFixedSecret() + suffix sum := sha256.Sum256([]byte(seed)) key := make([]byte, 32) copy(key, sum[:]) diff --git a/sdk/private_keys.template.env b/sdk/private_keys.template.env index 77e7552b..e4eaa715 100644 --- a/sdk/private_keys.template.env +++ b/sdk/private_keys.template.env @@ -22,4 +22,26 @@ KEY_K="PLACEHOLDER_KEY_K_REPLACE_ME_32B" KEY_Y="PLACEHOLDER_KEY_Y_REPLACE_ME_32B" # 密钥N:用于授权文件的最终加密 (建议 32 bytes) -KEY_N="PLACEHOLDER_KEY_N_REPLACE_ME_32B" \ No newline at end of file +KEY_N="PLACEHOLDER_KEY_N_REPLACE_ME_32B" + +# ============================== +# 旧有对称加密能力(现已支持构建时注入) +# ============================== + +# 密钥A:用于加密签名ID和生成动态密钥 (建议 32 bytes) +KEY_A="PLACEHOLDER_KEY_A_REPLACE_ME_32B" + +# 密钥B:用于导出/导入签名文件加密 (建议 32 bytes) +KEY_B="PLACEHOLDER_KEY_B_REPLACE_ME_32B" + +# 专辑 signature 字段内层加密密钥 (建议 32 bytes) +KEY_ALBUM_SIGNATURE_FIELD="PLACEHOLDER_ALBUM_SIGNATURE_FIELD_KEY_32B" + +# 专辑导出文件 XOR 密钥 v1(旧版本,用于向后兼容) +KEY_ALBUM_EXPORT_V1="PLACEHOLDER_ALBUM_EXPORT_KEY_V1" + +# 专辑导出文件 XOR 密钥 v2(当前版本) +KEY_ALBUM_EXPORT_V2="PLACEHOLDER_ALBUM_EXPORT_KEY_V2" + +# 专辑配置加密派生 secret(可变长度;用于派生 AES key,不要求 32 bytes) +KEY_ALBUM_CONFIG_SECRET="PLACEHOLDER_ALBUM_CONFIG_SECRET" \ No newline at end of file diff --git a/sdk/server/server.go b/sdk/server/server.go index c254daa8..e64629aa 100644 --- a/sdk/server/server.go +++ b/sdk/server/server.go @@ -33,6 +33,7 @@ import ( "crypto" "crypto/sha256" "encoding/binary" + "encoding/hex" "encoding/json" "fmt" "io" @@ -53,14 +54,52 @@ const ( KeytoneVersion = "1.0.0" // 当前版本号 KeytoneFileSignature = "KTALBUM" // 文件签名 KeytoneFileVersion = 1 // 文件版本(已废弃,仅用于向后兼容) +) + +// ============================== +// 专辑导出文件对称密钥(版本化) +// 注意:这些变量不再是 const,而是 var,以便在编译时通过 -ldflags 进行注入 +// 注入的值应为经过 XOR 混淆后的 Hex 字符串(与授权流一致) +// ============================== - // 版本化加密密钥 - KeytoneEncryptKeyV1 = "KeyTone2024SecretKey" // v1 密钥(旧版本,用于向后兼容) - KeytoneEncryptKeyV2 = "KeyTone2025AlbumSecureEncryptionKeyV2" // v2 密钥(当前版本) - KeytoneEncryptKeyCurrent = KeytoneEncryptKeyV2 // 当前使用的密钥版本 - KeytoneEncryptKey = KeytoneEncryptKeyV1 // 已废弃:向后兼容,请使用 KeytoneEncryptKeyV1 +// xorMask 用于混淆密钥的掩码,必须与授权流一致 +var xorMask = []byte{0x55, 0xAA, 0x33, 0xCC, 0x99, 0x66, 0x11, 0xEE, 0x77, 0xBB, 0x22, 0xDD, 0x88, 0x44, 0xFF, 0x00} + +// 默认开源密钥常量(明文) +const ( + DefaultKeytoneEncryptKeyV1 = "KeyTone2024SecretKey" // v1 密钥(旧版本,用于向后兼容) + DefaultKeytoneEncryptKeyV2 = "KeyTone2025AlbumSecureEncryptionKeyV2" // v2 密钥(当前版本) + DefaultKeytoneEncryptKeyCurrent = DefaultKeytoneEncryptKeyV2 ) +// 版本化加密密钥(可注入) +var ( + KeytoneEncryptKeyV1 = DefaultKeytoneEncryptKeyV1 + KeytoneEncryptKeyV2 = DefaultKeytoneEncryptKeyV2 + KeytoneEncryptKeyCurrent = DefaultKeytoneEncryptKeyCurrent + KeytoneEncryptKey = KeytoneEncryptKeyV1 // 已废弃:向后兼容,请使用 KeytoneEncryptKeyV1 +) + +func deobfuscateString(obfuscatedHex string) string { + obfuscated, err := hex.DecodeString(obfuscatedHex) + if err != nil { + // 非 hex(可能是默认明文,或用户错误注入了明文) + return obfuscatedHex + } + realBytes := make([]byte, len(obfuscated)) + for i, b := range obfuscated { + realBytes[i] = b ^ xorMask[i%len(xorMask)] + } + return string(realBytes) +} + +func getPlainEncryptKey(value string, defaultValue string) string { + if value == defaultValue { + return defaultValue + } + return deobfuscateString(value) +} + // KeytoneAlbumMeta 用于存储专辑元数据 type KeytoneAlbumMeta struct { MagicNumber string `json:"magicNumber"` @@ -339,13 +378,13 @@ func xorCrypt(data []byte, key string) []byte { func getEncryptKeyByVersion(version uint8) string { switch version { case 1: - return KeytoneEncryptKeyV1 + return getPlainEncryptKey(KeytoneEncryptKeyV1, DefaultKeytoneEncryptKeyV1) case 2: - return KeytoneEncryptKeyV2 + return getPlainEncryptKey(KeytoneEncryptKeyV2, DefaultKeytoneEncryptKeyV2) default: // 未知版本,返回当前密钥 logger.Warn("未知的文件版本号,使用当前密钥", "version", version) - return KeytoneEncryptKeyCurrent + return getPlainEncryptKey(KeytoneEncryptKeyCurrent, DefaultKeytoneEncryptKeyCurrent) } } @@ -372,7 +411,7 @@ func decryptAlbumData(encryptedData []byte, header KeytoneFileHeader) ([]byte, u // 如果校验失败且版本不是v1,尝试使用v1密钥回退 if header.Version != 1 { logger.Warn("使用版本密钥解密失败,尝试v1密钥回退", "version", header.Version) - zipData = xorCrypt(encryptedData, KeytoneEncryptKeyV1) + zipData = xorCrypt(encryptedData, getEncryptKeyByVersion(1)) checksum = sha256.Sum256(zipData) if checksum == header.Checksum { logger.Info("使用v1密钥成功解密", "file_version", header.Version) @@ -388,8 +427,8 @@ func ServerRun() { // 启动签名名片图片清理任务(在SDK启动5秒后执行一次) go func() { time.Sleep(5 * time.Second) - encryptionKey := []byte("KeyTone2024SignatureEncryptionKey"[:32]) // 截取前32字节 - if err := signature.CleanupOrphanCardImages(encryptionKey); err != nil { + // CleanupOrphanCardImages 的参数为历史兼容保留;实际解密逻辑使用 KeyA(支持构建注入) + if err := signature.CleanupOrphanCardImages(signature.GetKeyA()); err != nil { logger.Error("签名名片图片清理任务执行失败", "error", err.Error()) } }() @@ -1525,7 +1564,7 @@ func keytonePkgRouters(r *gin.Engine) { checksum := sha256.Sum256(zipData) // 加密 zip 数据(使用当前版本密钥 v2) - encryptedData := xorCrypt(zipData, KeytoneEncryptKeyCurrent) + encryptedData := xorCrypt(zipData, getEncryptKeyByVersion(2)) // 创建文件头(使用版本号 2) header := KeytoneFileHeader{ diff --git a/sdk/setup_build_env.sh b/sdk/setup_build_env.sh index a09573aa..ed3bdd71 100755 --- a/sdk/setup_build_env.sh +++ b/sdk/setup_build_env.sh @@ -40,11 +40,23 @@ OBFUSCATOR_TOOL="../tools/key-obfuscator/main.go" # 格式说明: # KEY_NAME : 在 private_keys.env 文件中的键名 (如 KEY_F) # GO_VAR : 在 Go 代码中接收注入的变量全路径 (如 KeyTone/signature.KeyToneAuthRequestEncryptionKeyF) +# +# 说明: +# - 推荐 32 字节密钥(AES-256 标准) +# - 部分“种子/口令”可能不是 32 字节(例如专辑配置派生 secret),混淆工具会给出长度警告但仍可工作 KEYS_TO_PROCESS=( "KEY_F:KeyTone/signature.KeyToneAuthRequestEncryptionKeyF" "KEY_K:KeyTone/signature.KeyToneAuthRequestEncryptionKeyK" "KEY_Y:KeyTone/signature.KeyToneAuthGrantEncryptionKeyY" "KEY_N:KeyTone/signature.KeyToneAuthGrantEncryptionKeyN" + + # 旧有对称加密密钥(本次补齐到构建注入体系) + "KEY_A:KeyTone/signature.KeyToneSignatureEncryptionKeyA" + "KEY_B:KeyTone/signature.KeyToneSignatureEncryptionKeyB" + "KEY_ALBUM_SIGNATURE_FIELD:KeyTone/signature.KeyToneAlbumSignatureEncryptionKey" + "KEY_ALBUM_EXPORT_V1:KeyTone/server.KeytoneEncryptKeyV1" + "KEY_ALBUM_EXPORT_V2:KeyTone/server.KeytoneEncryptKeyV2" + "KEY_ALBUM_CONFIG_SECRET:KeyTone/audioPackage/enc.FixedSecret" # 示例:新增密钥时,取消注释并修改下行 # "KEY_NEW:KeyTone/signature.KeyToneNewKey" ) diff --git a/sdk/signature/album.go b/sdk/signature/album.go index ddc1338f..590814a3 100644 --- a/sdk/signature/album.go +++ b/sdk/signature/album.go @@ -25,6 +25,15 @@ import ( "fmt" ) +// ============================== +// 专辑签名字段对称密钥变量定义 +// 注意:该密钥由源码提供默认值(开源构建保持原行为),也可在构建时通过 -ldflags 注入 +// 注入的值应为经过 XOR 混淆后的 Hex 字符串(与授权流一致) +// ============================== + +// 默认开源密钥常量(明文) +const DefaultAlbumSignatureFieldKey = "KeyTone2024Album_Signature_Field_EncryptionKey_32Bytes" + // KeyToneAlbumSignatureEncryptionKey 专辑签名字段专用加密密钥 // // 用途:加密专辑配置中的signature字段内容 @@ -35,7 +44,7 @@ import ( // - 此密钥独立于签名管理的KeyA/KeyB,职责分离 // - 专辑配置本身已有基于albumUUID的派生密钥保护(外层加密) // - 此密钥用于signature字段的内层加密,双重保护 -const KeyToneAlbumSignatureEncryptionKey = "KeyTone2024Album_Signature_Field_EncryptionKey_32Bytes" +var KeyToneAlbumSignatureEncryptionKey = DefaultAlbumSignatureFieldKey // GetAlbumSignatureKey 获取专辑签名加密密钥(32字节) // @@ -46,14 +55,18 @@ const KeyToneAlbumSignatureEncryptionKey = "KeyTone2024Album_Signature_Field_Enc // - 确保密钥长度符合AES-256要求 // - 如密钥字符串不足32字节,自动填充0 func GetAlbumSignatureKey() []byte { - key := []byte(KeyToneAlbumSignatureEncryptionKey) - // 确保密钥长度为32字节 - if len(key) < 32 { - for len(key) < 32 { - key = append(key, 0) + // 1. 如果变量值等于默认常量,说明未注入,直接使用默认明文密钥 + if KeyToneAlbumSignatureEncryptionKey == DefaultAlbumSignatureFieldKey { + key := []byte(DefaultAlbumSignatureFieldKey) + if len(key) < 32 { + for len(key) < 32 { + key = append(key, 0) + } } + return key[:32] } - return key[:32] + // 2. 否则说明已被注入,执行解混淆逻辑(hex -> xor -> plaintext) + return deobfuscateKey(KeyToneAlbumSignatureEncryptionKey) } // EncryptAlbumSignatureField 加密专辑配置中的签名字段 diff --git a/sdk/signature/encryption.go b/sdk/signature/encryption.go index 11873c82..aeedcb63 100644 --- a/sdk/signature/encryption.go +++ b/sdk/signature/encryption.go @@ -26,42 +26,60 @@ import ( "golang.org/x/crypto/pbkdf2" ) -// 密钥常量定义 +// ============================== +// 签名相关对称密钥变量定义 +// 注意:这些变量不再是 const,而是 var,以便在编译时通过 -ldflags 进行注入 +// 注入的值应为经过 XOR 混淆后的 Hex 字符串(与授权流一致) +// ============================== + +// 定义默认的开源密钥常量,用于运行时比对 +const ( + DefaultKeyA = "KeyTone2024Signature_KeyA_SecureEncryptionKeyForIDEncryption" + DefaultKeyB = "KeyTone2024Signature_KeyB_SuperSecureEncryptionKeyForExportImportOperation" +) // KeyToneSignatureEncryptionKeyA 密钥A:用于加密签名ID和生成动态密钥 // 安全等级:标准 // 长度: 32字节 -const KeyToneSignatureEncryptionKeyA = "KeyTone2024Signature_KeyA_SecureEncryptionKeyForIDEncryption" +var KeyToneSignatureEncryptionKeyA = DefaultKeyA // KeyToneSignatureEncryptionKeyB 密钥B:用于导出/导入加密,安全级别更高 // 安全等级:高 // 长度: 32字节 -const KeyToneSignatureEncryptionKeyB = "KeyTone2024Signature_KeyB_SuperSecureEncryptionKeyForExportImportOperation" +var KeyToneSignatureEncryptionKeyB = DefaultKeyB // GetKeyA 获取密钥A (32字节) // 用途:加密签名ID、生成动态密钥 func GetKeyA() []byte { - key := []byte(KeyToneSignatureEncryptionKeyA) - if len(key) < 32 { - // 如果密钥长度不足,需要填充 - for len(key) < 32 { - key = append(key, 0) + // 1. 如果变量值等于默认常量,说明未注入,直接使用默认明文密钥 + if KeyToneSignatureEncryptionKeyA == DefaultKeyA { + key := []byte(DefaultKeyA) + if len(key) < 32 { + for len(key) < 32 { + key = append(key, 0) + } } + return key[:32] } - return key[:32] + // 2. 否则说明已被注入,执行解混淆逻辑(hex -> xor -> plaintext) + return deobfuscateKey(KeyToneSignatureEncryptionKeyA) } // GetKeyB 获取密钥B (32字节) // 用途:导出/导入签名文件加密 func GetKeyB() []byte { - key := []byte(KeyToneSignatureEncryptionKeyB) - if len(key) < 32 { - // 如果密钥长度不足,需要填充 - for len(key) < 32 { - key = append(key, 0) + // 1. 如果变量值等于默认常量,说明未注入,直接使用默认明文密钥 + if KeyToneSignatureEncryptionKeyB == DefaultKeyB { + key := []byte(DefaultKeyB) + if len(key) < 32 { + for len(key) < 32 { + key = append(key, 0) + } } + return key[:32] } - return key[:32] + // 2. 否则说明已被注入,执行解混淆逻辑(hex -> xor -> plaintext) + return deobfuscateKey(KeyToneSignatureEncryptionKeyB) } // GenerateDynamicKey 根据加密的签名ID生成动态密钥 diff --git a/tools/ktalbum-tools/commands/extract.go b/tools/ktalbum-tools/commands/extract.go index 074cdbd5..ea06fbc8 100644 --- a/tools/ktalbum-tools/commands/extract.go +++ b/tools/ktalbum-tools/commands/extract.go @@ -43,13 +43,21 @@ func Extract(inputFile, outputFile string, verbose bool) error { return fmt.Errorf("读取加密数据失败: %v", err) } - // 解密数据 - zipData := utils.XorCrypt(encryptedData, utils.KeytoneEncryptKey) + // 解密数据(按版本选择密钥) + decryptKey := utils.GetEncryptKeyByVersion(header.Version) + zipData := utils.XorCrypt(encryptedData, decryptKey) // 验证校验和 checksum := sha256.Sum256(zipData) if checksum != header.Checksum { - return fmt.Errorf("文件校验失败,文件可能已损坏") + // 与 SDK 一致:若版本不是 v1,尝试使用 v1 密钥回退 + if header.Version != 1 { + zipData = utils.XorCrypt(encryptedData, utils.GetEncryptKeyByVersion(1)) + checksum = sha256.Sum256(zipData) + } + if checksum != header.Checksum { + return fmt.Errorf("文件校验失败,文件可能已损坏或密钥不匹配") + } } // 写入解密后的zip数据 diff --git a/tools/ktalbum-tools/commands/info.go b/tools/ktalbum-tools/commands/info.go index e395035a..6aaa78a4 100644 --- a/tools/ktalbum-tools/commands/info.go +++ b/tools/ktalbum-tools/commands/info.go @@ -43,13 +43,21 @@ func GetFileInfo(filePath string) (*FileInfo, error) { return nil, fmt.Errorf("读取加密数据失败: %v", err) } - // 解密数据 - zipData := utils.XorCrypt(encryptedData, utils.KeytoneEncryptKey) + // 解密数据(按版本选择密钥) + decryptKey := utils.GetEncryptKeyByVersion(header.Version) + zipData := utils.XorCrypt(encryptedData, decryptKey) // 验证校验和 checksum := utils.CalculateChecksum(zipData) if !bytes.Equal(checksum[:], header.Checksum[:]) { - return nil, fmt.Errorf("文件校验失败,文件可能已损坏") + // 与 SDK 一致:若版本不是 v1,尝试使用 v1 密钥回退 + if header.Version != 1 { + zipData = utils.XorCrypt(encryptedData, utils.GetEncryptKeyByVersion(1)) + checksum = utils.CalculateChecksum(zipData) + } + if !bytes.Equal(checksum[:], header.Checksum[:]) { + return nil, fmt.Errorf("文件校验失败,文件可能已损坏或密钥不匹配") + } } // 从 zip 数据中读取 .keytone-album 文件 diff --git a/tools/ktalbum-tools/utils/header.go b/tools/ktalbum-tools/utils/header.go index 7f23fc3c..919cc772 100644 --- a/tools/ktalbum-tools/utils/header.go +++ b/tools/ktalbum-tools/utils/header.go @@ -1,12 +1,69 @@ package utils -import "time" +import ( + "encoding/hex" + "time" +) const ( KeytoneFileSignature = "KTALBUM" - KeytoneEncryptKey = "KeyTone2024SecretKey" ) +// ============================== +// 专辑导出文件对称密钥(版本化) +// 注意:这些变量不再是 const,而是 var,以便在编译时通过 -ldflags 进行注入 +// 注入的值应为经过 XOR 混淆后的 Hex 字符串(与 SDK 授权流一致) +// ============================== + +// xorMask 用于混淆密钥的掩码,必须与 SDK 授权流一致 +var xorMask = []byte{0x55, 0xAA, 0x33, 0xCC, 0x99, 0x66, 0x11, 0xEE, 0x77, 0xBB, 0x22, 0xDD, 0x88, 0x44, 0xFF, 0x00} + +// 默认开源密钥常量(明文) +const ( + DefaultKeytoneEncryptKeyV1 = "KeyTone2024SecretKey" // v1 + DefaultKeytoneEncryptKeyV2 = "KeyTone2025AlbumSecureEncryptionKeyV2" // v2 +) + +// 版本化加密密钥(可注入) +var ( + KeytoneEncryptKeyV1 = DefaultKeytoneEncryptKeyV1 + KeytoneEncryptKeyV2 = DefaultKeytoneEncryptKeyV2 + // 向后兼容:旧变量名仍保留(等价于 v1) + KeytoneEncryptKey = KeytoneEncryptKeyV1 +) + +func deobfuscateString(obfuscatedHex string) string { + obfuscated, err := hex.DecodeString(obfuscatedHex) + if err != nil { + return obfuscatedHex + } + realBytes := make([]byte, len(obfuscated)) + for i, b := range obfuscated { + realBytes[i] = b ^ xorMask[i%len(xorMask)] + } + return string(realBytes) +} + +func getPlainEncryptKey(value string, defaultValue string) string { + if value == defaultValue { + return defaultValue + } + return deobfuscateString(value) +} + +// GetEncryptKeyByVersion 根据文件头版本号返回对应的明文密钥。 +func GetEncryptKeyByVersion(version uint8) string { + switch version { + case 1: + return getPlainEncryptKey(KeytoneEncryptKeyV1, DefaultKeytoneEncryptKeyV1) + case 2: + return getPlainEncryptKey(KeytoneEncryptKeyV2, DefaultKeytoneEncryptKeyV2) + default: + // 未知版本:保守回退到 v2(与 SDK 保持一致的“当前版本”语义) + return getPlainEncryptKey(KeytoneEncryptKeyV2, DefaultKeytoneEncryptKeyV2) + } +} + type KeytoneFileHeader struct { Signature [7]byte Version uint8 From 3eab7347a411bc2d1012ca22546e2e27bb6a0e94 Mon Sep 17 00:00:00 2001 From: LuSrackhall <3647637206@qq.com> Date: Wed, 31 Dec 2025 17:35:53 +0800 Subject: [PATCH 2/7] =?UTF-8?q?chore(tools):=20=E5=AE=8C=E5=96=84=20ktalbu?= =?UTF-8?q?m-tools=20=E6=94=AF=E6=8C=81=E6=9E=84=E5=BB=BA=E6=97=B6?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E6=B3=A8=E5=85=A5=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=80=A7=E4=B8=8E=E8=B0=83=E8=AF=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD(=E4=BB=85=E7=94=B1ai=E5=AE=9E=E7=8E=B0,=20=E7=9B=AE?= =?UTF-8?q?=E5=89=8D=E6=9C=AA=E9=AA=8C=E8=AF=81,=20=E5=9B=A0=E4=B8=BA?= =?UTF-8?q?=E8=BF=99=E4=B8=AA=E5=B7=A5=E5=85=B7=E5=87=A0=E4=B9=8E=E6=B2=A1?= =?UTF-8?q?=E7=94=A8=E8=BF=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design.md | 13 +++ .../proposal.md | 8 ++ .../specs/encrypted-outputs/spec.md | 2 + .../tasks.md | 9 +- openspec/specs/encrypted-outputs/spec.md | 2 + tools/ktalbum-tools/README.md | 50 +++++++-- tools/ktalbum-tools/build.sh | 21 +++- tools/ktalbum-tools/commands/extract.go | 38 ++++--- tools/ktalbum-tools/commands/info.go | 40 ++++--- tools/ktalbum-tools/private_keys.template.env | 11 ++ tools/ktalbum-tools/setup_build_env.sh | 100 ++++++++++++++++++ tools/ktalbum-tools/utils/header.go | 29 +++++ 12 files changed, 284 insertions(+), 39 deletions(-) create mode 100644 tools/ktalbum-tools/private_keys.template.env create mode 100644 tools/ktalbum-tools/setup_build_env.sh diff --git a/openspec/changes/update-build-injected-symmetric-keys/design.md b/openspec/changes/update-build-injected-symmetric-keys/design.md index 57699fe8..9fd08730 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/design.md +++ b/openspec/changes/update-build-injected-symmetric-keys/design.md @@ -60,6 +60,19 @@ - `sdk/private_keys.template.env` 增加对应 KEY_* 项 - `sdk/setup_build_env.sh` 增加 `KEYS_TO_PROCESS` 映射,使本地私有构建可以自动生成 `EXTRA_LDFLAGS` +### ktalbum-tools(本地调试工具) + +ktalbum-tools 的目标是便于本地查看/调试 `.ktalbum` 文件,因此它需要在“一个二进制”中尽量兼容两类产物: + +- 开源默认密钥产物 +- 私有构建注入密钥产物 + +实现方式: + +- 构建注入脚本:`tools/ktalbum-tools/setup_build_env.sh`(默认复用 `sdk/private_keys.env`,也可使用本地 `tools/ktalbum-tools/private_keys.env`) +- 一键构建:`tools/ktalbum-tools/build.sh` 会自动合并并应用 `EXTRA_LDFLAGS` +- 运行时解密:按顺序尝试“注入密钥 → 默认密钥”,并在校验失败时按 SDK 策略回退尝试 v1 + ## Compatibility Notes - 未注入:行为与当前开源版本完全一致 diff --git a/openspec/changes/update-build-injected-symmetric-keys/proposal.md b/openspec/changes/update-build-injected-symmetric-keys/proposal.md index 216b8eca..8ff05741 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/proposal.md +++ b/openspec/changes/update-build-injected-symmetric-keys/proposal.md @@ -15,6 +15,8 @@ - 专辑配置加密派生 secret(FixedSecret) - 统一注入方式:注入值为 XOR 混淆后的 hex 字符串;运行时自动解混淆。 - 更新构建脚本与模板:`sdk/setup_build_env.sh` 与 `sdk/private_keys.template.env` 增加新 key 项。 +- 为本地调试工具 ktalbum-tools 补齐注入脚本:`tools/ktalbum-tools/setup_build_env.sh`(默认复用 `sdk/private_keys.env`)。 +- ktalbum-tools 构建脚本会自动应用 `EXTRA_LDFLAGS`:`tools/ktalbum-tools/build.sh`。 - 更新文档:`BUILD_COMPATIBILITY.md` 补充“Build-Time Injected Keys”列表。 ## Non-Goals @@ -33,7 +35,11 @@ - `sdk/private_keys.template.env` - `tools/ktalbum-tools/utils/header.go` - `tools/ktalbum-tools/commands/*.go` + - `tools/ktalbum-tools/setup_build_env.sh` + - `tools/ktalbum-tools/build.sh` + - `tools/ktalbum-tools/private_keys.template.env` - `BUILD_COMPATIBILITY.md` + - `BUILD_COMPATIBILITY.zh-CN.md` - Affected specs: - New capability: `openspec/specs/encrypted-outputs/spec.md` @@ -50,3 +56,5 @@ | 专辑配置派生 secret | `LuSrackhall_KeyTone_2024_Signature_66688868686688` | `KeyTone/audioPackage/enc.FixedSecret` | 派生 AES key:`SHA256(secret + last6(sha1(albumUUID)))` | 注:工具链 `tools/ktalbum-tools` 也提供同等注入点(模块路径不同)。 + +另外:ktalbum-tools 为本地查看/调试用途,解密 `.ktalbum` 时会按顺序尝试“注入密钥 → 开源默认密钥”,并在校验失败时回退尝试 v1,以便同一构建可同时兼容开源与私有两类产物。 diff --git a/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md index fbf7a5d6..7a7b7f7a 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md +++ b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md @@ -22,6 +22,8 @@ Normative: The system SHALL keep default symmetric keys/secrets hardcoded in sou Normative: The system SHALL encrypt `.ktalbum` file body using the current version key (v2), store `header.Version=2`, and SHALL select decryption key by `header.Version` with a v1 fallback on checksum mismatch. +Note: For local debugging, ktalbum-tools MAY additionally attempt both injected and default open-source keys for the same version (in that order) before declaring failure. This is tooling behavior and does not change the core app compatibility boundary. + #### Scenario: 导出使用 v2 - **GIVEN** 用户导出专辑 diff --git a/openspec/changes/update-build-injected-symmetric-keys/tasks.md b/openspec/changes/update-build-injected-symmetric-keys/tasks.md index 1ac4c48a..a90f7f08 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/tasks.md +++ b/openspec/changes/update-build-injected-symmetric-keys/tasks.md @@ -21,8 +21,12 @@ ## 3. 工具链:ktalbum-tools 适配 - [x] `tools/ktalbum-tools/utils/header.go`:增加 v1/v2 key 与注入点、提供 `GetEncryptKeyByVersion` -- [x] `tools/ktalbum-tools/commands/extract.go`:按版本解密 + 校验失败回退 v1 -- [x] `tools/ktalbum-tools/commands/info.go`:按版本解密 + 校验失败回退 v1 +- [x] `tools/ktalbum-tools/utils/header.go`:增加 `GetDecryptKeyCandidatesByVersion`(注入→默认双候选)以兼容开源/私有两类产物 +- [x] `tools/ktalbum-tools/commands/extract.go`:按版本循环尝试候选密钥 + 校验失败回退 v1 候选 +- [x] `tools/ktalbum-tools/commands/info.go`:按版本循环尝试候选密钥 + 校验失败回退 v1 候选 +- [x] `tools/ktalbum-tools/setup_build_env.sh`:复用 sdk 私钥文件生成注入用 `EXTRA_LDFLAGS` +- [x] `tools/ktalbum-tools/build.sh`:自动应用 `EXTRA_LDFLAGS`(一键构建时支持注入) +- [x] `tools/ktalbum-tools/private_keys.template.env`:可选的独立私钥模板(不提交 private_keys.env) ## 4. 构建入口与模板 @@ -32,6 +36,7 @@ ## 5. 文档与规格同步 - [x] `BUILD_COMPATIBILITY.md`:补充 Build-Time Injected Keys +- [x] `BUILD_COMPATIBILITY.zh-CN.md`:同步补充 Build-Time Injected Keys - [x] 新增能力规格:`openspec/specs/encrypted-outputs/spec.md` - [x] 变更增量规格:`openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md` diff --git a/openspec/specs/encrypted-outputs/spec.md b/openspec/specs/encrypted-outputs/spec.md index b3cb1005..3c2ab1d7 100644 --- a/openspec/specs/encrypted-outputs/spec.md +++ b/openspec/specs/encrypted-outputs/spec.md @@ -33,6 +33,8 @@ Normative: The system SHALL support overriding selected symmetric keys/secrets a Normative: The system SHALL encrypt the `.ktalbum` zip body using a versioned XOR key; it MUST store the key version in the file header and MUST select the decryption key by header version, with a v1 fallback on checksum mismatch. +Note: A local debug tool (ktalbum-tools) MAY additionally try both the injected key and the default open-source key for the same version to improve inspection compatibility. This does not change the main application's compatibility boundary. + #### Scenario: 导出写入 v2 - **GIVEN** 用户导出专辑 diff --git a/tools/ktalbum-tools/README.md b/tools/ktalbum-tools/README.md index fb17b57d..ec2733a5 100644 --- a/tools/ktalbum-tools/README.md +++ b/tools/ktalbum-tools/README.md @@ -37,9 +37,9 @@ ktalbum-tools-v0.1.0-windows-amd64.exe web ./ktalbum-tools-v0.1.0-darwin-arm64 web ``` -2. 打开浏览器访问 `http://localhost:8080` +1. 打开浏览器访问 `http://localhost:8080` -3. 通过界面拖放或选择 .ktalbum 文件进行操作 +1. 通过界面拖放或选择 .ktalbum 文件进行操作 ### 命令行 @@ -81,7 +81,7 @@ cd web/frontend npm install ``` -2. 构建: +1. 构建: ```bash # Linux/macOS @@ -92,9 +92,45 @@ chmod +x build.sh build.bat ``` -### 目录结构 +### (可选)私有密钥注入构建 + +ktalbum-tools 仅用于本地查看/调试 `.ktalbum` 文件内容。 + +它的密钥逻辑与项目主程序保持一致: + +- 支持 v1/v2 版本化 XOR 密钥 +- 支持通过 Go `-ldflags -X` 注入混淆密钥(与 SDK 授权流相同的 XOR+hex 格式) +- **为保证“同时兼容开源版本与私有密钥版本的产物”**:解密时会按顺序尝试“注入密钥 → 开源默认密钥”,并在需要时回退尝试 v1(与 SDK 的兼容策略一致) + +#### 使用方式(推荐复用 SDK 私钥文件) + +1. 在 SDK 目录准备私钥文件:复制 `sdk/private_keys.template.env` 为 `sdk/private_keys.env` 并填入 `KEY_ALBUM_EXPORT_V1`、`KEY_ALBUM_EXPORT_V2` + +1. 在 ktalbum-tools 目录加载注入参数: + +```bash +cd tools/ktalbum-tools +source ./setup_build_env.sh +``` + +1. 构建(推荐使用 build.sh,会自动应用 EXTRA_LDFLAGS): +```bash +chmod +x build.sh +./build.sh ``` + +如果你只想构建当前平台(不打包多平台 release),也可以: + +```bash +go build -ldflags "$EXTRA_LDFLAGS" ./... +``` + +如果你不想复用 SDK 私钥文件,可在 `tools/ktalbum-tools` 下创建 `private_keys.env`,脚本会自动回退读取。 + +### 目录结构 + +```text ktalbum-tools/ ├── commands/ # 命令实现 ├── utils/ # 工具函数 @@ -108,9 +144,9 @@ ktalbum-tools/ ## 注意事项 1. Web 服务默认只监听 localhost,仅供本地使用 -2. 确保有足够的磁盘空间用于临时文件 -3. 处理大文件时可能需要较长时间 -4. 在 macOS/Linux 上需要给可执行文件添加执行权限: +1. 确保有足够的磁盘空间用于临时文件 +1. 处理大文件时可能需要较长时间 +1. 在 macOS/Linux 上需要给可执行文件添加执行权限: ```bash chmod +x ktalbum-tools-* diff --git a/tools/ktalbum-tools/build.sh b/tools/ktalbum-tools/build.sh index 9d47a7fc..1e2a448e 100644 --- a/tools/ktalbum-tools/build.sh +++ b/tools/ktalbum-tools/build.sh @@ -3,6 +3,17 @@ # 版本号 VERSION="v0.1.0" +# 额外 ldflags(用于构建时密钥注入) +# - 若未设置 EXTRA_LDFLAGS,则保持开源默认密钥行为 +# - 若已通过 setup_build_env.sh 设置,则会自动合并到 -ldflags 中 +LDFLAGS="-s -w" +if [ -n "$EXTRA_LDFLAGS" ]; then + echo "检测到 EXTRA_LDFLAGS,将在构建时注入密钥..." + LDFLAGS="$LDFLAGS $EXTRA_LDFLAGS" +else + echo "未设置 EXTRA_LDFLAGS,将使用开源默认密钥构建。" +fi + # 构建前端 echo "构建前端..." cd web/frontend @@ -30,19 +41,19 @@ mkdir -p release echo "构建各平台版本..." # Windows (64-bit, x86_64) -GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o release/ktalbum-tools-${VERSION}-windows-amd64.exe +GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o release/ktalbum-tools-${VERSION}-windows-amd64.exe # Windows (64-bit, ARM64) -GOOS=windows GOARCH=arm64 go build -ldflags="-s -w" -o release/ktalbum-tools-${VERSION}-windows-arm64.exe +GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o release/ktalbum-tools-${VERSION}-windows-arm64.exe # macOS (64-bit, Intel) -GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o release/ktalbum-tools-${VERSION}-darwin-amd64 +GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o release/ktalbum-tools-${VERSION}-darwin-amd64 # macOS (64-bit, Apple Silicon) -GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o release/ktalbum-tools-${VERSION}-darwin-arm64 +GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o release/ktalbum-tools-${VERSION}-darwin-arm64 # Linux (64-bit) -GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o release/ktalbum-tools-${VERSION}-linux-amd64 +GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o release/ktalbum-tools-${VERSION}-linux-amd64 # 为 Unix-like 系统添加执行权限 chmod +x release/ktalbum-tools-${VERSION}-darwin-* diff --git a/tools/ktalbum-tools/commands/extract.go b/tools/ktalbum-tools/commands/extract.go index ea06fbc8..b5d13e2e 100644 --- a/tools/ktalbum-tools/commands/extract.go +++ b/tools/ktalbum-tools/commands/extract.go @@ -43,23 +43,37 @@ func Extract(inputFile, outputFile string, verbose bool) error { return fmt.Errorf("读取加密数据失败: %v", err) } - // 解密数据(按版本选择密钥) - decryptKey := utils.GetEncryptKeyByVersion(header.Version) - zipData := utils.XorCrypt(encryptedData, decryptKey) + // 解密数据(按版本选择候选密钥;私有密钥构建优先注入,回退默认,兼容开源产物) + var zipData []byte + decrypted := false - // 验证校验和 - checksum := sha256.Sum256(zipData) - if checksum != header.Checksum { - // 与 SDK 一致:若版本不是 v1,尝试使用 v1 密钥回退 - if header.Version != 1 { - zipData = utils.XorCrypt(encryptedData, utils.GetEncryptKeyByVersion(1)) - checksum = sha256.Sum256(zipData) + for _, decryptKey := range utils.GetDecryptKeyCandidatesByVersion(header.Version) { + candidate := utils.XorCrypt(encryptedData, decryptKey) + checksum := sha256.Sum256(candidate) + if checksum == header.Checksum { + zipData = candidate + decrypted = true + break } - if checksum != header.Checksum { - return fmt.Errorf("文件校验失败,文件可能已损坏或密钥不匹配") + } + + // 与 SDK 一致:若校验失败且版本不是 v1,尝试 v1 候选回退 + if !decrypted && header.Version != 1 { + for _, decryptKey := range utils.GetDecryptKeyCandidatesByVersion(1) { + candidate := utils.XorCrypt(encryptedData, decryptKey) + checksum := sha256.Sum256(candidate) + if checksum == header.Checksum { + zipData = candidate + decrypted = true + break + } } } + if !decrypted { + return fmt.Errorf("文件校验失败,文件可能已损坏或密钥不匹配") + } + // 写入解密后的zip数据 if err := os.WriteFile(outputFile, zipData, 0644); err != nil { return fmt.Errorf("写入输出文件失败: %v", err) diff --git a/tools/ktalbum-tools/commands/info.go b/tools/ktalbum-tools/commands/info.go index 6aaa78a4..30718d70 100644 --- a/tools/ktalbum-tools/commands/info.go +++ b/tools/ktalbum-tools/commands/info.go @@ -43,23 +43,37 @@ func GetFileInfo(filePath string) (*FileInfo, error) { return nil, fmt.Errorf("读取加密数据失败: %v", err) } - // 解密数据(按版本选择密钥) - decryptKey := utils.GetEncryptKeyByVersion(header.Version) - zipData := utils.XorCrypt(encryptedData, decryptKey) - - // 验证校验和 - checksum := utils.CalculateChecksum(zipData) - if !bytes.Equal(checksum[:], header.Checksum[:]) { - // 与 SDK 一致:若版本不是 v1,尝试使用 v1 密钥回退 - if header.Version != 1 { - zipData = utils.XorCrypt(encryptedData, utils.GetEncryptKeyByVersion(1)) - checksum = utils.CalculateChecksum(zipData) + // 解密数据(按版本选择候选密钥;私有密钥构建优先注入,回退默认,兼容开源产物) + var zipData []byte + decrypted := false + + for _, decryptKey := range utils.GetDecryptKeyCandidatesByVersion(header.Version) { + candidate := utils.XorCrypt(encryptedData, decryptKey) + checksum := utils.CalculateChecksum(candidate) + if bytes.Equal(checksum[:], header.Checksum[:]) { + zipData = candidate + decrypted = true + break } - if !bytes.Equal(checksum[:], header.Checksum[:]) { - return nil, fmt.Errorf("文件校验失败,文件可能已损坏或密钥不匹配") + } + + // 与 SDK 一致:若校验失败且版本不是 v1,尝试 v1 候选回退 + if !decrypted && header.Version != 1 { + for _, decryptKey := range utils.GetDecryptKeyCandidatesByVersion(1) { + candidate := utils.XorCrypt(encryptedData, decryptKey) + checksum := utils.CalculateChecksum(candidate) + if bytes.Equal(checksum[:], header.Checksum[:]) { + zipData = candidate + decrypted = true + break + } } } + if !decrypted { + return nil, fmt.Errorf("文件校验失败,文件可能已损坏或密钥不匹配") + } + // 从 zip 数据中读取 .keytone-album 文件 zipReader, err := utils.ReadZipData(zipData) if err != nil { diff --git a/tools/ktalbum-tools/private_keys.template.env b/tools/ktalbum-tools/private_keys.template.env new file mode 100644 index 00000000..8b5e8e34 --- /dev/null +++ b/tools/ktalbum-tools/private_keys.template.env @@ -0,0 +1,11 @@ +# ktalbum-tools 私钥配置模板(可选) +# +# 通常建议直接复用 SDK 的私钥文件:sdk/private_keys.env +# 如果你希望 ktalbum-tools 使用独立文件,也可以在 tools/ktalbum-tools 下创建 private_keys.env +# 并填写下列字段(不要提交到 git)。 + +# 专辑导出文件 XOR 密钥 v1(旧版本,用于向后兼容) +KEY_ALBUM_EXPORT_V1="PLACEHOLDER_ALBUM_EXPORT_KEY_V1" + +# 专辑导出文件 XOR 密钥 v2(当前版本) +KEY_ALBUM_EXPORT_V2="PLACEHOLDER_ALBUM_EXPORT_KEY_V2" diff --git a/tools/ktalbum-tools/setup_build_env.sh b/tools/ktalbum-tools/setup_build_env.sh new file mode 100644 index 00000000..a297cca2 --- /dev/null +++ b/tools/ktalbum-tools/setup_build_env.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# tools/ktalbum-tools/setup_build_env.sh +# ====================================================================================== +# 脚本功能说明: +# 该脚本用于 ktalbum-tools 的本地构建(个人调试/查看用),支持与 SDK 相同的“构建时密钥注入”机制。 +# +# 设计目标: +# - 开源构建:不需要任何私钥文件即可工作(使用默认开源密钥) +# - 私有构建:可读取 sdk/private_keys.env(或本地 private_keys.env),生成混淆后的 Hex,并注入到 ktalbum-tools +# - 工具兼容性:ktalbum-tools 运行时会同时尝试“注入密钥”与“开源默认密钥”进行解密,以兼容两类产物 +# ====================================================================================== + +# 使用方法: +# 方式一(推荐):在当前终端加载环境变量 +# source ./setup_build_env.sh +# +# 方式二:仅获取 export 命令(可配合 eval) +# ./setup_build_env.sh +# eval $(./setup_build_env.sh) + +# =================配置区域================= + +# 1. 私钥文件路径 +# 优先复用 SDK 的私钥文件(不提交到 git) +# 可通过环境变量 KEYS_FILE 覆盖 +DEFAULT_KEYS_FILE="../../sdk/private_keys.env" +KEYS_FILE="${KEYS_FILE:-$DEFAULT_KEYS_FILE}" + +# 如果默认路径不存在,尝试使用当前目录的 private_keys.env +if [ ! -f "$KEYS_FILE" ]; then + if [ -f "private_keys.env" ]; then + KEYS_FILE="private_keys.env" + fi +fi + +# 2. 混淆工具源码路径(与 SDK 共用同一份) +OBFUSCATOR_TOOL="../key-obfuscator/main.go" + +# 3. 定义需要处理的密钥列表 +# ktalbum-tools 只需要专辑导出文件 XOR key(v1/v2)即可解密 .ktalbum +# 格式: "环境变量中的键名:Go代码中的变量全路径" +KEYS_TO_PROCESS=( + "KEY_ALBUM_EXPORT_V1:ktalbum-tools/utils.KeytoneEncryptKeyV1" + "KEY_ALBUM_EXPORT_V2:ktalbum-tools/utils.KeytoneEncryptKeyV2" +) + +# =================逻辑区域================= + +# 检查混淆工具是否存在 +if [ ! -f "$OBFUSCATOR_TOOL" ]; then + echo "错误: 未找到混淆工具源码 $OBFUSCATOR_TOOL" >&2 + return 1 2>/dev/null || exit 1 +fi + +# 如果没有私钥文件,允许继续(开源构建不需要 EXTRA_LDFLAGS) +if [ ! -f "$KEYS_FILE" ]; then + echo "提示: 未找到私钥文件 $KEYS_FILE,将不设置 EXTRA_LDFLAGS(开源默认密钥模式)。" >&2 + export EXTRA_LDFLAGS="" + if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "export EXTRA_LDFLAGS=\"\"" + fi + exit 0 +fi + +# 初始化 LDFLAGS 字符串 +LDFLAGS="" + +echo "正在处理 ktalbum-tools 密钥混淆..." >&2 + +for entry in "${KEYS_TO_PROCESS[@]}"; do + KEY_NAME=$(echo "$entry" | cut -d':' -f1) + GO_VAR=$(echo "$entry" | cut -d':' -f2) + + PLAINTEXT_KEY=$(grep "^$KEY_NAME=" "$KEYS_FILE" | cut -d'"' -f2) + if [ -z "$PLAINTEXT_KEY" ]; then + echo "错误: 在 $KEYS_FILE 中未找到 $KEY_NAME" >&2 + return 1 2>/dev/null || exit 1 + fi + + OBFUSCATED_VAL=$(go run "$OBFUSCATOR_TOOL" -key "$PLAINTEXT_KEY") + if [ $? -ne 0 ]; then + echo "错误: 密钥 $KEY_NAME 混淆失败" >&2 + return 1 2>/dev/null || exit 1 + fi + + LDFLAGS="$LDFLAGS -X '$GO_VAR=$OBFUSCATED_VAL'" +done + +export EXTRA_LDFLAGS="$LDFLAGS" + +echo "成功!已设置 EXTRA_LDFLAGS(ktalbum-tools)。" >&2 + +echo "示例构建:" >&2 + +echo " go build -ldflags \"$EXTRA_LDFLAGS\" ./..." >&2 + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "export EXTRA_LDFLAGS=\"$LDFLAGS\"" +fi diff --git a/tools/ktalbum-tools/utils/header.go b/tools/ktalbum-tools/utils/header.go index 919cc772..c65fe414 100644 --- a/tools/ktalbum-tools/utils/header.go +++ b/tools/ktalbum-tools/utils/header.go @@ -51,6 +51,20 @@ func getPlainEncryptKey(value string, defaultValue string) string { return deobfuscateString(value) } +func getDecryptKeyCandidates(value string, defaultValue string) []string { + // 未注入:仅返回默认值 + if value == defaultValue { + return []string{defaultValue} + } + + // 已注入:优先返回注入后的明文密钥,并追加默认值作为“兼容开源产物”的回退 + primary := getPlainEncryptKey(value, defaultValue) + if primary == defaultValue { + return []string{defaultValue} + } + return []string{primary, defaultValue} +} + // GetEncryptKeyByVersion 根据文件头版本号返回对应的明文密钥。 func GetEncryptKeyByVersion(version uint8) string { switch version { @@ -64,6 +78,21 @@ func GetEncryptKeyByVersion(version uint8) string { } } +// GetDecryptKeyCandidatesByVersion 返回解密候选密钥列表。 +// 设计目标: +// - 开源构建:仅使用默认密钥 +// - 私有密钥构建(注入后):优先使用注入密钥,同时回退尝试默认密钥,保证工具可同时解密两类产物 +func GetDecryptKeyCandidatesByVersion(version uint8) []string { + switch version { + case 1: + return getDecryptKeyCandidates(KeytoneEncryptKeyV1, DefaultKeytoneEncryptKeyV1) + case 2: + return getDecryptKeyCandidates(KeytoneEncryptKeyV2, DefaultKeytoneEncryptKeyV2) + default: + return getDecryptKeyCandidates(KeytoneEncryptKeyV2, DefaultKeytoneEncryptKeyV2) + } +} + type KeytoneFileHeader struct { Signature [7]byte Version uint8 From dc0718c274f463bfd9588855323c251b5e908a6e Mon Sep 17 00:00:00 2001 From: LuSrackhall <3647637206@qq.com> Date: Wed, 31 Dec 2025 22:09:38 +0800 Subject: [PATCH 3/7] =?UTF-8?q?chore:=20=E5=86=8D=E6=AC=A1=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E6=89=80=E6=9C=89=E5=AF=86=E9=92=A5=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E5=B7=B2=E8=A2=AB=E6=9E=84=E5=BB=BA=E8=84=9A=E6=9C=AC=E8=A6=86?= =?UTF-8?q?=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../update-build-injected-symmetric-keys/proposal.md | 8 +++++++- .../changes/update-build-injected-symmetric-keys/tasks.md | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openspec/changes/update-build-injected-symmetric-keys/proposal.md b/openspec/changes/update-build-injected-symmetric-keys/proposal.md index 8ff05741..206cc5f0 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/proposal.md +++ b/openspec/changes/update-build-injected-symmetric-keys/proposal.md @@ -57,4 +57,10 @@ 注:工具链 `tools/ktalbum-tools` 也提供同等注入点(模块路径不同)。 -另外:ktalbum-tools 为本地查看/调试用途,解密 `.ktalbum` 时会按顺序尝试“注入密钥 → 开源默认密钥”,并在校验失败时回退尝试 v1,以便同一构建可同时兼容开源与私有两类产物。 +另外:ktalbum-tools 为本地查看/调试用途,解密 `.ktalbum` 时会按顺序尝试“注入密钥 → 开源默认密钥”,并在校验失败时回退尝试 v1,以便同一构建可同时兼容开源与私有两类产物。## 扫描验证结论 + +2025-12-31 再次全量扫描主项目(sdk + frontend): + +- SDK 共发现 **10 个**对称加密密钥/secret,全部已被 `setup_build_env.sh` 覆盖 +- 前端无任何硬编码对称密钥参与加密操作 +- **结论:无遗漏** \ No newline at end of file diff --git a/openspec/changes/update-build-injected-symmetric-keys/tasks.md b/openspec/changes/update-build-injected-symmetric-keys/tasks.md index a90f7f08..aa34fd18 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/tasks.md +++ b/openspec/changes/update-build-injected-symmetric-keys/tasks.md @@ -43,5 +43,6 @@ ## 6. 验证 - [x] `go build ./...`(sdk) +- [x] 再次全量扫描主项目(sdk + frontend),确认所有对称密钥均已被构建脚本覆盖(共 10 个,无遗漏) - [ ] 可选:在本地提供 `sdk/private_keys.env` 并运行 `source sdk/setup_build_env.sh` 后构建,验证注入 keys 生效 - [ ] 可选:为 `tools/ktalbum-tools` 构建时传入 `-ldflags -X`,验证能解密对应构建身份产物 From b14dc39fea5d0df55298d01fc8be9da3dc0decb9 Mon Sep 17 00:00:00 2001 From: LuSrackhall <3647637206@qq.com> Date: Thu, 1 Jan 2026 02:18:12 +0800 Subject: [PATCH 4/7] =?UTF-8?q?chore:=20=E6=98=8E=E7=A1=AE=20printconfig?= =?UTF-8?q?=20=E5=8F=AF=E4=BB=A5=E8=A2=AB=E7=A7=81=E9=92=A5=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E5=88=B0,=20=E5=B9=B6=E5=AE=8C=E5=96=84=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=B3=A8=E9=87=8A=E3=80=81=E4=BD=BF=E7=94=A8=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E5=8F=8A=E5=85=BC=E5=AE=B9=E6=80=A7=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../design.md | 14 ++++ .../proposal.md | 6 +- .../specs/encrypted-outputs/spec.md | 2 + .../tasks.md | 3 + openspec/specs/encrypted-outputs/spec.md | 2 + sdk/audioPackage/cmd/printconfig/main.go | 72 +++++++++++++++++++ 7 files changed, 99 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7af96f4b..a3138f71 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /sdk/KeyToneSetting.json /sdk/log.jsonl /sdk/temporaryDebug +/sdk/printconfig # Private Keys private_keys.env diff --git a/openspec/changes/update-build-injected-symmetric-keys/design.md b/openspec/changes/update-build-injected-symmetric-keys/design.md index 9fd08730..721d78aa 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/design.md +++ b/openspec/changes/update-build-injected-symmetric-keys/design.md @@ -73,6 +73,20 @@ ktalbum-tools 的目标是便于本地查看/调试 `.ktalbum` 文件,因此 - 一键构建:`tools/ktalbum-tools/build.sh` 会自动合并并应用 `EXTRA_LDFLAGS` - 运行时解密:按顺序尝试“注入密钥 → 默认密钥”,并在校验失败时按 SDK 策略回退尝试 v1 +### printconfig(SDK 内部调试工具) + +`sdk/audioPackage/cmd/printconfig` 是用于解密查看键音专辑配置的内部工具。 + +- 位于 SDK 模块内,构建时自动继承 `EXTRA_LDFLAGS` 注入的 `FixedSecret` +- 无需单独配置注入脚本 +- 私有构建后可解密私有产物的加密配置 + +使用方式补充: + +- 可直接 `go run ./audioPackage/cmd/printconfig --path ...`(开源默认密钥) +- 若要解密私有构建产物,需要 `go run -ldflags "$EXTRA_LDFLAGS" ./audioPackage/cmd/printconfig --path ...` +- 注入发生在编译/链接阶段,因此同一次运行无法同时兼容两套密钥(需分别运行) + ## Compatibility Notes - 未注入:行为与当前开源版本完全一致 diff --git a/openspec/changes/update-build-injected-symmetric-keys/proposal.md b/openspec/changes/update-build-injected-symmetric-keys/proposal.md index 206cc5f0..2ee38ce0 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/proposal.md +++ b/openspec/changes/update-build-injected-symmetric-keys/proposal.md @@ -57,10 +57,14 @@ 注:工具链 `tools/ktalbum-tools` 也提供同等注入点(模块路径不同)。 -另外:ktalbum-tools 为本地查看/调试用途,解密 `.ktalbum` 时会按顺序尝试“注入密钥 → 开源默认密钥”,并在校验失败时回退尝试 v1,以便同一构建可同时兼容开源与私有两类产物。## 扫描验证结论 +另外:ktalbum-tools 为本地查看/调试用途,解密 `.ktalbum` 时会按顺序尝试“注入密钥 → 开源默认密钥”,并在校验失败时回退尝试 v1,以便同一构建可同时兼容开源与私有两类产物。 + +## 扫描验证结论 2025-12-31 再次全量扫描主项目(sdk + frontend): - SDK 共发现 **10 个**对称加密密钥/secret,全部已被 `setup_build_env.sh` 覆盖 - 前端无任何硬编码对称密钥参与加密操作 +- SDK 内部调试工具 `sdk/audioPackage/cmd/printconfig` 依赖 `FixedSecret`,因在 SDK 模块内,构建时会自动继承注入 +- `printconfig` 可通过 `go run` 直接使用;私有密钥需要 `go run -ldflags "$EXTRA_LDFLAGS" ...`,且同一次运行无法同时兼容两套密钥(需分别运行) - **结论:无遗漏** \ No newline at end of file diff --git a/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md index 7a7b7f7a..a721b2f8 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md +++ b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md @@ -6,6 +6,8 @@ Normative: The system SHALL keep default symmetric keys/secrets hardcoded in source for open-source builds, and SHALL allow overriding them at build-time via Go `-ldflags -X` using XOR-obfuscated hex values; at runtime it MUST deobfuscate injected values before use. +Note: The internal debug utility `sdk/audioPackage/cmd/printconfig` depends on `FixedSecret` and MAY be executed via `go run`; private builds must use the same build-time injection (e.g. `go run -ldflags "$EXTRA_LDFLAGS" ...`). + #### Scenario: 未注入私钥时保持原行为 - **GIVEN** 构建过程中未提供任何 `-ldflags -X` 覆盖值 diff --git a/openspec/changes/update-build-injected-symmetric-keys/tasks.md b/openspec/changes/update-build-injected-symmetric-keys/tasks.md index aa34fd18..180c31d0 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/tasks.md +++ b/openspec/changes/update-build-injected-symmetric-keys/tasks.md @@ -44,5 +44,8 @@ - [x] `go build ./...`(sdk) - [x] 再次全量扫描主项目(sdk + frontend),确认所有对称密钥均已被构建脚本覆盖(共 10 个,无遗漏) +- [x] 确认 SDK 内部工具 `sdk/audioPackage/cmd/printconfig` 依赖 `FixedSecret`,因在 SDK 模块内自动继承注入 +- [x] 为 `printconfig` 添加文件头部使用说明(含密钥/构建说明) +- [x] 为 `printconfig` 补充 `go run` 使用说明(含开源/私有注入与兼容性边界) - [ ] 可选:在本地提供 `sdk/private_keys.env` 并运行 `source sdk/setup_build_env.sh` 后构建,验证注入 keys 生效 - [ ] 可选:为 `tools/ktalbum-tools` 构建时传入 `-ldflags -X`,验证能解密对应构建身份产物 diff --git a/openspec/specs/encrypted-outputs/spec.md b/openspec/specs/encrypted-outputs/spec.md index 3c2ab1d7..db19eec9 100644 --- a/openspec/specs/encrypted-outputs/spec.md +++ b/openspec/specs/encrypted-outputs/spec.md @@ -51,6 +51,8 @@ Note: A local debug tool (ktalbum-tools) MAY additionally try both the injected Normative: The system SHALL derive a 32-byte AES key using `SHA256(secret + last6(sha1(albumUUID)))`, where `secret` is configurable via build-time injection; the encrypted config bytes MUST be stored as `nonce + ciphertext`. +Note: The internal debug utility `sdk/audioPackage/cmd/printconfig` can be used to inspect decrypted config; for private builds it must be run/built with the same `-ldflags -X` injection (e.g. via `EXTRA_LDFLAGS`). + #### Scenario: 使用默认 secret 派生 key - **GIVEN** 未注入自定义 secret diff --git a/sdk/audioPackage/cmd/printconfig/main.go b/sdk/audioPackage/cmd/printconfig/main.go index ba257d74..28e6a3f2 100644 --- a/sdk/audioPackage/cmd/printconfig/main.go +++ b/sdk/audioPackage/cmd/printconfig/main.go @@ -1,3 +1,75 @@ +/** + * This file is part of the KeyTone project. + * + * Copyright (C) 2024 LuSrackhall + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/* +printconfig - KeyTone 专辑配置解密查看工具 + + +密钥说明: + 解密使用 FixedSecret 派生 AES key(SHA256(secret + last6(sha1(albumUUID))))。 + FixedSecret 支持构建时注入(-ldflags -X KeyTone/audioPackage/enc.FixedSecret=...)。 + - 开源构建:使用默认 FixedSecret,只能解密开源构建产物 + - 私有构建:注入私有 FixedSecret 后,可解密对应私有构建产物 + +构建方式: + 假设在 sdk/ 目录下, 执行该命令, 会在 sdk/ 目录下生成可执行文件 printconfig, 此cli工具可用于解密开源构建产物, 方便开发调试。 + > 目前已将默认产物添加到.gitignore中避免误提交。 + + # 开源构建(使用默认密钥) + go build ./audioPackage/cmd/printconfig + + # 私有构建(需先加载注入参数) + source ./setup_build_env.sh + go build -ldflags "$EXTRA_LDFLAGS" ./audioPackage/cmd/printconfig + +直接运行(不落地构建产物): + + # 开源默认密钥(仅能解密开源构建产物) + go run ./audioPackage/cmd/printconfig --path /path/to/album/uuid + + # 私有注入密钥(可解密对应私有构建产物) + source ./setup_build_env.sh + go run -ldflags "$EXTRA_LDFLAGS" ./audioPackage/cmd/printconfig --path /path/to/album/uuid + +兼容性说明: + - FixedSecret 的覆盖发生在编译/链接阶段(-ldflags -X),不是运行时参数。 + - 因此:同一次运行(同一个 go run/go build 产物)无法同时兼容两套密钥。 + 若要分别查看开源/私有产物,请使用不同的 ldflags 分别运行/构建。 + +用途: + 解密并打印键音专辑的 package.json 配置文件内容。 + 当用户选择"需要签名"导出专辑时,配置文件会被 AES-GCM 加密, + 此工具用于在终端中查看加密后的真实配置内容,便于调试。 + +使用方法: + printconfig --path [--raw] + +参数: + --path 专辑目录路径(包含 package.json 或 stub + core 文件) + --raw 输出原始密文 hex(不解密) + +示例: + # 解密并查看配置 + printconfig --path /path/to/album/uuid + + # 仅输出密文 hex(不解密) + printconfig --path /path/to/album/uuid --raw +*/ package main import ( From 78126a8c64ef08c312092b29542dae8274a126c7 Mon Sep 17 00:00:00 2001 From: LuSrackhall <3647637206@qq.com> Date: Sun, 4 Jan 2026 02:21:32 +0800 Subject: [PATCH 5/7] =?UTF-8?q?chore:=20=E5=A2=9E=E5=BC=BA=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E6=B3=A8=E5=85=A5=E5=85=BC=E5=AE=B9=E6=80=A7=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=AF=86=E9=92=A5=E6=96=87=E4=BB=B6=E5=86=85?= =?UTF-8?q?=E7=BC=BA=E5=A4=B1=E5=AF=86=E9=92=A5=E6=97=B6,=20=E6=89=80?= =?UTF-8?q?=E7=BC=BA=E5=A4=B1=E5=AF=86=E9=92=A5=E5=8F=AF=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E5=B9=B6=E4=BD=BF=E7=94=A8=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E4=B8=AD=E5=86=99=E6=AD=BB=E7=9A=84=E9=BB=98=E8=AE=A4=E5=AF=86?= =?UTF-8?q?=E9=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design.md | 10 +++ .../proposal.md | 9 +++ .../specs/encrypted-outputs/spec.md | 2 + .../tasks.md | 8 ++ openspec/specs/encrypted-outputs/spec.md | 2 + sdk/setup_build_env.sh | 75 +++++++++++++++---- tools/ktalbum-tools/setup_build_env.sh | 50 +++++++++++-- 7 files changed, 136 insertions(+), 20 deletions(-) diff --git a/openspec/changes/update-build-injected-symmetric-keys/design.md b/openspec/changes/update-build-injected-symmetric-keys/design.md index 721d78aa..b03777d3 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/design.md +++ b/openspec/changes/update-build-injected-symmetric-keys/design.md @@ -60,6 +60,16 @@ - `sdk/private_keys.template.env` 增加对应 KEY_* 项 - `sdk/setup_build_env.sh` 增加 `KEYS_TO_PROCESS` 映射,使本地私有构建可以自动生成 `EXTRA_LDFLAGS` +### Compatibility Behavior (重要) + +为保证历史 `private_keys.env`(未包含新增 KEY_*)以及“仅开源构建”场景可正常运行: + +- `sdk/setup_build_env.sh` 采用 best-effort 策略: + - 若未找到 `private_keys.env`,脚本不会报错退出,而是设置 `EXTRA_LDFLAGS=""` 并继续(等价于不注入)。 + - 若某个 `KEY_*` 缺失,或值仍为模板占位符(如 `PLACEHOLDER_*` / `REPLACE_ME`),脚本会跳过该 key 的注入,让运行时回退到源码默认值。 + +该行为的目标是:在不要求用户立刻补齐新增环境变量的前提下,依旧保持与适配前版本的兼容性。 + ### ktalbum-tools(本地调试工具) ktalbum-tools 的目标是便于本地查看/调试 `.ktalbum` 文件,因此它需要在“一个二进制”中尽量兼容两类产物: diff --git a/openspec/changes/update-build-injected-symmetric-keys/proposal.md b/openspec/changes/update-build-injected-symmetric-keys/proposal.md index 2ee38ce0..6304841f 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/proposal.md +++ b/openspec/changes/update-build-injected-symmetric-keys/proposal.md @@ -46,6 +46,15 @@ ## Review Notes / Audit Trail +## 2026-01-04 Compatibility Fixes (Audit) + +为满足“未使用新增 KEY_* 变量也能运行/构建成功,并与适配前兼容”的预期,补充以下实现约束: + +- `sdk/setup_build_env.sh` 必须在 `set -euo pipefail` 环境下稳定运行(不允许因 `grep` / pipeline 失败而静默退出)。 +- 未提供 `sdk/private_keys.env` 时,脚本不得失败,必须设置 `EXTRA_LDFLAGS=""`(等价于不注入,回退源码默认值)。 +- `private_keys.env` 存在但缺少新增条目时(历史文件),脚本不得失败;缺失项应跳过注入。 +- 若 `private_keys.env` 中仍是模板占位符(如 `PLACEHOLDER_*` / `REPLACE_ME`),必须视为“未配置”并跳过注入,避免误覆盖开源默认密钥导致兼容性破坏。 + | Key/Secret | Default(源码) | 注入变量(Go -ldflags -X) | 用途摘要 | | ----------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- | | 签名 KeyA | `KeyTone2024Signature_KeyA_SecureEncryptionKeyForIDEncryption` | `KeyTone/signature.KeyToneSignatureEncryptionKeyA` | 加密签名ID、派生动态密钥(PBKDF2) | diff --git a/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md index a721b2f8..f31509a9 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md +++ b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md @@ -14,6 +14,8 @@ Note: The internal debug utility `sdk/audioPackage/cmd/printconfig` depends on ` - **WHEN** 系统进行签名管理加解密、专辑导出/导入、专辑配置加解密 - **THEN** 系统使用源码默认密钥/secret,行为与当前开源版本一致 +Note: For local development, the helper script `sdk/setup_build_env.sh` SHOULD treat missing `private_keys.env` or missing/placeholder `KEY_*` entries as “not injected” (skip `-ldflags -X` for that key) to preserve backward compatibility. + #### Scenario: 注入密钥后自动解混淆 - **GIVEN** 构建过程中通过 `-ldflags -X` 注入了 XOR 混淆后的 hex diff --git a/openspec/changes/update-build-injected-symmetric-keys/tasks.md b/openspec/changes/update-build-injected-symmetric-keys/tasks.md index 180c31d0..991ac745 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/tasks.md +++ b/openspec/changes/update-build-injected-symmetric-keys/tasks.md @@ -49,3 +49,11 @@ - [x] 为 `printconfig` 补充 `go run` 使用说明(含开源/私有注入与兼容性边界) - [ ] 可选:在本地提供 `sdk/private_keys.env` 并运行 `source sdk/setup_build_env.sh` 后构建,验证注入 keys 生效 - [ ] 可选:为 `tools/ktalbum-tools` 构建时传入 `-ldflags -X`,验证能解密对应构建身份产物 + +## 7. 兼容性修复(2026-01-04) + +- [x] `sdk/setup_build_env.sh`:在 `set -euo pipefail` 下不因 grep/pipeline 失败而静默退出 +- [x] `sdk/setup_build_env.sh`:缺失 `private_keys.env` 时不失败,设置 `EXTRA_LDFLAGS=""` 并回退默认行为 +- [x] `sdk/setup_build_env.sh`:缺失新增 `KEY_*` 时跳过注入(兼容历史 private_keys.env) +- [x] `sdk/setup_build_env.sh`:模板占位符(`PLACEHOLDER_*`/`REPLACE_ME`)视为未配置并跳过,避免误覆盖默认密钥 +- [x] `tools/ktalbum-tools/setup_build_env.sh`:同等容错(缺失/占位符跳过) diff --git a/openspec/specs/encrypted-outputs/spec.md b/openspec/specs/encrypted-outputs/spec.md index db19eec9..78b049ee 100644 --- a/openspec/specs/encrypted-outputs/spec.md +++ b/openspec/specs/encrypted-outputs/spec.md @@ -17,6 +17,8 @@ Normative: The system SHALL support overriding selected symmetric keys/secrets at build-time via Go `-ldflags -X` using XOR-obfuscated hex values; when not injected, it MUST fall back to the default hardcoded values so open-source builds remain functional. +Note: The recommended helper script `sdk/setup_build_env.sh` is intentionally best-effort for compatibility. If `sdk/private_keys.env` is missing, or if individual `KEY_*` entries are missing / still template placeholders, the script SHOULD skip those injections and leave `EXTRA_LDFLAGS` empty or partial, so builds remain compatible with the default open-source behavior. + #### Scenario: 开源构建未注入 - **GIVEN** 用户从公开源码直接构建且未提供私钥文件/注入参数 diff --git a/sdk/setup_build_env.sh b/sdk/setup_build_env.sh index ed3bdd71..4bab7620 100755 --- a/sdk/setup_build_env.sh +++ b/sdk/setup_build_env.sh @@ -63,14 +63,59 @@ KEYS_TO_PROCESS=( # =================逻辑区域================= +should_exit_or_return() { + local code="$1" + return "$code" 2>/dev/null || exit "$code" +} + +is_placeholder_value() { + local v="$1" + [[ -z "$v" ]] && return 0 + [[ "$v" == PLACEHOLDER_* ]] && return 0 + [[ "$v" == *REPLACE_ME* ]] && return 0 + return 1 +} + +read_env_value_from_file() { + local key_name="$1" + local file_path="$2" + + # 注意:dev.sh 使用了 `set -euo pipefail`,因此这里必须避免 grep/pipeline 失败导致脚本静默退出。 + local line + line=$(grep -m 1 "^${key_name}=" "$file_path" 2>/dev/null || true) + if [ -z "$line" ]; then + echo "" + return 0 + fi + + local value + value="${line#*=}" + # 去掉可能的 CRLF + value="${value%$'\r'}" + + # 支持 KEY="value" / KEY='value' / KEY=value 三种写法 + if [[ "$value" == '"'*'"' ]]; then + value="${value#\"}" + value="${value%\"}" + elif [[ "$value" == "'"*"'" ]]; then + value="${value#\'}" + value="${value%\'}" + fi + + echo "$value" +} + # 检查私钥文件是否存在 +# 兼容性策略: +# - 未提供私钥文件时,开源构建依然应当可运行(使用源码默认密钥);因此这里不再视为错误。 +# - 若仅需要部分注入(例如只注入授权流密钥),也允许其它 key 缺失并自动跳过。 if [ ! -f "$KEYS_FILE" ]; then - # >&2 表示将输出重定向到标准错误(stderr),避免干扰正常的标准输出(stdout) - echo "错误: 未找到私钥文件 $KEYS_FILE" >&2 - echo "请复制 private_keys.template.env 为 $KEYS_FILE 并填入您的密钥。" >&2 - # return 1 用于在 source 执行时退出脚本但不退出终端 - # exit 1 用于在直接执行时退出脚本 - return 1 2>/dev/null || exit 1 + echo "提示: 未找到私钥文件 $KEYS_FILE,将不设置 EXTRA_LDFLAGS(使用源码默认密钥/secret)。" >&2 + export EXTRA_LDFLAGS="" + if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "export EXTRA_LDFLAGS=\"\"" + fi + should_exit_or_return 0 fi # 检查混淆工具是否存在 @@ -92,15 +137,15 @@ for entry in "${KEYS_TO_PROCESS[@]}"; do KEY_NAME=$(echo "$entry" | cut -d':' -f1) GO_VAR=$(echo "$entry" | cut -d':' -f2) - # 从文件中读取明文密钥 - # grep "^$KEY_NAME=" : 查找以 KEY_NAME= 开头的行 - # cut -d'"' -f2 : 以双引号为分隔符,提取中间的内容(即密钥值) - PLAINTEXT_KEY=$(grep "^$KEY_NAME=" "$KEYS_FILE" | cut -d'"' -f2) + # 从文件中读取明文密钥(兼容 set -euo pipefail) + PLAINTEXT_KEY=$(read_env_value_from_file "$KEY_NAME" "$KEYS_FILE") - # 检查是否成功读取到密钥 - if [ -z "$PLAINTEXT_KEY" ]; then - echo "错误: 在 $KEYS_FILE 中未找到 $KEY_NAME" >&2 - return 1 2>/dev/null || exit 1 + # 兼容性: + # - 若缺失该 KEY_*,则跳过注入,让 Go 侧回退到源码默认值(保持与旧版/开源版兼容) + # - 若仍是模板占位符,则视为“未配置”,同样跳过注入 + if is_placeholder_value "$PLAINTEXT_KEY"; then + echo "提示: 跳过 ${KEY_NAME}(未配置或仍为模板占位符),将使用源码默认密钥/secret。" >&2 + continue fi # 调用 Go 工具生成混淆后的 Hex 字符串 @@ -113,7 +158,7 @@ for entry in "${KEYS_TO_PROCESS[@]}"; do # $? 获取上一个命令的退出状态码,0 表示成功 if [ $? -ne 0 ]; then echo "错误: 密钥 $KEY_NAME 混淆失败" >&2 - return 1 2>/dev/null || exit 1 + should_exit_or_return 1 fi # 拼接到 LDFLAGS 字符串中 diff --git a/tools/ktalbum-tools/setup_build_env.sh b/tools/ktalbum-tools/setup_build_env.sh index a297cca2..7254d9ee 100644 --- a/tools/ktalbum-tools/setup_build_env.sh +++ b/tools/ktalbum-tools/setup_build_env.sh @@ -47,6 +47,41 @@ KEYS_TO_PROCESS=( # =================逻辑区域================= +should_exit_or_return() { + local code="$1" + return "$code" 2>/dev/null || exit "$code" +} + +is_placeholder_value() { + local v="$1" + [[ -z "$v" ]] && return 0 + [[ "$v" == PLACEHOLDER_* ]] && return 0 + [[ "$v" == *REPLACE_ME* ]] && return 0 + return 1 +} + +read_env_value_from_file() { + local key_name="$1" + local file_path="$2" + local line + line=$(grep -m 1 "^${key_name}=" "$file_path" 2>/dev/null || true) + if [ -z "$line" ]; then + echo "" + return 0 + fi + local value + value="${line#*=}" + value="${value%$'\r'}" + if [[ "$value" == '"'*'"' ]]; then + value="${value#\"}" + value="${value%\"}" + elif [[ "$value" == "'"*"'" ]]; then + value="${value#\'}" + value="${value%\'}" + fi + echo "$value" +} + # 检查混淆工具是否存在 if [ ! -f "$OBFUSCATOR_TOOL" ]; then echo "错误: 未找到混淆工具源码 $OBFUSCATOR_TOOL" >&2 @@ -72,16 +107,16 @@ for entry in "${KEYS_TO_PROCESS[@]}"; do KEY_NAME=$(echo "$entry" | cut -d':' -f1) GO_VAR=$(echo "$entry" | cut -d':' -f2) - PLAINTEXT_KEY=$(grep "^$KEY_NAME=" "$KEYS_FILE" | cut -d'"' -f2) - if [ -z "$PLAINTEXT_KEY" ]; then - echo "错误: 在 $KEYS_FILE 中未找到 $KEY_NAME" >&2 - return 1 2>/dev/null || exit 1 + PLAINTEXT_KEY=$(read_env_value_from_file "$KEY_NAME" "$KEYS_FILE") + if is_placeholder_value "$PLAINTEXT_KEY"; then + echo "提示: 跳过 ${KEY_NAME}(未配置或仍为模板占位符),将不设置 EXTRA_LDFLAGS(开源默认密钥模式)。" >&2 + continue fi OBFUSCATED_VAL=$(go run "$OBFUSCATOR_TOOL" -key "$PLAINTEXT_KEY") if [ $? -ne 0 ]; then echo "错误: 密钥 $KEY_NAME 混淆失败" >&2 - return 1 2>/dev/null || exit 1 + should_exit_or_return 1 fi LDFLAGS="$LDFLAGS -X '$GO_VAR=$OBFUSCATED_VAL'" @@ -89,6 +124,11 @@ done export EXTRA_LDFLAGS="$LDFLAGS" +# 若没有任何 key 被注入,则显式清空(避免上次会话残留) +if [ -z "$LDFLAGS" ]; then + export EXTRA_LDFLAGS="" +fi + echo "成功!已设置 EXTRA_LDFLAGS(ktalbum-tools)。" >&2 echo "示例构建:" >&2 From 4c5c8624bc67bee6cbfb85da81ec6cf55c173af3 Mon Sep 17 00:00:00 2001 From: LuSrackhall <3647637206@qq.com> Date: Sun, 4 Jan 2026 02:30:31 +0800 Subject: [PATCH 6/7] =?UTF-8?q?chore:=20=E5=A2=9E=E5=BC=BA=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=81=A5=E5=A3=AE=E6=80=A7=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=A7=81=E9=92=A5=E6=96=87=E4=BB=B6=E7=BC=BA=E5=A4=B1=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E6=97=A0=E6=B3=95=E6=9E=84=E5=BB=BA=E9=97=AE=E9=A2=98?= =?UTF-8?q?,=20=E7=A1=AE=E4=BF=9D=E6=97=A0=E7=A7=81=E9=92=A5=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B9=9F=E5=8F=AF=E6=AD=A3=E5=B8=B8=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=B8=AD=E5=86=99=E6=AD=BB=E7=9A=84=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design.md | 7 +++++++ .../proposal.md | 5 +++++ .../tasks.md | 3 +++ sdk/setup_build_env.sh | 16 ++++++---------- tools/ktalbum-tools/setup_build_env.sh | 15 +++++---------- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/openspec/changes/update-build-injected-symmetric-keys/design.md b/openspec/changes/update-build-injected-symmetric-keys/design.md index b03777d3..491bdcca 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/design.md +++ b/openspec/changes/update-build-injected-symmetric-keys/design.md @@ -70,6 +70,13 @@ 该行为的目标是:在不要求用户立刻补齐新增环境变量的前提下,依旧保持与适配前版本的兼容性。 +### Shell Robustness Notes + +为了保证在 `dev.sh` 的严格模式(`set -euo pipefail`)下也稳定: + +- 当脚本需要在“缺失私钥文件”场景下提前结束时,必须在脚本顶层直接使用 `return ... || exit ...`,而不是在函数内封装 `return`(函数内 `return` 只会返回函数,无法中止被 `source` 的脚本继续执行)。 +- 在包含中文标点的输出字符串中,变量展开建议统一使用 `${VAR}` 形式,避免某些 locale/编码情况下 `set -u` 将紧邻的非 ASCII 字节误判为变量名的一部分。 + ### ktalbum-tools(本地调试工具) ktalbum-tools 的目标是便于本地查看/调试 `.ktalbum` 文件,因此它需要在“一个二进制”中尽量兼容两类产物: diff --git a/openspec/changes/update-build-injected-symmetric-keys/proposal.md b/openspec/changes/update-build-injected-symmetric-keys/proposal.md index 6304841f..fb894a12 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/proposal.md +++ b/openspec/changes/update-build-injected-symmetric-keys/proposal.md @@ -55,6 +55,11 @@ - `private_keys.env` 存在但缺少新增条目时(历史文件),脚本不得失败;缺失项应跳过注入。 - 若 `private_keys.env` 中仍是模板占位符(如 `PLACEHOLDER_*` / `REPLACE_ME`),必须视为“未配置”并跳过注入,避免误覆盖开源默认密钥导致兼容性破坏。 +补充(本次修复中实际发现的两个具体故障形态): + +- `set -u` 下出现 `KEYS_FILE�: unbound variable`:根因是输出字符串中使用 `$KEYS_FILE,`(紧邻非 ASCII 标点),在某些编码/locale 下会被解析成“带异常字节的变量名”。修复:统一改用 `${KEYS_FILE}` 形式。 +- “无私钥文件”分支虽然设置了 `EXTRA_LDFLAGS=""`,但仍继续执行后续混淆流程:根因是把 `return/exit` 封装进函数,函数内 `return` 只返回函数,无法中止被 `source` 的脚本。修复:在脚本顶层直接 `return ... || exit ...`。 + | Key/Secret | Default(源码) | 注入变量(Go -ldflags -X) | 用途摘要 | | ----------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- | | 签名 KeyA | `KeyTone2024Signature_KeyA_SecureEncryptionKeyForIDEncryption` | `KeyTone/signature.KeyToneSignatureEncryptionKeyA` | 加密签名ID、派生动态密钥(PBKDF2) | diff --git a/openspec/changes/update-build-injected-symmetric-keys/tasks.md b/openspec/changes/update-build-injected-symmetric-keys/tasks.md index 991ac745..a46ae3b6 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/tasks.md +++ b/openspec/changes/update-build-injected-symmetric-keys/tasks.md @@ -57,3 +57,6 @@ - [x] `sdk/setup_build_env.sh`:缺失新增 `KEY_*` 时跳过注入(兼容历史 private_keys.env) - [x] `sdk/setup_build_env.sh`:模板占位符(`PLACEHOLDER_*`/`REPLACE_ME`)视为未配置并跳过,避免误覆盖默认密钥 - [x] `tools/ktalbum-tools/setup_build_env.sh`:同等容错(缺失/占位符跳过) +- [x] `sdk/setup_build_env.sh`:修复 `set -u` 下 `KEYS_FILE�: unbound variable`(统一使用 `${VAR}` 展开) +- [x] `sdk/setup_build_env.sh`:修复“无私钥文件分支仍继续执行”的 source 早退问题(顶层 `return ... || exit ...`) +- [x] `tools/ktalbum-tools/setup_build_env.sh`:修复 source 场景下 `exit 0` 会退出当前 shell 的问题(改为 `return 0 || exit 0`) diff --git a/sdk/setup_build_env.sh b/sdk/setup_build_env.sh index 4bab7620..94dd1a5b 100755 --- a/sdk/setup_build_env.sh +++ b/sdk/setup_build_env.sh @@ -63,11 +63,6 @@ KEYS_TO_PROCESS=( # =================逻辑区域================= -should_exit_or_return() { - local code="$1" - return "$code" 2>/dev/null || exit "$code" -} - is_placeholder_value() { local v="$1" [[ -z "$v" ]] && return 0 @@ -110,17 +105,18 @@ read_env_value_from_file() { # - 未提供私钥文件时,开源构建依然应当可运行(使用源码默认密钥);因此这里不再视为错误。 # - 若仅需要部分注入(例如只注入授权流密钥),也允许其它 key 缺失并自动跳过。 if [ ! -f "$KEYS_FILE" ]; then - echo "提示: 未找到私钥文件 $KEYS_FILE,将不设置 EXTRA_LDFLAGS(使用源码默认密钥/secret)。" >&2 + echo "提示: 未找到私钥文件 ${KEYS_FILE},将不设置 EXTRA_LDFLAGS(使用源码默认密钥/secret)。" >&2 export EXTRA_LDFLAGS="" if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then echo "export EXTRA_LDFLAGS=\"\"" fi - should_exit_or_return 0 + # 注意:这里必须直接 return/exit 以中止被 source 的脚本继续执行 + return 0 2>/dev/null || exit 0 fi # 检查混淆工具是否存在 if [ ! -f "$OBFUSCATOR_TOOL" ]; then - echo "错误: 未找到混淆工具源码 $OBFUSCATOR_TOOL" >&2 + echo "错误: 未找到混淆工具源码 ${OBFUSCATOR_TOOL}" >&2 return 1 2>/dev/null || exit 1 fi @@ -138,7 +134,7 @@ for entry in "${KEYS_TO_PROCESS[@]}"; do GO_VAR=$(echo "$entry" | cut -d':' -f2) # 从文件中读取明文密钥(兼容 set -euo pipefail) - PLAINTEXT_KEY=$(read_env_value_from_file "$KEY_NAME" "$KEYS_FILE") + PLAINTEXT_KEY=$(read_env_value_from_file "${KEY_NAME}" "${KEYS_FILE}") # 兼容性: # - 若缺失该 KEY_*,则跳过注入,让 Go 侧回退到源码默认值(保持与旧版/开源版兼容) @@ -158,7 +154,7 @@ for entry in "${KEYS_TO_PROCESS[@]}"; do # $? 获取上一个命令的退出状态码,0 表示成功 if [ $? -ne 0 ]; then echo "错误: 密钥 $KEY_NAME 混淆失败" >&2 - should_exit_or_return 1 + return 1 2>/dev/null || exit 1 fi # 拼接到 LDFLAGS 字符串中 diff --git a/tools/ktalbum-tools/setup_build_env.sh b/tools/ktalbum-tools/setup_build_env.sh index 7254d9ee..3d6be672 100644 --- a/tools/ktalbum-tools/setup_build_env.sh +++ b/tools/ktalbum-tools/setup_build_env.sh @@ -47,11 +47,6 @@ KEYS_TO_PROCESS=( # =================逻辑区域================= -should_exit_or_return() { - local code="$1" - return "$code" 2>/dev/null || exit "$code" -} - is_placeholder_value() { local v="$1" [[ -z "$v" ]] && return 0 @@ -84,18 +79,18 @@ read_env_value_from_file() { # 检查混淆工具是否存在 if [ ! -f "$OBFUSCATOR_TOOL" ]; then - echo "错误: 未找到混淆工具源码 $OBFUSCATOR_TOOL" >&2 + echo "错误: 未找到混淆工具源码 ${OBFUSCATOR_TOOL}" >&2 return 1 2>/dev/null || exit 1 fi # 如果没有私钥文件,允许继续(开源构建不需要 EXTRA_LDFLAGS) if [ ! -f "$KEYS_FILE" ]; then - echo "提示: 未找到私钥文件 $KEYS_FILE,将不设置 EXTRA_LDFLAGS(开源默认密钥模式)。" >&2 + echo "提示: 未找到私钥文件 ${KEYS_FILE},将不设置 EXTRA_LDFLAGS(开源默认密钥模式)。" >&2 export EXTRA_LDFLAGS="" if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then echo "export EXTRA_LDFLAGS=\"\"" fi - exit 0 + return 0 2>/dev/null || exit 0 fi # 初始化 LDFLAGS 字符串 @@ -107,7 +102,7 @@ for entry in "${KEYS_TO_PROCESS[@]}"; do KEY_NAME=$(echo "$entry" | cut -d':' -f1) GO_VAR=$(echo "$entry" | cut -d':' -f2) - PLAINTEXT_KEY=$(read_env_value_from_file "$KEY_NAME" "$KEYS_FILE") + PLAINTEXT_KEY=$(read_env_value_from_file "${KEY_NAME}" "${KEYS_FILE}") if is_placeholder_value "$PLAINTEXT_KEY"; then echo "提示: 跳过 ${KEY_NAME}(未配置或仍为模板占位符),将不设置 EXTRA_LDFLAGS(开源默认密钥模式)。" >&2 continue @@ -116,7 +111,7 @@ for entry in "${KEYS_TO_PROCESS[@]}"; do OBFUSCATED_VAL=$(go run "$OBFUSCATOR_TOOL" -key "$PLAINTEXT_KEY") if [ $? -ne 0 ]; then echo "错误: 密钥 $KEY_NAME 混淆失败" >&2 - should_exit_or_return 1 + return 1 2>/dev/null || exit 1 fi LDFLAGS="$LDFLAGS -X '$GO_VAR=$OBFUSCATED_VAL'" From 8a8e0b338e0d2174b1cadb466bf0e38c3a63af14 Mon Sep 17 00:00:00 2001 From: LuSrackhall <3647637206@qq.com> Date: Sun, 4 Jan 2026 23:09:38 +0800 Subject: [PATCH 7/7] =?UTF-8?q?chore(fix):=20=E6=9B=B4=E6=96=B0=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E6=B3=A8=E5=85=A5=E6=96=87=E6=A1=A3=EF=BC=8C=E6=98=8E?= =?UTF-8?q?=E7=A1=AE=E5=B7=A5=E5=85=B7=E8=BE=93=E5=87=BA=E8=A6=81=E6=B1=82?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=9E=84=E5=BB=BA=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7,=20=E9=98=B2=E6=AD=A2=E6=B7=B7=E6=B7=86=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=E4=B8=AD=E6=B7=B7=E5=85=A5=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E7=9A=84=E8=AD=A6=E5=91=8A=E6=88=96=E9=94=99=E8=AF=AF=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../update-build-injected-symmetric-keys/design.md | 9 +++++++++ .../update-build-injected-symmetric-keys/proposal.md | 5 +++++ .../specs/encrypted-outputs/spec.md | 2 ++ .../update-build-injected-symmetric-keys/tasks.md | 1 + openspec/specs/encrypted-outputs/spec.md | 2 ++ tools/key-obfuscator/main.go | 7 +++++-- 6 files changed, 24 insertions(+), 2 deletions(-) diff --git a/openspec/changes/update-build-injected-symmetric-keys/design.md b/openspec/changes/update-build-injected-symmetric-keys/design.md index 491bdcca..8600d0a8 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/design.md +++ b/openspec/changes/update-build-injected-symmetric-keys/design.md @@ -15,6 +15,15 @@ - 通过 Go 链接器 `-X 'package.path.VarName=VALUE'` 注入 - `VALUE` 推荐为:`tools/key-obfuscator` 输出的 hex(对任意长度字符串可用;长度非 32 时会提示 warning) +#### tool stdout/stderr contract(跨平台关键约束) + +`tools/key-obfuscator` 的输出必须满足: + +- `stdout`:仅输出混淆后的 hex 字符串(机器可消费,允许被脚本 `$(...)` 捕获并直接用于 `-ldflags -X`)。 +- `stderr`:输出任何 warning/info/error 文本(例如“长度不是 32”的提示)。 + +原因:CI(如 GitHub Actions)常将注入值复制为 secrets;若 warning 混入 stdout,会污染 `EXTRA_LDFLAGS`,导致构建失败或运行期走 fallback 造成兼容性破坏。 + ### Runtime - 若变量值等于默认常量,则直接按默认明文使用 diff --git a/openspec/changes/update-build-injected-symmetric-keys/proposal.md b/openspec/changes/update-build-injected-symmetric-keys/proposal.md index fb894a12..c742d9b6 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/proposal.md +++ b/openspec/changes/update-build-injected-symmetric-keys/proposal.md @@ -60,6 +60,11 @@ - `set -u` 下出现 `KEYS_FILE�: unbound variable`:根因是输出字符串中使用 `$KEYS_FILE,`(紧邻非 ASCII 标点),在某些编码/locale 下会被解析成“带异常字节的变量名”。修复:统一改用 `${KEYS_FILE}` 形式。 - “无私钥文件”分支虽然设置了 `EXTRA_LDFLAGS=""`,但仍继续执行后续混淆流程:根因是把 `return/exit` 封装进函数,函数内 `return` 只返回函数,无法中止被 `source` 的脚本。修复:在脚本顶层直接 `return ... || exit ...`。 +补充(CI 注入相关): + +- `tools/key-obfuscator` 在 key 长度不为 32 时会打印 `Warning: ...`,但历史实现将 warning 打到 `stdout`,导致 `sdk/setup_build_env.sh` 捕获 `$(go run ...)` 时把 warning 一并拼进 `-ldflags -X` 的值,进而污染 GitHub Actions secrets / 破坏构建与运行兼容性。 +- 修复策略:规定并实现 `stdout` 仅输出混淆后的 hex;任何 warning/info/error 必须写入 `stderr`。 + | Key/Secret | Default(源码) | 注入变量(Go -ldflags -X) | 用途摘要 | | ----------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- | | 签名 KeyA | `KeyTone2024Signature_KeyA_SecureEncryptionKeyForIDEncryption` | `KeyTone/signature.KeyToneSignatureEncryptionKeyA` | 加密签名ID、派生动态密钥(PBKDF2) | diff --git a/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md index f31509a9..3b7555a9 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md +++ b/openspec/changes/update-build-injected-symmetric-keys/specs/encrypted-outputs/spec.md @@ -16,6 +16,8 @@ Note: The internal debug utility `sdk/audioPackage/cmd/printconfig` depends on ` Note: For local development, the helper script `sdk/setup_build_env.sh` SHOULD treat missing `private_keys.env` or missing/placeholder `KEY_*` entries as “not injected” (skip `-ldflags -X` for that key) to preserve backward compatibility. +Note: The obfuscation tool used by the scripts (`tools/key-obfuscator`) MUST output only the obfuscated hex value to `stdout`. Any warning/info text (including non-32-byte length warnings) MUST go to `stderr` so `EXTRA_LDFLAGS` and CI secrets remain valid. + #### Scenario: 注入密钥后自动解混淆 - **GIVEN** 构建过程中通过 `-ldflags -X` 注入了 XOR 混淆后的 hex diff --git a/openspec/changes/update-build-injected-symmetric-keys/tasks.md b/openspec/changes/update-build-injected-symmetric-keys/tasks.md index a46ae3b6..d03002b8 100644 --- a/openspec/changes/update-build-injected-symmetric-keys/tasks.md +++ b/openspec/changes/update-build-injected-symmetric-keys/tasks.md @@ -60,3 +60,4 @@ - [x] `sdk/setup_build_env.sh`:修复 `set -u` 下 `KEYS_FILE�: unbound variable`(统一使用 `${VAR}` 展开) - [x] `sdk/setup_build_env.sh`:修复“无私钥文件分支仍继续执行”的 source 早退问题(顶层 `return ... || exit ...`) - [x] `tools/ktalbum-tools/setup_build_env.sh`:修复 source 场景下 `exit 0` 会退出当前 shell 的问题(改为 `return 0 || exit 0`) +- [x] `tools/key-obfuscator/main.go`:stdout 仅输出混淆后的 hex;Warning/错误输出到 stderr,避免污染 `EXTRA_LDFLAGS` 与 CI secrets diff --git a/openspec/specs/encrypted-outputs/spec.md b/openspec/specs/encrypted-outputs/spec.md index 78b049ee..609a7f79 100644 --- a/openspec/specs/encrypted-outputs/spec.md +++ b/openspec/specs/encrypted-outputs/spec.md @@ -19,6 +19,8 @@ Normative: The system SHALL support overriding selected symmetric keys/secrets a Note: The recommended helper script `sdk/setup_build_env.sh` is intentionally best-effort for compatibility. If `sdk/private_keys.env` is missing, or if individual `KEY_*` entries are missing / still template placeholders, the script SHOULD skip those injections and leave `EXTRA_LDFLAGS` empty or partial, so builds remain compatible with the default open-source behavior. +Note: The obfuscation helper (`tools/key-obfuscator`) MUST keep `stdout` machine-consumable. `stdout` SHALL contain only the obfuscated hex string (no warnings, no extra text). Any warnings (e.g. non-32-byte key length) or errors MUST be printed to `stderr` so CI systems (GitHub Actions secrets) and scripts capturing `$(...)` are not polluted. + #### Scenario: 开源构建未注入 - **GIVEN** 用户从公开源码直接构建且未提供私钥文件/注入参数 diff --git a/tools/key-obfuscator/main.go b/tools/key-obfuscator/main.go index 9aa7aad3..2fc1a720 100644 --- a/tools/key-obfuscator/main.go +++ b/tools/key-obfuscator/main.go @@ -16,13 +16,16 @@ func main() { flag.Parse() if *keyPtr == "" { - fmt.Println("Please provide a key using -key flag") + // NOTE: stdout is reserved for the machine-consumable hex output. + // Any human-facing messages MUST go to stderr to avoid polluting build flags. + fmt.Fprintln(os.Stderr, "Please provide a key using -key flag") os.Exit(1) } key := []byte(*keyPtr) if len(key) != 32 { - fmt.Printf("Warning: Key length is %d, expected 32 bytes.\n", len(key)) + // NOTE: keep warning on stderr so stdout stays pure hex. + fmt.Fprintf(os.Stderr, "Warning: Key length is %d, expected 32 bytes.\n", len(key)) } obfuscated := make([]byte, len(key))