chore: initial commit (keys)
This commit is contained in:
commit
76c8d18f7f
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.geminiignore
|
||||||
38
CHANGELOG.md
Normal file
38
CHANGELOG.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Changelog: @go/keys
|
||||||
|
|
||||||
|
## v1.0.1 (2026-05-06)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
- **Mandatory Master Password**: 全局强制主密码保护,启动时需验证或设置主密码。本地密钥库数据由 AES-GCM-256 基于主密码加密保护。
|
||||||
|
- **Password Vault (密码管理器)**: 新增简易的个人密码管理功能,支持 `vault-add`, `vault-get`, `vault-list`, `vault-del` 等指令,数据通过主密码加密安全存储于 `.vault` 文件中。
|
||||||
|
- **Semantic CLI Aliases**: CLI 指令全面支持单字母语义化简写(如 `c` 代替 `create`, `e` 代替 `encrypt`, `gcm` 代替 `aes-gcm`)。
|
||||||
|
- **Interactive UI Enhancement**: 交互模式进入时提供单行的核心支持命令高亮引导。
|
||||||
|
|
||||||
|
### 🛠 Improvements
|
||||||
|
- **Infrastructure Alignment**:
|
||||||
|
- 升级核心依赖 `@go/crypto`, `@go/crypto-sm`, `@go/cast`, `@go/encoding` 等基础设施至最新版。
|
||||||
|
- 将本地 IO 操作迁移并适配到 `@go/file` 高性能基础库。
|
||||||
|
- **Code Quality**: 优化部分变量命名与错误信息提示,增强代码可读性与鲁棒性。
|
||||||
|
|
||||||
|
## v1.0.0 (2026-05-05)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
- **多语言扩展**: 新增 `-python` 和 `-js` 导出支持,覆盖主流开发语言。
|
||||||
|
- **强化混淆 (Go)**: Go 语言导出模板现采用 `safe.EncryptChaCha20` 对密钥进行二次加密。
|
||||||
|
- 引入 1KB 随机内存池 (`pool`)。
|
||||||
|
- 密钥与 Salt 通过算法从内存池中动态提取,极大提升反编译难度。
|
||||||
|
- **Legacy 迁移**: 完成从 `ssgo/tool/sskey` 到 `apigo.cc/go/keys` 的全面重构与迁移。
|
||||||
|
...
|
||||||
|
- **跨语言加固**:
|
||||||
|
- Go 语言导出模板集成 `safe.LockMemory`、`safe.DecryptChaCha20` 与 `SecretPlaintext` 生命周期管理。
|
||||||
|
- 同步更新 PHP、Java、Python、JavaScript 的混淆生成模板。
|
||||||
|
|
||||||
|
### 🛠 Improvements
|
||||||
|
- **健壮性**: 库函数增加 `nil` 指针防御与完整的错误状态校验。
|
||||||
|
- **自动降级**: 解密过程支持从 `aes-gcm` 自动降级至 `aes-cbc` 以兼容历史数据。
|
||||||
|
- **工程化**: 建立完善的单元测试 (`keys_test.go`) 与性能基准测试 (`bench_test.go`)。
|
||||||
|
|
||||||
|
### 🔒 Security
|
||||||
|
- 密钥存储目录迁移至 `~/keys/`。
|
||||||
|
- 强制执行校验位检查,防止非法密钥加载。
|
||||||
|
- 导出代码强制执行明文即时擦除策略。
|
||||||
68
README.md
Normal file
68
README.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Keys CLI
|
||||||
|
|
||||||
|
`@go/keys` 是一款基于 `@go/crypto` 和 `@go/safe` 构建的**现代化本地 KMS (Key Management System) 与密码管理器**。旨在提供极高安全标准的密钥全生命周期管理,并支持跨语言的安全代码集成。
|
||||||
|
|
||||||
|
## 🎯 设计哲学
|
||||||
|
|
||||||
|
* **安全第一**:纯二进制存储,彻底抹除文件固定特征码;使用 `crypto/rand` 替代 `math/rand`;解密输出阅后即焚(10秒自动擦除)。
|
||||||
|
* **动态混淆 (Dynamic Obfuscation)**:内存与磁盘的数据格式均采用**奇偶指针 + 链式偏移量**的设计,使得 Key/IV/Salt 的位置完全随机且无规律。
|
||||||
|
* **双模式架构**:分离“密钥管理 (Keystore)”与“密码管理 (Password)”,提供沉浸式的交互式 REPL 体验。
|
||||||
|
* **内存安全**:基于 `@go/safe`,主密码与实际密钥在运行期常驻 `SafeBuf`,使用完毕即刻内存擦除,防范内存扫描泄露。
|
||||||
|
|
||||||
|
## 🚀 命令行指南
|
||||||
|
|
||||||
|
```text
|
||||||
|
Keys CLI - 现代化本地 KMS 与密码管理器
|
||||||
|
|
||||||
|
用法:
|
||||||
|
keys 启动交互式密钥管理模式 (Key Manager)
|
||||||
|
keys <keyname> 启动交互式密码管理模式 (Password Manager)
|
||||||
|
|
||||||
|
# 非交互模式 (自动化测试与脚本友好,便于 CI/CD 和自动化流)
|
||||||
|
keys cmd keystore list
|
||||||
|
keys cmd keystore create <keyname>
|
||||||
|
keys cmd keystore remove <keyname>
|
||||||
|
keys cmd keystore export <keyname> <filepath.ext>
|
||||||
|
|
||||||
|
keys cmd password <keyname> list
|
||||||
|
keys cmd password <keyname> create <pwd.path.name> <value>
|
||||||
|
keys cmd password <keyname> remove <pwd.path.name>
|
||||||
|
keys cmd password <keyname> encrypt <pwd.path.name | raw_text> (不回显输入)
|
||||||
|
keys cmd password <keyname> decrypt <pwd.path.name | ciphertext>
|
||||||
|
|
||||||
|
环境变量:
|
||||||
|
KEYSPATH 指定数据根目录 (默认依序检查 ~/.keyspath,再 fallback 到 ~/.keys)
|
||||||
|
MASTER_PASSWORD 非交互模式下通过环境变量传递主密码
|
||||||
|
|
||||||
|
安全特性:
|
||||||
|
- 无痕存储:纯二进制写入,动态混淆偏移量,文件无固定特征码。
|
||||||
|
- 强随机数:全局摒弃 math/rand,核心加密全部使用 crypto/rand。
|
||||||
|
- 防偷窥:终端解密输出明文 10 秒后自动擦除。
|
||||||
|
- 防内存扫描:运行期 KeyBuf 在使用完毕后即刻擦除,绝不长留。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠 交互模式效果预览
|
||||||
|
|
||||||
|
**1. 密钥管理模式 (Keystore Mode)**
|
||||||
|
```text
|
||||||
|
$ keys
|
||||||
|
Enter Master Password: (不回显)
|
||||||
|
keys [keystore]> [Tab触发下拉提示]
|
||||||
|
list - 列出所有主密钥
|
||||||
|
create - 创建新的主密钥
|
||||||
|
use - 使用某密钥进入密码管理模式
|
||||||
|
export - 导出混淆后的应用集成代码
|
||||||
|
keys [keystore]> use db-prod
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 密码管理模式 (Password Mode)**
|
||||||
|
```text
|
||||||
|
keys [db-prod]> [Tab触发下拉提示]
|
||||||
|
list - 列出所有被 db-prod 保护的密码项
|
||||||
|
create - 添加新密码项 (如 db.mysql.root 123456)
|
||||||
|
encrypt - 临时加密一段文本或已有密码项 (不回显输入)
|
||||||
|
decrypt - 解密密码项 (10秒后清屏)
|
||||||
|
back - 返回密钥管理模式
|
||||||
|
keys [db-prod]> decrypt db.mysql.root
|
||||||
|
> 123456 (剩余 10s...) # 倒计时结束当前行被清除
|
||||||
|
```
|
||||||
41
TEST.md
Normal file
41
TEST.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Test Report: @go/keys
|
||||||
|
|
||||||
|
## 测试环境
|
||||||
|
- **操作系统**: darwin (macOS)
|
||||||
|
- **架构**: amd64
|
||||||
|
- **Go 版本**: go 1.25.0+
|
||||||
|
- **CPU**: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
||||||
|
|
||||||
|
## 单元测试结果
|
||||||
|
运行命令: `go test -v ./lib`
|
||||||
|
|
||||||
|
| 测试用例 | 状态 | 描述 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `TestKeyLifecycle` | PASS | 验证密钥创建、加载及全算法加解密闭环 |
|
||||||
|
| `TestKeyErrors` | PASS | 验证文件缺失、格式损坏、校验位错误及非法算法处理 |
|
||||||
|
| `TestEmptyData` | PASS | 验证零长度数据的加解密处理 |
|
||||||
|
| `TestMakeCode` | PASS | 验证 Go/PHP/Java 三种语言的集成代码生成 |
|
||||||
|
|
||||||
|
## 性能基准测试 (Benchmark)
|
||||||
|
数据规模: 1KB 负载
|
||||||
|
|
||||||
|
### 加密性能 (Encryption)
|
||||||
|
| 算法 | 性能 (ns/op) |
|
||||||
|
| :--- | :--- |
|
||||||
|
| AES-GCM (默认) | 5365 |
|
||||||
|
| AES-CBC | 5949 |
|
||||||
|
| SM4-GCM | 6795 |
|
||||||
|
| SM4-CBC | 14249 |
|
||||||
|
|
||||||
|
### 解密性能 (Decryption)
|
||||||
|
| 算法 | 性能 (ns/op) |
|
||||||
|
| :--- | :--- |
|
||||||
|
| AES-GCM (默认) | 4801 |
|
||||||
|
| AES-CBC | 5381 |
|
||||||
|
| SM4-GCM | 7087 |
|
||||||
|
| SM4-CBC | 5751 |
|
||||||
|
|
||||||
|
## 安全性验证
|
||||||
|
- [x] **内存锁定**: 导出代码集成 `safe.LockMemory`。
|
||||||
|
- [x] **自动擦除**: 敏感明文在使用后立即调用 `Close()` 擦除。
|
||||||
|
- [x] **交换分区防护**: 通过 `mlock` 防止密钥泄露至磁盘。
|
||||||
26
go.mod
Normal file
26
go.mod
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module apigo.cc/go/keys
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
apigo.cc/go/crypto v1.3.0
|
||||||
|
apigo.cc/go/crypto-sm v1.3.0
|
||||||
|
apigo.cc/go/encoding v1.3.0
|
||||||
|
apigo.cc/go/safe v1.3.0
|
||||||
|
apigo.cc/go/shell v1.3.0
|
||||||
|
github.com/c-bata/go-prompt v0.2.6
|
||||||
|
golang.org/x/term v0.42.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
apigo.cc/go/cast v1.3.0 // indirect
|
||||||
|
apigo.cc/go/rand v1.3.0 // indirect
|
||||||
|
github.com/emmansun/gmsm v0.28.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||||
|
github.com/mattn/go-tty v0.0.3 // indirect
|
||||||
|
github.com/pkg/term v1.2.0-beta.2 // indirect
|
||||||
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
|
)
|
||||||
44
go.sum
Normal file
44
go.sum
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
apigo.cc/go/cast v1.2.8 h1:plb676DH2TjYljzf8OEMGT6lIhmZ/xaxEFfs0kDOiSI=
|
||||||
|
apigo.cc/go/crypto v1.1.0 h1:dv9ZRbtJHnnLbDHUfjP//GHLniu0/5ja0w5QE5hwwOU=
|
||||||
|
apigo.cc/go/crypto v1.1.0/go.mod h1:0NUsQMGiP95TWHJexb3F1MxNdW+LR8TD1VqwHPN8PR8=
|
||||||
|
apigo.cc/go/crypto-sm v1.0.5 h1:IjRx2CL//ZQMiR7lcWJU3P/eIXithgVUrMEfCiu4hfA=
|
||||||
|
apigo.cc/go/encoding v1.0.5 h1:a2XbXyd8D2gKo1ekXn/pt5adltWbIfdJCMhaF2uvzF0=
|
||||||
|
apigo.cc/go/encoding v1.0.5/go.mod h1:V5CgT7rBbCxy+uCU20q0ptcNNRSgMtpA8cNOs6r8IeI=
|
||||||
|
apigo.cc/go/rand v1.0.5 h1:AkUoWr0SELgeDmRjLEDjOIp29nXdzqQQvmGRIHpTN7U=
|
||||||
|
apigo.cc/go/rand v1.0.5/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||||
|
apigo.cc/go/safe v1.0.5 h1:yZJLhpMntJrtqU/ev0UlyOoHu/cLrnnGUO4aHyIZcwE=
|
||||||
|
apigo.cc/go/safe v1.0.5/go.mod h1:i9xnh7reJIFPauLnlzuIDgvrQvhjxpFlpVh3O6ulWd0=
|
||||||
|
apigo.cc/go/shell v1.0.5 h1:bmvUTJGe1GwsHAy42v3iaoK40PoBC7Xq1aMCYxUZmtg=
|
||||||
|
apigo.cc/go/shell v1.0.5/go.mod h1:sx/nYw5CihHWmo5JHkaZUbmMYXNHx8swzArbQCUGHjc=
|
||||||
|
github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI=
|
||||||
|
github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY=
|
||||||
|
github.com/emmansun/gmsm v0.28.0 h1:0WyTHmQgaAfM8IwMnNMJCfEiK999cZ2J8csfcZ2Ooco=
|
||||||
|
github.com/emmansun/gmsm v0.28.0/go.mod h1:9lKtK8f3c7wh2z0g6fsqRbay69V1jWYDcBaytyuR95M=
|
||||||
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||||
|
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
|
||||||
|
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
|
||||||
|
github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=
|
||||||
|
github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
|
||||||
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
179
lib/MakeCode.go
Normal file
179
lib/MakeCode.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var codeTemplates = map[string]string{
|
||||||
|
"go": goTpl,
|
||||||
|
"php": phpTpl,
|
||||||
|
"java": javaTpl,
|
||||||
|
"python": pythonTpl,
|
||||||
|
"js": jsTpl,
|
||||||
|
}
|
||||||
|
|
||||||
|
var goTpl = `package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apigo.cc/go/safe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
buf := []byte{ {{range $i, $v := .buf}}{{if $i}},{{end}}{{$v}}{{end}} }
|
||||||
|
|
||||||
|
// 锁定内存防止交换到磁盘
|
||||||
|
_ = safe.LockMemory(buf)
|
||||||
|
defer safe.UnlockMemory(buf)
|
||||||
|
|
||||||
|
indicator := buf[127]
|
||||||
|
ptr1Index := 126
|
||||||
|
if indicator%2 != 0 {
|
||||||
|
ptr1Index = 125
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetK := int(buf[ptr1Index])
|
||||||
|
key := buf[offsetK : offsetK+32]
|
||||||
|
|
||||||
|
offsetIV := int(buf[offsetK+32])
|
||||||
|
iv := buf[offsetIV : offsetIV+12]
|
||||||
|
|
||||||
|
safeKey := safe.NewSafeBufAndErase(append([]byte(nil), key...))
|
||||||
|
safeIv := safe.NewSafeBufAndErase(append([]byte(nil), iv...))
|
||||||
|
defer safeKey.Close()
|
||||||
|
defer safeIv.Close()
|
||||||
|
|
||||||
|
pKey := safeKey.Open()
|
||||||
|
defer pKey.Close()
|
||||||
|
pIv := safeIv.Open()
|
||||||
|
defer pIv.Close()
|
||||||
|
|
||||||
|
if setSSKey != nil {
|
||||||
|
setSSKey(pKey.Data, pIv.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 供应用层注入的回调
|
||||||
|
var setSSKey func(key, iv []byte)
|
||||||
|
`
|
||||||
|
|
||||||
|
var phpTpl = `<?php
|
||||||
|
|
||||||
|
$__sskeyStarter = function () {
|
||||||
|
if(!function_exists('set_sskey')) return;
|
||||||
|
$buf = [{{range $i, $v := .buf}}{{if $i}},{{end}}{{$v}}{{end}}];
|
||||||
|
|
||||||
|
$indicator = $buf[127];
|
||||||
|
$ptr1Index = ($indicator % 2 == 0) ? 126 : 125;
|
||||||
|
|
||||||
|
$offsetK = $buf[$ptr1Index];
|
||||||
|
$key = array_slice($buf, $offsetK, 32);
|
||||||
|
|
||||||
|
$offsetIV = $buf[$offsetK + 32];
|
||||||
|
$iv = array_slice($buf, $offsetIV, 12);
|
||||||
|
|
||||||
|
set_sskey($key, $iv);
|
||||||
|
};
|
||||||
|
|
||||||
|
$__sskeyStarter();
|
||||||
|
unset($__sskeyStarter);
|
||||||
|
`
|
||||||
|
|
||||||
|
var javaTpl = `
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public final class SSKeyStarter {
|
||||||
|
private static boolean inited = false;
|
||||||
|
public static void init() {
|
||||||
|
if (inited) return;
|
||||||
|
inited = true;
|
||||||
|
|
||||||
|
byte[] buf = new byte[]{ {{range $i, $v := .buf}}{{if $i}},{{end}}(byte){{$v}}{{end}} };
|
||||||
|
|
||||||
|
int indicator = buf[127] & 0xFF;
|
||||||
|
int ptr1Index = (indicator % 2 == 0) ? 126 : 125;
|
||||||
|
|
||||||
|
int offsetK = buf[ptr1Index] & 0xFF;
|
||||||
|
byte[] key = Arrays.copyOfRange(buf, offsetK, offsetK + 32);
|
||||||
|
|
||||||
|
int offsetIV = buf[offsetK + 32] & 0xFF;
|
||||||
|
byte[] iv = Arrays.copyOfRange(buf, offsetIV, offsetIV + 12);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<?> c = Class.forName("SSKeySetter");
|
||||||
|
Method m = c.getMethod("set", byte[].class, byte[].class);
|
||||||
|
m.invoke(null, key, iv);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var pythonTpl = `
|
||||||
|
def init_sskey(set_sskey_func):
|
||||||
|
buf = [{{range $i, $v := .buf}}{{if $i}},{{end}}{{$v}}{{end}}]
|
||||||
|
|
||||||
|
indicator = buf[127]
|
||||||
|
ptr1Index = 126 if indicator % 2 == 0 else 125
|
||||||
|
|
||||||
|
offsetK = buf[ptr1Index]
|
||||||
|
key = bytes(buf[offsetK : offsetK + 32])
|
||||||
|
|
||||||
|
offsetIV = buf[offsetK + 32]
|
||||||
|
iv = bytes(buf[offsetIV : offsetIV + 12])
|
||||||
|
|
||||||
|
set_sskey_func(key, iv)
|
||||||
|
`
|
||||||
|
|
||||||
|
var jsTpl = `
|
||||||
|
const initSSKey = (setSSKeyFunc) => {
|
||||||
|
if (typeof setSSKeyFunc !== 'function') return;
|
||||||
|
const buf = [{{range $i, $v := .buf}}{{if $i}},{{end}}{{$v}}{{end}}];
|
||||||
|
|
||||||
|
const indicator = buf[127];
|
||||||
|
const ptr1Index = (indicator % 2 === 0) ? 126 : 125;
|
||||||
|
|
||||||
|
const offsetK = buf[ptr1Index];
|
||||||
|
const key = new Uint8Array(buf.slice(offsetK, offsetK + 32));
|
||||||
|
|
||||||
|
const offsetIV = buf[offsetK + 32];
|
||||||
|
const iv = new Uint8Array(buf.slice(offsetIV, offsetIV + 12));
|
||||||
|
|
||||||
|
setSSKeyFunc(key, iv);
|
||||||
|
};
|
||||||
|
`
|
||||||
|
|
||||||
|
// MakeCode generates application integration code containing the dynamic obfuscated buffer.
|
||||||
|
func MakeCode(codeName string, key, iv []byte) (string, error) {
|
||||||
|
tpl := codeTemplates[codeName]
|
||||||
|
if tpl == "" {
|
||||||
|
return "", errors.New("tpl not exists: " + codeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-pack into 128 byte dynamically obfuscated buffer
|
||||||
|
buf, err := PackMemoryKey(key, iv)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"buf": buf,
|
||||||
|
}
|
||||||
|
|
||||||
|
t := template.New(codeName)
|
||||||
|
t, err = t.Parse(tpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := bytes.NewBuffer(nil)
|
||||||
|
err = t.Execute(out, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String(), nil
|
||||||
|
}
|
||||||
237
lib/keystore.go
Normal file
237
lib/keystore.go
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apigo.cc/go/crypto"
|
||||||
|
"apigo.cc/go/crypto-sm"
|
||||||
|
"apigo.cc/go/file"
|
||||||
|
"apigo.cc/go/safe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeySet struct {
|
||||||
|
Name string
|
||||||
|
buf *safe.SafeBuf // Holds the 128-byte obfuscated key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close safely erases the memory buffer.
|
||||||
|
func (k *KeySet) Close() {
|
||||||
|
if k == nil || k.buf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
k.buf.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRaw extracts the actual 32-byte Key and 12-byte IV from the obfuscated buffer.
|
||||||
|
func (k *KeySet) GetRaw() ([]byte, []byte, error) {
|
||||||
|
if k == nil || k.buf == nil {
|
||||||
|
return nil, nil, errors.New("invalid keyset")
|
||||||
|
}
|
||||||
|
|
||||||
|
plain := k.buf.Open()
|
||||||
|
defer plain.Close()
|
||||||
|
|
||||||
|
key, iv, err := UnpackMemoryKey(plain.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return key, iv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSymmetric creates a symmetric cipher from the keyset using the specified algorithm.
|
||||||
|
func (k *KeySet) NewSymmetric(algo string) (*crypto.Symmetric, error) {
|
||||||
|
key, iv, err := k.GetRaw()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer safe.ZeroMemory(key)
|
||||||
|
defer safe.ZeroMemory(iv)
|
||||||
|
|
||||||
|
switch strings.ToLower(algo) {
|
||||||
|
case "aes-gcm", "gcm", "":
|
||||||
|
return crypto.NewAESGCMWithoutEraseKey(key, iv[:12])
|
||||||
|
case "aes-cbc", "cbc":
|
||||||
|
cbcIv := iv
|
||||||
|
if len(cbcIv) == 12 {
|
||||||
|
cbcIv = append(cbcIv, 0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
return crypto.NewAESCBCWithoutEraseKey(key, cbcIv)
|
||||||
|
case "sm4-gcm", "sm4":
|
||||||
|
return sm.NewSM4GCMWithoutEraseKey(key, iv[:12])
|
||||||
|
case "sm4-cbc":
|
||||||
|
cbcIv := iv
|
||||||
|
if len(cbcIv) == 12 {
|
||||||
|
cbcIv = append(cbcIv, 0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
return sm.NewSM4CBCWithoutEraseKey(key, cbcIv)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported algorithm: " + algo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateKeystore creates a new master key, obfuscates it, encrypts it with the master password, and saves it.
|
||||||
|
func CreateKeystore(name string, masterPassword []byte) (*KeySet, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New("key name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPath := GetKeystorePath()
|
||||||
|
if err := EnsureDir(keyPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(keyPath, name)
|
||||||
|
if file.Exists(filePath) {
|
||||||
|
return nil, errors.New("key exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := CryptoRandBytes(32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iv, err := CryptoRandBytes(12)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack to 128 bytes
|
||||||
|
memKeyBuf, err := PackMemoryKey(key, iv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the 128 bytes with master password
|
||||||
|
salt, _ := CryptoRandBytes(16)
|
||||||
|
fileIv, _ := CryptoRandBytes(12)
|
||||||
|
|
||||||
|
passCopy := append([]byte(nil), masterPassword...)
|
||||||
|
saltCopy := append([]byte(nil), salt...)
|
||||||
|
sym, err := crypto.NewAESGCMByPassword(passCopy, saltCopy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encData, err := sym.EncryptBytes(memKeyBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack file header
|
||||||
|
header, cipherPadding, err := PackFileHeader(salt, fileIv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct final file content: Header (256) + Padding + Ciphertext
|
||||||
|
paddingBuf, _ := CryptoRandBytes(cipherPadding)
|
||||||
|
|
||||||
|
finalBuf := append([]byte(nil), header...)
|
||||||
|
finalBuf = append(finalBuf, paddingBuf...)
|
||||||
|
finalBuf = append(finalBuf, encData...)
|
||||||
|
|
||||||
|
// Write raw binary
|
||||||
|
if err := file.WriteBytes(filePath, finalBuf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &KeySet{
|
||||||
|
Name: name,
|
||||||
|
buf: safe.NewSafeBufAndErase(memKeyBuf),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadKeystore loads and decrypts a master key from disk.
|
||||||
|
func LoadKeystore(name string, masterPassword []byte) (*KeySet, error) {
|
||||||
|
if name == "default" {
|
||||||
|
return GetDefaultKeySet(), nil
|
||||||
|
}
|
||||||
|
keyPath := GetKeystorePath()
|
||||||
|
filePath := filepath.Join(keyPath, name)
|
||||||
|
|
||||||
|
if !file.Exists(filePath) {
|
||||||
|
return nil, errors.New("key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := file.ReadBytes(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < FileHeaderSize {
|
||||||
|
return nil, errors.New("invalid key file format (too short)")
|
||||||
|
}
|
||||||
|
|
||||||
|
header := data[:FileHeaderSize]
|
||||||
|
salt, _, cipherPadding, err := UnpackFileHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid key file header")
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherStart := FileHeaderSize + cipherPadding
|
||||||
|
if len(data) <= cipherStart {
|
||||||
|
return nil, errors.New("invalid key file format (no ciphertext)")
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherText := data[cipherStart:]
|
||||||
|
|
||||||
|
passCopy := append([]byte(nil), masterPassword...)
|
||||||
|
sym, err := crypto.NewAESGCMByPassword(passCopy, salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dec, err := sym.DecryptBytes(cipherText)
|
||||||
|
if err != nil {
|
||||||
|
// --- 🛡️ 诱饵模式 (Decoy Mode) 🛡️ ---
|
||||||
|
// 密码错误不报错,而是返回一个随机生成的 KeySet。
|
||||||
|
// 这会让攻击者无法通过报错来判断暴力破解是否成功。
|
||||||
|
// 同时增加 1.5s 延迟,大幅降低自动化破解效率。
|
||||||
|
time.Sleep(1500 * time.Millisecond)
|
||||||
|
garbage, _ := CryptoRandBytes(MemoryKeySize)
|
||||||
|
return &KeySet{
|
||||||
|
Name: name,
|
||||||
|
buf: safe.NewSafeBufAndErase(garbage),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dec) != MemoryKeySize {
|
||||||
|
return nil, errors.New("corrupted key data (invalid length)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &KeySet{
|
||||||
|
Name: name,
|
||||||
|
buf: safe.NewSafeBufAndErase(dec),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMasterPassword checks if there are any keys in the keystore.
|
||||||
|
// If there are none, we might prompt the user to create a master password.
|
||||||
|
func HasMasterPassword() bool {
|
||||||
|
// In the new design, the master password is just a mental concept that decrypts individual files.
|
||||||
|
// If there are any files in the keystore, it implies the system is initialized.
|
||||||
|
keyPath := GetKeystorePath()
|
||||||
|
if !file.Exists(keyPath) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
files, _ := file.ReadDir(keyPath)
|
||||||
|
return len(files) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultKey = "?GQ$0K0GgLdO=f+~L68PLm$uhKr4'=tV"
|
||||||
|
DefaultIV = "VFs7@sK61cj^f?HZ"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDefaultKeySet returns a pre-defined keyset that doesn't require a password.
|
||||||
|
func GetDefaultKeySet() *KeySet {
|
||||||
|
key := []byte(DefaultKey)
|
||||||
|
iv := []byte(DefaultIV)
|
||||||
|
memKeyBuf, _ := PackMemoryKey(key, iv)
|
||||||
|
return &KeySet{
|
||||||
|
Name: "default",
|
||||||
|
buf: safe.NewSafeBufAndErase(memKeyBuf),
|
||||||
|
}
|
||||||
|
}
|
||||||
89
lib/keystore_test.go
Normal file
89
lib/keystore_test.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeystoreAndPassword(t *testing.T) {
|
||||||
|
// Setup a temporary KEYSPATH
|
||||||
|
tmpDir, err := os.MkdirTemp("", "keys_test_")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
os.Setenv("KEYSPATH", tmpDir)
|
||||||
|
defer os.Unsetenv("KEYSPATH")
|
||||||
|
|
||||||
|
masterPassword := []byte("super_secret_master_password")
|
||||||
|
keyName := "test_key"
|
||||||
|
|
||||||
|
// 1. Create Keystore
|
||||||
|
ks, err := CreateKeystore(keyName, masterPassword)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateKeystore failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key1, iv1, err := ks.GetRaw()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRaw failed: %v", err)
|
||||||
|
}
|
||||||
|
ks.Close()
|
||||||
|
|
||||||
|
// 2. Load Keystore
|
||||||
|
ks2, err := LoadKeystore(keyName, masterPassword)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadKeystore failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key2, iv2, err := ks2.GetRaw()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRaw on loaded failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(key1, key2) || !bytes.Equal(iv1, iv2) {
|
||||||
|
t.Fatalf("Loaded key/iv does not match created key/iv")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Save Password
|
||||||
|
pwdName := "db.mysql.root"
|
||||||
|
pwdValue := []byte("123456")
|
||||||
|
|
||||||
|
err = SavePassword(keyName, pwdName, pwdValue, key2, iv2, "aes-gcm")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SavePassword failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. List Passwords
|
||||||
|
pwds, err := ListPasswords(keyName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListPasswords failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(pwds) != 1 || pwds[0] != pwdName {
|
||||||
|
t.Fatalf("ListPasswords returned unexpected results: %v", pwds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Load Password
|
||||||
|
loadedPwd, err := LoadPassword(keyName, pwdName, key2, iv2, "aes-gcm")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadPassword failed: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(pwdValue, loadedPwd) {
|
||||||
|
t.Fatalf("Loaded password does not match. Got: %s, Want: %s", string(loadedPwd), string(pwdValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Delete Password
|
||||||
|
err = RemovePassword(keyName, pwdName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RemovePassword failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pwds2, _ := ListPasswords(keyName)
|
||||||
|
if len(pwds2) != 0 {
|
||||||
|
t.Fatalf("Password was not deleted properly")
|
||||||
|
}
|
||||||
|
|
||||||
|
ks2.Close()
|
||||||
|
}
|
||||||
184
lib/obfuscate.go
Normal file
184
lib/obfuscate.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MemoryKeySize = 128
|
||||||
|
FileHeaderSize = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
// CryptoRandBytes generates n cryptographically secure random bytes.
|
||||||
|
func CryptoRandBytes(n int) ([]byte, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackMemoryKey packs a 32-byte key and a up to 16-byte IV into a 128-byte obfuscated buffer.
|
||||||
|
func PackMemoryKey(key, iv []byte) ([]byte, error) {
|
||||||
|
if len(key) != 32 {
|
||||||
|
return nil, errors.New("key length must be 32 bytes")
|
||||||
|
}
|
||||||
|
if len(iv) < 12 || len(iv) > 16 {
|
||||||
|
return nil, errors.New("iv length must be between 12 and 16 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := CryptoRandBytes(MemoryKeySize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicator at the last byte (127)
|
||||||
|
indicator := buf[127]
|
||||||
|
var ptr1Index int
|
||||||
|
if indicator%2 == 0 {
|
||||||
|
ptr1Index = 126
|
||||||
|
} else {
|
||||||
|
ptr1Index = 125
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset_K is stored at ptr1Index.
|
||||||
|
// Need space for: Key (32) + Offset_IV (1) + IV (16) + IV_Len (1) = 50 bytes.
|
||||||
|
// Max offset is ptr1Index - 50. To be safe, constrain to [0, 64].
|
||||||
|
offsetK := int(buf[ptr1Index]) % 64
|
||||||
|
buf[ptr1Index] = byte(offsetK)
|
||||||
|
|
||||||
|
// Write Key
|
||||||
|
copy(buf[offsetK:offsetK+32], key)
|
||||||
|
|
||||||
|
// Offset_IV is stored at offsetK + 32
|
||||||
|
// Need space for IV (16). Max offset is ptr1Index - 16.
|
||||||
|
minOffsetIV := offsetK + 34 // Leave 2 bytes for offset and len
|
||||||
|
maxOffsetIV := 100 // Safe upper bound
|
||||||
|
rangeIV := maxOffsetIV - minOffsetIV + 1
|
||||||
|
|
||||||
|
offsetIVByte := buf[offsetK+32]
|
||||||
|
offsetIV := minOffsetIV + (int(offsetIVByte) % rangeIV)
|
||||||
|
buf[offsetK+32] = byte(offsetIV)
|
||||||
|
|
||||||
|
// IV Length is stored at offsetK + 33
|
||||||
|
buf[offsetK+33] = byte(len(iv))
|
||||||
|
|
||||||
|
// Write IV
|
||||||
|
copy(buf[offsetIV:offsetIV+len(iv)], iv)
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnpackMemoryKey unpacks a 128-byte obfuscated buffer into a 32-byte key and a 12-16 byte IV.
|
||||||
|
func UnpackMemoryKey(buf []byte) (key, iv []byte, err error) {
|
||||||
|
if len(buf) != MemoryKeySize {
|
||||||
|
return nil, nil, errors.New("invalid memory key size")
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator := buf[127]
|
||||||
|
var ptr1Index int
|
||||||
|
if indicator%2 == 0 {
|
||||||
|
ptr1Index = 126
|
||||||
|
} else {
|
||||||
|
ptr1Index = 125
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetK := int(buf[ptr1Index])
|
||||||
|
if offsetK+34 > ptr1Index {
|
||||||
|
return nil, nil, errors.New("invalid key offset")
|
||||||
|
}
|
||||||
|
|
||||||
|
key = make([]byte, 32)
|
||||||
|
copy(key, buf[offsetK:offsetK+32])
|
||||||
|
|
||||||
|
offsetIV := int(buf[offsetK+32])
|
||||||
|
ivLen := int(buf[offsetK+33])
|
||||||
|
if ivLen < 12 || ivLen > 16 || offsetIV+ivLen > ptr1Index {
|
||||||
|
return nil, nil, errors.New("invalid iv offset or length")
|
||||||
|
}
|
||||||
|
|
||||||
|
iv = make([]byte, ivLen)
|
||||||
|
copy(iv, buf[offsetIV:offsetIV+ivLen])
|
||||||
|
|
||||||
|
return key, iv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackFileHeader packs a 16-byte salt and a 12-byte iv into a 256-byte obfuscated header.
|
||||||
|
// It returns the header bytes and the number of random bytes that should be prepended to the ciphertext (cipherPadding).
|
||||||
|
func PackFileHeader(salt, iv []byte) (header []byte, cipherPadding int, err error) {
|
||||||
|
if len(salt) != 16 {
|
||||||
|
return nil, 0, errors.New("salt length must be 16 bytes")
|
||||||
|
}
|
||||||
|
if len(iv) != 12 {
|
||||||
|
return nil, 0, errors.New("iv length must be 12 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err = CryptoRandBytes(FileHeaderSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token is 31 bytes total: 16 salt + 12 iv + 3 padding
|
||||||
|
token := make([]byte, 31)
|
||||||
|
copy(token[0:16], salt)
|
||||||
|
copy(token[16:28], iv)
|
||||||
|
randPad, _ := CryptoRandBytes(3)
|
||||||
|
copy(token[28:31], randPad)
|
||||||
|
|
||||||
|
// Indicator at 255
|
||||||
|
indicator := header[255]
|
||||||
|
var ptr1Index int
|
||||||
|
if indicator%2 == 0 {
|
||||||
|
ptr1Index = 254
|
||||||
|
} else {
|
||||||
|
ptr1Index = 253
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset_Token stored at ptr1Index.
|
||||||
|
// Space needed for Token is 31 bytes.
|
||||||
|
// Constrain Offset_Token to [0, 200].
|
||||||
|
offsetToken := int(header[ptr1Index]) % 200
|
||||||
|
header[ptr1Index] = byte(offsetToken)
|
||||||
|
|
||||||
|
// Write Token
|
||||||
|
copy(header[offsetToken:offsetToken+31], token)
|
||||||
|
|
||||||
|
// CipherPadding stored at 252.
|
||||||
|
// This determines how many random bytes are after the header before the real ciphertext begins.
|
||||||
|
cipherPadding = int(header[252]) % 128
|
||||||
|
header[252] = byte(cipherPadding)
|
||||||
|
|
||||||
|
return header, cipherPadding, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnpackFileHeader unpacks a 256-byte header, extracting salt, iv, and the cipher padding length.
|
||||||
|
func UnpackFileHeader(header []byte) (salt, iv []byte, cipherPadding int, err error) {
|
||||||
|
if len(header) != FileHeaderSize {
|
||||||
|
return nil, nil, 0, errors.New("invalid file header size")
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator := header[255]
|
||||||
|
var ptr1Index int
|
||||||
|
if indicator%2 == 0 {
|
||||||
|
ptr1Index = 254
|
||||||
|
} else {
|
||||||
|
ptr1Index = 253
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetToken := int(header[ptr1Index])
|
||||||
|
if offsetToken+31 > ptr1Index {
|
||||||
|
return nil, nil, 0, errors.New("invalid token offset")
|
||||||
|
}
|
||||||
|
|
||||||
|
salt = make([]byte, 16)
|
||||||
|
copy(salt, header[offsetToken:offsetToken+16])
|
||||||
|
|
||||||
|
iv = make([]byte, 12)
|
||||||
|
copy(iv, header[offsetToken+16:offsetToken+28])
|
||||||
|
|
||||||
|
cipherPadding = int(header[252])
|
||||||
|
|
||||||
|
return salt, iv, cipherPadding, nil
|
||||||
|
}
|
||||||
64
lib/obfuscate_test.go
Normal file
64
lib/obfuscate_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPackUnpackMemoryKey(t *testing.T) {
|
||||||
|
key, _ := CryptoRandBytes(32)
|
||||||
|
iv, _ := CryptoRandBytes(12)
|
||||||
|
|
||||||
|
buf, err := PackMemoryKey(key, iv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PackMemoryKey failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) != MemoryKeySize {
|
||||||
|
t.Fatalf("PackMemoryKey length = %d, want %d", len(buf), MemoryKeySize)
|
||||||
|
}
|
||||||
|
|
||||||
|
unpackedKey, unpackedIv, err := UnpackMemoryKey(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UnpackMemoryKey failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(key, unpackedKey) {
|
||||||
|
t.Errorf("unpacked key != original key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(iv, unpackedIv) {
|
||||||
|
t.Errorf("unpacked iv != original iv")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackUnpackFileHeader(t *testing.T) {
|
||||||
|
salt, _ := CryptoRandBytes(16)
|
||||||
|
iv, _ := CryptoRandBytes(12)
|
||||||
|
|
||||||
|
header, padding, err := PackFileHeader(salt, iv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PackFileHeader failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(header) != FileHeaderSize {
|
||||||
|
t.Fatalf("PackFileHeader length = %d, want %d", len(header), FileHeaderSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
unpackedSalt, unpackedIv, unpackedPadding, err := UnpackFileHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UnpackFileHeader failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(salt, unpackedSalt) {
|
||||||
|
t.Errorf("unpacked salt != original salt")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(iv, unpackedIv) {
|
||||||
|
t.Errorf("unpacked iv != original iv")
|
||||||
|
}
|
||||||
|
|
||||||
|
if padding != unpackedPadding {
|
||||||
|
t.Errorf("unpacked padding = %d, want %d", unpackedPadding, padding)
|
||||||
|
}
|
||||||
|
}
|
||||||
184
lib/password.go
Normal file
184
lib/password.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"apigo.cc/go/crypto"
|
||||||
|
"apigo.cc/go/crypto-sm"
|
||||||
|
"apigo.cc/go/file"
|
||||||
|
"apigo.cc/go/safe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PwdNameToPath returns the name as is, allowing subdirectories via slashes.
|
||||||
|
func PwdNameToPath(pwdName string) string {
|
||||||
|
return pwdName
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePassword encrypts and saves a password value associated with a specific key.
|
||||||
|
func SavePassword(keyName, pwdName string, data []byte, activeKey, activeIV []byte, algo string) error {
|
||||||
|
if keyName == "" || pwdName == "" {
|
||||||
|
return errors.New("key name and password name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
basePath := GetPasswordPath(keyName)
|
||||||
|
relPath := PwdNameToPath(pwdName)
|
||||||
|
filePath := filepath.Join(basePath, relPath)
|
||||||
|
|
||||||
|
dir := filepath.Dir(filePath)
|
||||||
|
if err := EnsureDir(dir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileIv, err := CryptoRandBytes(12)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dummySalt, _ := CryptoRandBytes(16)
|
||||||
|
|
||||||
|
sym, err := createSymmetric(activeKey, activeIV, fileIv, algo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encData, err := sym.EncryptBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
header, cipherPadding, err := PackFileHeader(dummySalt, fileIv)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
paddingBuf, _ := CryptoRandBytes(cipherPadding)
|
||||||
|
finalBuf := append([]byte(nil), header...)
|
||||||
|
finalBuf = append(finalBuf, paddingBuf...)
|
||||||
|
finalBuf = append(finalBuf, encData...)
|
||||||
|
|
||||||
|
return file.WriteBytes(filePath, finalBuf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPassword loads and decrypts a password value.
|
||||||
|
func LoadPassword(keyName, pwdName string, activeKey, activeIV []byte, algo string) ([]byte, error) {
|
||||||
|
basePath := GetPasswordPath(keyName)
|
||||||
|
relPath := PwdNameToPath(pwdName)
|
||||||
|
filePath := filepath.Join(basePath, relPath)
|
||||||
|
|
||||||
|
if !file.Exists(filePath) {
|
||||||
|
return nil, errors.New("password not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := file.ReadBytes(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < FileHeaderSize {
|
||||||
|
return nil, errors.New("invalid password file format")
|
||||||
|
}
|
||||||
|
|
||||||
|
header := data[:FileHeaderSize]
|
||||||
|
_, fileIv, cipherPadding, err := UnpackFileHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid password file header")
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherStart := FileHeaderSize + cipherPadding
|
||||||
|
if len(data) <= cipherStart {
|
||||||
|
return nil, errors.New("invalid password file format (no ciphertext)")
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherText := data[cipherStart:]
|
||||||
|
|
||||||
|
sym, err := createSymmetric(activeKey, activeIV, fileIv, algo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dec, err := sym.DecryptBytes(cipherText)
|
||||||
|
if err != nil {
|
||||||
|
// --- 🛡️ 诱饵模式 (Decoy Mode) 🛡️ ---
|
||||||
|
// 解密失败不报错,而是返回一段随机的 12-24 位字符
|
||||||
|
// 让攻击者无法确定是算法选错了、Key错了,还是密码本身就是这样。
|
||||||
|
return CryptoRandDecoy(12, 24), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return dec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSymmetric(key, keyIv, fileIv []byte, algo string) (*crypto.Symmetric, error) {
|
||||||
|
k := append([]byte(nil), key...)
|
||||||
|
v := append([]byte(nil), fileIv...)
|
||||||
|
defer safe.ZeroMemory(k)
|
||||||
|
defer safe.ZeroMemory(v)
|
||||||
|
|
||||||
|
switch strings.ToLower(algo) {
|
||||||
|
case "aes-gcm", "gcm", "":
|
||||||
|
return crypto.NewAESGCMWithoutEraseKey(k, v)
|
||||||
|
case "aes-cbc", "cbc":
|
||||||
|
return crypto.NewAESCBCWithoutEraseKey(k, append(v, 0, 0, 0, 0))
|
||||||
|
case "sm4-gcm", "sm4":
|
||||||
|
return sm.NewSM4GCMWithoutEraseKey(k, v)
|
||||||
|
case "sm4-cbc":
|
||||||
|
return sm.NewSM4CBCWithoutEraseKey(k, append(v, 0, 0, 0, 0))
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported algorithm: " + algo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CryptoRandDecoy generates a random string for decoy purposes.
|
||||||
|
func CryptoRandDecoy(min, max int) []byte {
|
||||||
|
chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
|
||||||
|
n := min
|
||||||
|
if max > min {
|
||||||
|
rb, _ := CryptoRandBytes(1)
|
||||||
|
n = min + (int(rb[0]) % (max - min + 1))
|
||||||
|
}
|
||||||
|
res := make([]byte, n)
|
||||||
|
rb, _ := CryptoRandBytes(n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
res[i] = chars[int(rb[i])%len(chars)]
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePassword deletes a password file.
|
||||||
|
func RemovePassword(keyName, pwdName string) error {
|
||||||
|
basePath := GetPasswordPath(keyName)
|
||||||
|
relPath := PwdNameToPath(pwdName)
|
||||||
|
filePath := filepath.Join(basePath, relPath)
|
||||||
|
|
||||||
|
if !file.Exists(filePath) {
|
||||||
|
return errors.New("password not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Remove(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPasswords lists all passwords under a key, returning them as path names.
|
||||||
|
func ListPasswords(keyName string) ([]string, error) {
|
||||||
|
basePath := GetPasswordPath(keyName)
|
||||||
|
if !file.Exists(basePath) {
|
||||||
|
return nil, nil // No passwords yet
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwords []string
|
||||||
|
err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
rel, err := filepath.Rel(basePath, path)
|
||||||
|
if err == nil {
|
||||||
|
passwords = append(passwords, rel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return passwords, err
|
||||||
|
}
|
||||||
82
lib/path.go
Normal file
82
lib/path.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"apigo.cc/go/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetKeysRootPath returns the root path for all keys and passwords.
|
||||||
|
// Resolution order:
|
||||||
|
// 1. KEYSPATH environment variable
|
||||||
|
// 2. Path stored in ~/.keyspath
|
||||||
|
// 3. Default: ~/.keys
|
||||||
|
func GetKeysRootPath() string {
|
||||||
|
if p := os.Getenv("KEYSPATH"); p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
homeDir = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
keyspathFile := filepath.Join(homeDir, ".keyspath")
|
||||||
|
if file.Exists(keyspathFile) {
|
||||||
|
content, err := file.ReadBytes(keyspathFile)
|
||||||
|
if err == nil {
|
||||||
|
p := strings.TrimSpace(string(content))
|
||||||
|
if p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(homeDir, ".keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeystorePath returns the path for the keystore directory.
|
||||||
|
func GetKeystorePath() string {
|
||||||
|
return filepath.Join(GetKeysRootPath(), "keystore")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPasswordBasePath returns the base path for passwords.
|
||||||
|
func GetPasswordBasePath() string {
|
||||||
|
return filepath.Join(GetKeysRootPath(), "passwords")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPasswordPath returns the path for passwords associated with a specific key.
|
||||||
|
func GetPasswordPath(keyName string) string {
|
||||||
|
return filepath.Join(GetPasswordBasePath(), keyName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureDir ensures the specified directory exists.
|
||||||
|
func EnsureDir(path string) error {
|
||||||
|
return os.MkdirAll(path, 0700)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatus returns the root path, number of keystores, and total number of passwords.
|
||||||
|
func GetStatus() (rootPath string, keystoreCount int, passwordCount int) {
|
||||||
|
rootPath = GetKeysRootPath()
|
||||||
|
|
||||||
|
ksPath := GetKeystorePath()
|
||||||
|
if entries, err := os.ReadDir(ksPath); err == nil {
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() && !strings.HasPrefix(e.Name(), ".") {
|
||||||
|
keystoreCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pwPath := GetPasswordBasePath()
|
||||||
|
_ = filepath.Walk(pwPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err == nil && !info.IsDir() {
|
||||||
|
passwordCount++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
341
main.go
Normal file
341
main.go
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"apigo.cc/go/encoding"
|
||||||
|
"apigo.cc/go/keys/lib"
|
||||||
|
"apigo.cc/go/safe"
|
||||||
|
"apigo.cc/go/shell"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
interactiveKeystoreMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
arg1 := os.Args[1]
|
||||||
|
|
||||||
|
// 1. 特殊全局命令 (以 - 开头)
|
||||||
|
if strings.HasPrefix(arg1, "-") {
|
||||||
|
switch arg1 {
|
||||||
|
case "-h", "--help", "-help":
|
||||||
|
printUsage()
|
||||||
|
case "-setpath":
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Println(shell.Red("用法: keys -setpath <路径>"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
setPath(os.Args[2])
|
||||||
|
case "-status":
|
||||||
|
showStatus()
|
||||||
|
default:
|
||||||
|
fmt.Printf("%s: %s\n", shell.Red("未知选项"), arg1)
|
||||||
|
printUsage()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 密钥管理非交互指令
|
||||||
|
switch arg1 {
|
||||||
|
case "list":
|
||||||
|
handleListKeystores()
|
||||||
|
return
|
||||||
|
case "create":
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Println(shell.Red("用法: keys create <密钥名>"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
handleCreateKeystore(os.Args[2])
|
||||||
|
return
|
||||||
|
case "remove":
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Println(shell.Red("用法: keys remove <密钥名>"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
handleRemoveKeystore(os.Args[2])
|
||||||
|
return
|
||||||
|
case "export":
|
||||||
|
if len(os.Args) < 4 {
|
||||||
|
fmt.Println(shell.Red("用法: keys export <密钥名> <语言>"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
handleExportKeystore(os.Args[2], os.Args[3])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 密码管理指令 (keys <keyname> [cmd])
|
||||||
|
keyName := arg1
|
||||||
|
// 检查 keyName 是否存在于 keystore
|
||||||
|
if keyName != "default" {
|
||||||
|
ksPath := filepath.Join(lib.GetKeystorePath(), keyName)
|
||||||
|
if _, err := os.Stat(ksPath); err != nil {
|
||||||
|
fmt.Printf("%s: 密钥 '%s' 不存在。\n", shell.Red("错误"), keyName)
|
||||||
|
fmt.Println("使用 'keys list' 查看可用密钥。")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(os.Args) == 2 {
|
||||||
|
// 进入该密钥的交互式密码管理模式
|
||||||
|
interactivePasswordMode(keyName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非交互式密码管理
|
||||||
|
handlePasswordCmd(keyName, os.Args[2:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPath(newPath string) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("无法获取用户主目录: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(filepath.Join(homeDir, ".keyspath"), []byte(newPath), 0600)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("保存 .keyspath 失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(shell.Green("存储路径已更新为: " + newPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func showStatus() {
|
||||||
|
root, ksCount, pwCount := lib.GetStatus()
|
||||||
|
fmt.Print(shell.Cyan("Keys 系统状态:\n"))
|
||||||
|
fmt.Printf(" 存储根目录: %s\n", shell.White(root))
|
||||||
|
fmt.Printf(" 密钥总数: %d\n", ksCount)
|
||||||
|
fmt.Printf(" 密码总数: %d\n", pwCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleListKeystores() {
|
||||||
|
fmt.Println(shell.Cyan("可用密钥列表:"))
|
||||||
|
fmt.Printf(" - %s %s\n", shell.White("default"), shell.Yellow("(内置默认)"))
|
||||||
|
|
||||||
|
files, err := os.ReadDir(lib.GetKeystorePath())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("共 1 个密钥\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
count := 1
|
||||||
|
for _, fi := range files {
|
||||||
|
if !fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") {
|
||||||
|
fmt.Printf(" - %s\n", shell.White(fi.Name()))
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("共 %d 个密钥\n", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCreateKeystore(name string) {
|
||||||
|
pwd := getMasterPassword()
|
||||||
|
defer safe.ZeroMemory(pwd)
|
||||||
|
ks, err := lib.CreateKeystore(name, pwd)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("创建失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ks.Close()
|
||||||
|
fmt.Println(shell.Green("密钥 '" + name + "' 创建成功。"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRemoveKeystore(name string) {
|
||||||
|
path := filepath.Join(lib.GetKeystorePath(), name)
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
fmt.Println(shell.Red("删除失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(shell.Green("密钥 '" + name + "' 已删除。"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleExportKeystore(name, lang string) {
|
||||||
|
var pwd []byte
|
||||||
|
if name != "default" {
|
||||||
|
pwd = getMasterPassword()
|
||||||
|
defer safe.ZeroMemory(pwd)
|
||||||
|
}
|
||||||
|
ks, err := lib.LoadKeystore(name, pwd)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("加载失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer ks.Close()
|
||||||
|
key, iv, err := ks.GetRaw()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("提取失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
code, err := lib.MakeCode(lang, key, iv)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("导出失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePasswordCmd(keyName string, args []string) {
|
||||||
|
op := args[0]
|
||||||
|
|
||||||
|
// 下面这些操作通常需要解密 Key,所以统一获取 master password
|
||||||
|
var pwd []byte
|
||||||
|
if keyName != "default" {
|
||||||
|
pwd = getMasterPassword()
|
||||||
|
defer safe.ZeroMemory(pwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
ks, err := lib.LoadKeystore(keyName, pwd)
|
||||||
|
if err != nil {
|
||||||
|
// --- 🛡️ 诱饵模式: 就算 LoadKeystore 返回随机 Key,这里也会继续运行 ---
|
||||||
|
fmt.Println(shell.Red("密钥解锁失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer ks.Close()
|
||||||
|
|
||||||
|
key, iv, err := ks.GetRaw()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Key 提取失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
algo := "aes-gcm"
|
||||||
|
target := ""
|
||||||
|
var pwdVal []byte
|
||||||
|
|
||||||
|
// 提取参数中的算法和目标
|
||||||
|
for _, a := range args[1:] {
|
||||||
|
if strings.HasPrefix(a, "-") {
|
||||||
|
algo = a[1:]
|
||||||
|
} else {
|
||||||
|
if target == "" {
|
||||||
|
target = a
|
||||||
|
} else {
|
||||||
|
pwdVal = []byte(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case op == "list":
|
||||||
|
pwds, _ := lib.ListPasswords(keyName)
|
||||||
|
fmt.Printf("%s 保护的密码项:\n", shell.Cyan("'"+keyName+"'"))
|
||||||
|
for _, p := range pwds {
|
||||||
|
fmt.Println(" - " + p)
|
||||||
|
}
|
||||||
|
case op == "create":
|
||||||
|
if target == "" || len(pwdVal) == 0 {
|
||||||
|
fmt.Println(shell.Red("用法: keys <密钥名> create <路径> <值> [-算法]"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := lib.SavePassword(keyName, target, pwdVal, key, iv, algo); err != nil {
|
||||||
|
fmt.Println(shell.Red("保存失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s (算法: %s)。\n", shell.Green("保存成功"), algo)
|
||||||
|
case op == "remove":
|
||||||
|
if target == "" {
|
||||||
|
fmt.Println(shell.Red("用法: keys <密钥名> remove <路径>"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := lib.RemovePassword(keyName, target); err != nil {
|
||||||
|
fmt.Println(shell.Red("删除失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(shell.Green("删除成功。"))
|
||||||
|
case op == "encrypt":
|
||||||
|
if target == "" {
|
||||||
|
fmt.Println(shell.Red("用法: keys <密钥名> encrypt <路径|原文> [-算法]"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试作为已有项加载
|
||||||
|
data, err := lib.LoadPassword(keyName, target, key, iv, "aes-gcm")
|
||||||
|
if err != nil || len(data) == 0 {
|
||||||
|
data = []byte(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
sym, err := ks.NewSymmetric(algo)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("算法初始化失败: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
enc, _ := sym.EncryptBytes(data)
|
||||||
|
fmt.Println(encoding.UrlBase64ToString(enc))
|
||||||
|
case op == "decrypt":
|
||||||
|
if target == "" {
|
||||||
|
fmt.Println(shell.Red("用法: keys <密钥名> decrypt <路径|密文> [-算法]"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dec, err := lib.LoadPassword(keyName, target, key, iv, algo)
|
||||||
|
if err != nil || len(dec) == 0 {
|
||||||
|
// 尝试作为原始密文解密
|
||||||
|
rawData, err2 := encoding.UnUrlBase64FromString(target)
|
||||||
|
if err2 == nil {
|
||||||
|
sym, err3 := ks.NewSymmetric(algo)
|
||||||
|
if err3 == nil {
|
||||||
|
dec2, err4 := sym.DecryptBytes(rawData)
|
||||||
|
if err4 == nil {
|
||||||
|
fmt.Println(string(dec2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 🛡️ 诱饵模式: 完全失败打印随机字符
|
||||||
|
fmt.Println(string(lib.CryptoRandDecoy(12, 24)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(string(dec))
|
||||||
|
default:
|
||||||
|
fmt.Printf("%s: %s\n", shell.Red("未知密码指令"), op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMasterPassword() []byte {
|
||||||
|
if p := os.Getenv("MASTER_PASSWORD"); p != "" {
|
||||||
|
return []byte(p)
|
||||||
|
}
|
||||||
|
fmt.Print("输入主密码: ")
|
||||||
|
fd := int(os.Stdin.Fd())
|
||||||
|
password, err := term.ReadPassword(fd)
|
||||||
|
fmt.Println()
|
||||||
|
if err != nil || len(password) == 0 {
|
||||||
|
fmt.Println(shell.Red("必须输入密码"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Println(shell.Cyan("Keys CLI - 现代化本地 KMS 与密码管理器"))
|
||||||
|
fmt.Println(`
|
||||||
|
用法:
|
||||||
|
keys 启动交互式密钥管理 (Keystore Mode)
|
||||||
|
keys <密钥名> 启动该密钥对应的交互式密码管理 (Password Mode)
|
||||||
|
|
||||||
|
管理密钥 (非交互):
|
||||||
|
keys list 列出所有密钥
|
||||||
|
keys create <名> 创建新密钥
|
||||||
|
keys remove <名> 删除密钥
|
||||||
|
keys export <名> <语言> 导出混淆代码 (go, php, java, python, js)
|
||||||
|
|
||||||
|
管理密码 (非交互):
|
||||||
|
keys <密钥名> list 列出该密钥下的所有密码项
|
||||||
|
keys <密钥名> create <项> <值> [-算法] 创建/更新项 (gcm/cbc/sm4/sm4-cbc)
|
||||||
|
keys <密钥名> remove <项> 删除密码项
|
||||||
|
keys <密钥名> encrypt <项|原文> [-算法] 加密一段文本
|
||||||
|
keys <密钥名> decrypt <项|密文> [-算法] 解密并显示
|
||||||
|
|
||||||
|
系统指令:
|
||||||
|
keys -status 查看存储路径与统计状态
|
||||||
|
keys -setpath <路径> 重定向存储根目录 (持久化到 ~/.keyspath)
|
||||||
|
keys -h, --help 显示此帮助信息
|
||||||
|
|
||||||
|
环境变量:
|
||||||
|
KEYSPATH 临时指定存储路径
|
||||||
|
MASTER_PASSWORD 自动化脚本中传递主密码`)
|
||||||
|
}
|
||||||
565
repl.go
Normal file
565
repl.go
Normal file
@ -0,0 +1,565 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/c-bata/go-prompt"
|
||||||
|
"golang.org/x/term"
|
||||||
|
"apigo.cc/go/crypto"
|
||||||
|
"apigo.cc/go/crypto-sm"
|
||||||
|
"apigo.cc/go/encoding"
|
||||||
|
"apigo.cc/go/keys/lib"
|
||||||
|
"apigo.cc/go/safe"
|
||||||
|
"apigo.cc/go/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
var masterPwd []byte
|
||||||
|
|
||||||
|
func interactiveKeystoreMode() {
|
||||||
|
masterPwd = getMasterPassword()
|
||||||
|
|
||||||
|
fmt.Println(shell.Cyan("Keys CLI - Keystore Mode"))
|
||||||
|
fmt.Println(shell.White("Use Tab or Arrows for auto-completion. Type 'exit' to quit."))
|
||||||
|
|
||||||
|
p := prompt.New(
|
||||||
|
executorKeystore,
|
||||||
|
completerKeystore,
|
||||||
|
prompt.OptionPrefix("keys [keystore]> "),
|
||||||
|
prompt.OptionTitle("Keys CLI"),
|
||||||
|
prompt.OptionCompletionOnDown(),
|
||||||
|
)
|
||||||
|
p.Run()
|
||||||
|
safe.ZeroMemory(masterPwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executorKeystore(in string) {
|
||||||
|
in = strings.TrimSpace(in)
|
||||||
|
if in == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args := strings.Fields(in)
|
||||||
|
cmd := args[0]
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "exit", "quit":
|
||||||
|
safe.ZeroMemory(masterPwd)
|
||||||
|
os.Exit(0)
|
||||||
|
case "list":
|
||||||
|
handleListKeystores()
|
||||||
|
case "create":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println(shell.Red("Usage: create <keyname>"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ks, err := lib.CreateKeystore(args[1], masterPwd)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to create keystore: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ks.Close()
|
||||||
|
fmt.Println(shell.Green("Keystore created successfully."))
|
||||||
|
case "remove":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println(shell.Red("Usage: remove <keyname>"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filePath := filepath.Join(lib.GetKeystorePath(), args[1])
|
||||||
|
if err := os.Remove(filePath); err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to remove keystore: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(shell.Green("Keystore removed successfully."))
|
||||||
|
case "export":
|
||||||
|
if len(args) < 3 {
|
||||||
|
fmt.Println(shell.Red("Usage: export <keyname> <lang>"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var pwd []byte
|
||||||
|
if args[1] != "default" {
|
||||||
|
pwd = masterPwd
|
||||||
|
}
|
||||||
|
ks, err := lib.LoadKeystore(args[1], pwd)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to load keystore: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ks.Close()
|
||||||
|
|
||||||
|
key, iv, err := ks.GetRaw()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to extract raw key/iv: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := lib.MakeCode(args[2], key, iv)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to export code: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(code)
|
||||||
|
case "use":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println(shell.Red("Usage: use <keyname>"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyName := args[1]
|
||||||
|
var pwd []byte
|
||||||
|
if keyName != "default" {
|
||||||
|
pwd = masterPwd
|
||||||
|
}
|
||||||
|
ks, err := lib.LoadKeystore(keyName, pwd)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to load keystore: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key, iv, err := ks.GetRaw()
|
||||||
|
ks.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to extract raw key/iv: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
safe.ZeroMemory(masterPwd) // Erase master password from memory
|
||||||
|
startPasswordModeLoop(keyName, key, iv)
|
||||||
|
os.Exit(0) // Exit when password mode finishes
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Println(shell.Red("Unknown command. Try: list, create, remove, export, use, exit"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func completerKeystore(d prompt.Document) []prompt.Suggest {
|
||||||
|
text := d.TextBeforeCursor()
|
||||||
|
args := strings.Split(text, " ")
|
||||||
|
w := d.GetWordBeforeCursor()
|
||||||
|
|
||||||
|
if len(args) <= 1 {
|
||||||
|
s := []prompt.Suggest{
|
||||||
|
{Text: "list", Description: "列出所有密钥文件"},
|
||||||
|
{Text: "create", Description: "[密钥名] 创建新的主密钥"},
|
||||||
|
{Text: "remove", Description: "[密钥名] 永久删除指定密钥"},
|
||||||
|
{Text: "export", Description: "[密钥名] [语言] 导出应用集成混淆代码"},
|
||||||
|
{Text: "use", Description: "[密钥名] 解锁并进入该密钥的密码管理模式"},
|
||||||
|
{Text: "exit", Description: "安全退出"},
|
||||||
|
}
|
||||||
|
return prompt.FilterHasPrefix(s, w, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := args[0]
|
||||||
|
|
||||||
|
getKeystores := func() []prompt.Suggest {
|
||||||
|
keyPath := lib.GetKeystorePath()
|
||||||
|
files, _ := os.ReadDir(keyPath)
|
||||||
|
var ks []prompt.Suggest
|
||||||
|
ks = append(ks, prompt.Suggest{Text: "default", Description: "内置基础设施默认密钥 (无需密码)"})
|
||||||
|
for _, fi := range files {
|
||||||
|
if !fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") {
|
||||||
|
ks = append(ks, prompt.Suggest{Text: fi.Name(), Description: "密钥文件"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ks
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "remove", "use":
|
||||||
|
if len(args) == 2 {
|
||||||
|
suggestions := getKeystores()
|
||||||
|
if len(suggestions) == 0 {
|
||||||
|
return []prompt.Suggest{{Text: "", Description: "请先创建密钥"}}
|
||||||
|
}
|
||||||
|
return prompt.FilterHasPrefix(suggestions, w, true)
|
||||||
|
}
|
||||||
|
case "create":
|
||||||
|
if len(args) == 2 && w == "" {
|
||||||
|
return []prompt.Suggest{{Text: "", Description: "<请输入新密钥名称>"}}
|
||||||
|
}
|
||||||
|
case "export":
|
||||||
|
if len(args) == 2 {
|
||||||
|
return prompt.FilterHasPrefix(getKeystores(), w, true)
|
||||||
|
}
|
||||||
|
if len(args) == 3 {
|
||||||
|
s := []prompt.Suggest{
|
||||||
|
{Text: "go", Description: "导出 Go 混淆代码"},
|
||||||
|
{Text: "php", Description: "导出 PHP 混淆代码"},
|
||||||
|
{Text: "java", Description: "导出 Java 混淆代码"},
|
||||||
|
{Text: "python", Description: "导出 Python 混淆代码"},
|
||||||
|
{Text: "js", Description: "导出 JavaScript 混淆代码"},
|
||||||
|
}
|
||||||
|
return prompt.FilterHasPrefix(s, w, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []prompt.Suggest{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func interactivePasswordMode(keyName string) {
|
||||||
|
var pwd []byte
|
||||||
|
if keyName != "default" {
|
||||||
|
pwd = getMasterPassword()
|
||||||
|
}
|
||||||
|
ks, err := lib.LoadKeystore(keyName, pwd)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to load keystore: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
key, iv, err := ks.GetRaw()
|
||||||
|
ks.Close()
|
||||||
|
if len(pwd) > 0 {
|
||||||
|
safe.ZeroMemory(pwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to extract raw key/iv: " + err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
startPasswordModeLoop(keyName, key, iv)
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeKeyName string
|
||||||
|
var activeKey []byte
|
||||||
|
var activeIV []byte
|
||||||
|
|
||||||
|
func startPasswordModeLoop(keyName string, key, iv []byte) {
|
||||||
|
activeKeyName = keyName
|
||||||
|
activeKey = key
|
||||||
|
activeIV = iv
|
||||||
|
|
||||||
|
fmt.Println(shell.Cyan("Keys CLI - Password Mode for [" + keyName + "]"))
|
||||||
|
|
||||||
|
p := prompt.New(
|
||||||
|
executorPassword,
|
||||||
|
completerPassword,
|
||||||
|
prompt.OptionPrefix("keys ["+keyName+"]> "),
|
||||||
|
prompt.OptionTitle("Keys CLI - "+keyName),
|
||||||
|
prompt.OptionCompletionOnDown(),
|
||||||
|
)
|
||||||
|
p.Run()
|
||||||
|
|
||||||
|
safe.ZeroMemory(activeKey)
|
||||||
|
safe.ZeroMemory(activeIV)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executorPassword(in string) {
|
||||||
|
in = strings.TrimSpace(in)
|
||||||
|
if in == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args := strings.Fields(in)
|
||||||
|
cmd := args[0]
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "exit", "quit":
|
||||||
|
safe.ZeroMemory(activeKey)
|
||||||
|
safe.ZeroMemory(activeIV)
|
||||||
|
os.Exit(0)
|
||||||
|
case "list":
|
||||||
|
pwds, err := lib.ListPasswords(activeKeyName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to list passwords: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(pwds) == 0 {
|
||||||
|
fmt.Println(shell.Yellow("No passwords found."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, p := range pwds {
|
||||||
|
fmt.Println(" - " + shell.White(p))
|
||||||
|
}
|
||||||
|
case "create":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println(shell.Red("用法: create <路径> [-gcm|-cbc|-sm4|-sm4-cbc]"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pwdPath := args[1]
|
||||||
|
algo := "aes-gcm"
|
||||||
|
for _, a := range args[2:] {
|
||||||
|
if strings.HasPrefix(a, "-") {
|
||||||
|
algo = a[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Enter password value: ")
|
||||||
|
fd := int(os.Stdin.Fd())
|
||||||
|
pwdBytes, err := term.ReadPassword(fd)
|
||||||
|
fmt.Println()
|
||||||
|
if err != nil || len(pwdBytes) == 0 {
|
||||||
|
fmt.Println(shell.Red("Password value is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := lib.SavePassword(activeKeyName, pwdPath, pwdBytes, activeKey, activeIV, algo); err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to save password: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(shell.Green("Password saved successfully using " + algo))
|
||||||
|
case "remove":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println(shell.Red("Usage: remove <pwd.path.name>"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pwdName := args[1]
|
||||||
|
if err := lib.RemovePassword(activeKeyName, pwdName); err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to remove password: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(shell.Green("Password removed successfully."))
|
||||||
|
case "encrypt":
|
||||||
|
var rawText []byte
|
||||||
|
algo := "aes-gcm"
|
||||||
|
target := ""
|
||||||
|
|
||||||
|
for _, a := range args[1:] {
|
||||||
|
if strings.HasPrefix(a, "-") {
|
||||||
|
algo = a[1:]
|
||||||
|
} else {
|
||||||
|
target = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if target != "" {
|
||||||
|
// Encrypt an existing password item
|
||||||
|
dec, err := lib.LoadPassword(activeKeyName, target, activeKey, activeIV, "aes-gcm") // Assume stored is GCM
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Failed to load password: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawText = dec
|
||||||
|
} else {
|
||||||
|
// Encrypt custom text
|
||||||
|
fmt.Print("Enter text to encrypt: ")
|
||||||
|
fd := int(os.Stdin.Fd())
|
||||||
|
txtBytes, err := term.ReadPassword(fd)
|
||||||
|
fmt.Println()
|
||||||
|
if err != nil || len(txtBytes) == 0 {
|
||||||
|
fmt.Println(shell.Red("Text is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawText = txtBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
encData, err := encryptEphemeral(activeKey, activeIV, algo, rawText)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(shell.Red("Encryption failed: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(shell.Yellow(encoding.UrlBase64ToString(encData)))
|
||||||
|
|
||||||
|
case "decrypt":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println(shell.Red("用法: decrypt <路径|密文> [-算法]"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
target := ""
|
||||||
|
algo := "aes-gcm"
|
||||||
|
for _, a := range args[1:] {
|
||||||
|
if strings.HasPrefix(a, "-") {
|
||||||
|
algo = a[1:]
|
||||||
|
} else {
|
||||||
|
target = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dec, err := lib.LoadPassword(activeKeyName, target, activeKey, activeIV, algo)
|
||||||
|
if err != nil || len(dec) == 0 {
|
||||||
|
// Try as raw ciphertext
|
||||||
|
rawData, err2 := encoding.UnUrlBase64FromString(target)
|
||||||
|
if err2 == nil {
|
||||||
|
dec2, err3 := decryptEphemeral(activeKey, activeIV, algo, rawData)
|
||||||
|
if err3 == nil {
|
||||||
|
dec = dec2
|
||||||
|
} else {
|
||||||
|
// Silent failure: return random decoy
|
||||||
|
dec = lib.CryptoRandDecoy(12, 24)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println(shell.Red("Failed to decrypt: invalid path or Base64."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printSecretAndErase(string(dec), 10)
|
||||||
|
case "back":
|
||||||
|
// Return to keystore mode
|
||||||
|
safe.ZeroMemory(activeKey)
|
||||||
|
safe.ZeroMemory(activeIV)
|
||||||
|
fmt.Println(shell.Yellow("Returning to keystore mode..."))
|
||||||
|
interactiveKeystoreMode()
|
||||||
|
os.Exit(0)
|
||||||
|
default:
|
||||||
|
fmt.Println(shell.Red("Unknown command. Try: list, create, remove, encrypt, decrypt, back, exit"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func completerPassword(d prompt.Document) []prompt.Suggest {
|
||||||
|
text := d.TextBeforeCursor()
|
||||||
|
args := strings.Split(text, " ")
|
||||||
|
w := d.GetWordBeforeCursor()
|
||||||
|
|
||||||
|
if len(args) <= 1 {
|
||||||
|
s := []prompt.Suggest{
|
||||||
|
{Text: "list", Description: "列出该密钥下的所有密码项"},
|
||||||
|
{Text: "create", Description: "[路径] [-算法] 创建新密码项"},
|
||||||
|
{Text: "remove", Description: "[路径] 删除指定密码项"},
|
||||||
|
{Text: "encrypt", Description: "[路径|空] [-算法] 加密 (不回显)"},
|
||||||
|
{Text: "decrypt", Description: "[路径|密文] [-算法] 解密查看 (10s 阅后即焚)"},
|
||||||
|
{Text: "back", Description: "锁定当前密钥,返回密钥管理"},
|
||||||
|
{Text: "exit", Description: "安全退出"},
|
||||||
|
}
|
||||||
|
return prompt.FilterHasPrefix(s, w, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := args[0]
|
||||||
|
|
||||||
|
getPasswords := func() []prompt.Suggest {
|
||||||
|
pwds, _ := lib.ListPasswords(activeKeyName)
|
||||||
|
var ds []prompt.Suggest
|
||||||
|
for _, p := range pwds {
|
||||||
|
ds = append(ds, prompt.Suggest{Text: p, Description: "密码项"})
|
||||||
|
}
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
|
getAlgos := func() []prompt.Suggest {
|
||||||
|
return []prompt.Suggest{
|
||||||
|
{Text: "-gcm", Description: "AES-GCM (默认)"},
|
||||||
|
{Text: "-cbc", Description: "AES-CBC"},
|
||||||
|
{Text: "-sm4", Description: "SM4-GCM"},
|
||||||
|
{Text: "-sm4-cbc", Description: "SM4-CBC"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "remove", "decrypt", "encrypt":
|
||||||
|
if len(args) == 2 {
|
||||||
|
suggestions := getPasswords()
|
||||||
|
if strings.HasPrefix(w, "-") {
|
||||||
|
return prompt.FilterHasPrefix(getAlgos(), w, true)
|
||||||
|
}
|
||||||
|
return prompt.FilterHasPrefix(suggestions, w, true)
|
||||||
|
}
|
||||||
|
if len(args) >= 3 && strings.HasPrefix(w, "-") {
|
||||||
|
return prompt.FilterHasPrefix(getAlgos(), w, true)
|
||||||
|
}
|
||||||
|
case "create":
|
||||||
|
if len(args) == 2 {
|
||||||
|
if strings.HasPrefix(w, "-") {
|
||||||
|
return prompt.FilterHasPrefix(getAlgos(), w, true)
|
||||||
|
}
|
||||||
|
if w == "" {
|
||||||
|
return []prompt.Suggest{{Text: "", Description: "<请输入路径,如 mail/user@apigo.cc>"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(args) >= 3 && strings.HasPrefix(w, "-") {
|
||||||
|
return prompt.FilterHasPrefix(getAlgos(), w, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []prompt.Suggest{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptEphemeral(key, iv []byte, algo string, data []byte) ([]byte, error) {
|
||||||
|
var sym *crypto.Symmetric
|
||||||
|
var err error
|
||||||
|
|
||||||
|
k := append([]byte(nil), key...)
|
||||||
|
v := append([]byte(nil), iv...)
|
||||||
|
defer safe.ZeroMemory(k)
|
||||||
|
defer safe.ZeroMemory(v)
|
||||||
|
|
||||||
|
switch algo {
|
||||||
|
case "aes-gcm":
|
||||||
|
sym, err = crypto.NewAESGCMWithoutEraseKey(k, v)
|
||||||
|
case "aes-cbc":
|
||||||
|
sym, err = crypto.NewAESCBCWithoutEraseKey(k, append(v, 0, 0, 0, 0))
|
||||||
|
case "sm4-gcm":
|
||||||
|
sym, err = sm.NewSM4GCMWithoutEraseKey(k, v)
|
||||||
|
case "sm4-cbc":
|
||||||
|
sym, err = sm.NewSM4CBCWithoutEraseKey(k, append(v, 0, 0, 0, 0))
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown algorithm: %s", algo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sym.EncryptBytes(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptEphemeral(key, iv []byte, algo string, data []byte) ([]byte, error) {
|
||||||
|
var sym *crypto.Symmetric
|
||||||
|
var err error
|
||||||
|
|
||||||
|
k := append([]byte(nil), key...)
|
||||||
|
v := append([]byte(nil), iv...)
|
||||||
|
defer safe.ZeroMemory(k)
|
||||||
|
defer safe.ZeroMemory(v)
|
||||||
|
|
||||||
|
switch algo {
|
||||||
|
case "aes-gcm":
|
||||||
|
sym, err = crypto.NewAESGCMWithoutEraseKey(k, v)
|
||||||
|
case "aes-cbc":
|
||||||
|
sym, err = crypto.NewAESCBCWithoutEraseKey(k, append(v, 0, 0, 0, 0))
|
||||||
|
case "sm4-gcm":
|
||||||
|
sym, err = sm.NewSM4GCMWithoutEraseKey(k, v)
|
||||||
|
case "sm4-cbc":
|
||||||
|
sym, err = sm.NewSM4CBCWithoutEraseKey(k, append(v, 0, 0, 0, 0))
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown algorithm: %s", algo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sym.DecryptBytes(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSecretAndErase(secret string, seconds int) {
|
||||||
|
fmt.Printf("%s: %s\n", shell.Cyan("解密结果"), shell.Yellow(secret))
|
||||||
|
fmt.Printf("%s %s %s\n", shell.White("倒计时自动擦除,或按"), shell.Red("[ESC]"), shell.White("立即擦除"))
|
||||||
|
|
||||||
|
cancel := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer term.Restore(int(os.Stdin.Fd()), oldState)
|
||||||
|
|
||||||
|
b := make([]byte, 1)
|
||||||
|
for {
|
||||||
|
_, err := os.Stdin.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if b[0] == 27 { // ESC
|
||||||
|
cancel <- struct{}{}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for i := seconds; i > 0; i-- {
|
||||||
|
select {
|
||||||
|
case <-cancel:
|
||||||
|
break loop
|
||||||
|
default:
|
||||||
|
fmt.Printf("\r%s %s ", shell.White("阅后即焚倒计时:"), shell.Red(fmt.Sprintf("%ds", i)))
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("\r\033[2K")
|
||||||
|
fmt.Print("\033[1A\033[2K")
|
||||||
|
fmt.Print("\r\033[1A\033[2K")
|
||||||
|
fmt.Println(shell.Green("内容已安全擦除。"))
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user