Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/sdk/KeyToneSetting.json
/sdk/log.jsonl
/sdk/temporaryDebug
/sdk/printconfig

# Private Keys
private_keys.env
Expand Down
17 changes: 17 additions & 0 deletions BUILD_COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions BUILD_COMPATIBILITY.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

---

# 加密产物的兼容性
Expand Down
121 changes: 121 additions & 0 deletions openspec/changes/update-build-injected-symmetric-keys/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Design: 构建注入式对称密钥适配

## Overview

本设计将历史遗留的对称密钥/secret 统一到与授权流相同的构建注入体系:

- 默认值保留在源码中(开源构建无需私钥文件即可工作)
- 私有构建通过 `-ldflags -X` 覆盖变量值
- 覆盖值为 XOR 混淆后的 hex 字符串,运行时自动解混淆为明文

## Injection Format

### Build-Time

- 通过 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

- 若变量值等于默认常量,则直接按默认明文使用
- 否则按以下逻辑尝试解混淆:
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 Behavior (重要)

为保证历史 `private_keys.env`(未包含新增 KEY_*)以及“仅开源构建”场景可正常运行:

- `sdk/setup_build_env.sh` 采用 best-effort 策略:
- 若未找到 `private_keys.env`,脚本不会报错退出,而是设置 `EXTRA_LDFLAGS=""` 并继续(等价于不注入)。
- 若某个 `KEY_*` 缺失,或值仍为模板占位符(如 `PLACEHOLDER_*` / `REPLACE_ME`),脚本会跳过该 key 的注入,让运行时回退到源码默认值。

该行为的目标是:在不要求用户立刻补齐新增环境变量的前提下,依旧保持与适配前版本的兼容性。

### Shell Robustness Notes

为了保证在 `dev.sh` 的严格模式(`set -euo pipefail`)下也稳定:

- 当脚本需要在“缺失私钥文件”场景下提前结束时,必须在脚本顶层直接使用 `return ... || exit ...`,而不是在函数内封装 `return`(函数内 `return` 只会返回函数,无法中止被 `source` 的脚本继续执行)。
- 在包含中文标点的输出字符串中,变量展开建议统一使用 `${VAR}` 形式,避免某些 locale/编码情况下 `set -u` 将紧邻的非 ASCII 字节误判为变量名的一部分。

### 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

### 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

- 未注入:行为与当前开源版本完全一致
- 注入后:
- 官方/私有构建与社区构建的加密产物可能不互通(符合 BUILD_COMPATIBILITY 设计)
- 若选择覆盖 v1 key,将导致旧 v1 产物在该私有构建中不可解密(预期行为,需在 proposal 中显式告知)
89 changes: 89 additions & 0 deletions openspec/changes/update-build-injected-symmetric-keys/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 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 项。
- 为本地调试工具 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

- 不改变默认开源构建行为:未提供私钥注入时,仍使用源码默认值。
- 不更换加密算法(仅调整密钥来源/注入方式)。

## 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`
- `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`

## 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`),必须视为“未配置”并跳过注入,避免误覆盖开源默认密钥导致兼容性破坏。

补充(本次修复中实际发现的两个具体故障形态):

- `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) |
| 签名 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` 也提供同等注入点(模块路径不同)。

另外: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" ...`,且同一次运行无法同时兼容两套密钥(需分别运行)
- **结论:无遗漏**
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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.

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` 覆盖值
- **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.

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
- **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.

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** 用户导出专辑
- **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 回退
Loading
Loading