cyi-box: Memphis-style toolbox admin platform

Go(fun)+SQLite backend, Vue 3 frontend. Modules: dashboard, pay gateway
(Epay-compatible + CDK stock), license/card system (per-app appid, domain
binding), AI memory bank with built-in MCP server (/mcp, FTS5 search,
signed read links), server SSH monitor + terminal + SFTP, proxy pool with
protocol-level alive checks, DB manager (mysql/pg/sqlite + SQL
autocomplete + import/export), disk share, vault (AES-GCM + TOTP),
bookmarks, dev tools.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-01 01:58:47 +08:00
co-authored by Devin
commit 834ecacdb9
326 changed files with 27813 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
// Package vault 密码箱原语:AES-256-GCM 字段加密 + RFC 6238 TOTP + otpauth:// 解析。
// 不引入第三方依赖,全部标准库实现。
package vault
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"encoding/base32"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"net/url"
"strings"
"time"
)
// DeriveKey 任意密钥材料规整为 32 字节 AES key(hex/base64 解码失败则取 sha256)
func DeriveKey(material string) []byte {
material = strings.TrimSpace(material)
if b, err := hex.DecodeString(material); err == nil && len(b) >= 16 {
sum := sha256.Sum256(b)
return sum[:]
}
if b, err := base64.StdEncoding.DecodeString(material); err == nil && len(b) >= 16 {
sum := sha256.Sum256(b)
return sum[:]
}
sum := sha256.Sum256([]byte(material))
return sum[:]
}
// RandomKey 生成 32 字节随机 key 的 hex 表示
func RandomKey() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
// Encrypt AES-256-GCM → base64(nonce|ct)
func Encrypt(key []byte, plaintext string) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return "", err
}
ct := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ct), nil
}
// Decrypt 解密 Encrypt 产物
func Decrypt(key []byte, encoded string) (string, error) {
raw, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
if len(raw) < gcm.NonceSize() {
return "", errors.New("vault: ciphertext too short")
}
pt, err := gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil)
if err != nil {
return "", errors.New("vault: decrypt failed")
}
return string(pt), nil
}
// ParseSecret 接受裸 base32 密钥或 otpauth://totp/...?secret=... 链接,统一返回 base32 密钥。
// 输入非法返回 error,服务层转成业务错误码。
func ParseSecret(input string) (string, error) {
s := strings.TrimSpace(input)
if s == "" {
return "", errors.New("empty secret")
}
if strings.HasPrefix(strings.ToLower(s), "otpauth://") {
u, err := url.Parse(s)
if err != nil {
return "", fmt.Errorf("vault: bad otpauth uri: %w", err)
}
secret := u.Query().Get("secret")
if secret == "" {
return "", errors.New("vault: otpauth uri missing secret")
}
s = secret
}
// 容错:去空格、横线,转大写;无 padding 形式也接受
clean := strings.NewReplacer(" ", "", "-", "").Replace(s)
clean = strings.ToUpper(clean)
if _, err := decodeBase32(clean); err != nil {
return "", fmt.Errorf("vault: invalid base32 secret: %w", err)
}
return clean, nil
}
func decodeBase32(s string) ([]byte, error) {
if b, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(s); err == nil {
return b, nil
}
return base32.StdEncoding.DecodeString(s)
}
// TOTP 计算当前验证码(30s 步长,6 位,SHA1),同时返回本码剩余秒数
func TOTP(secret string, t time.Time) (string, int64, error) {
key, err := decodeBase32(secret)
if err != nil {
return "", 0, fmt.Errorf("vault: invalid totp secret: %w", err)
}
counter := uint64(t.Unix() / 30)
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], counter)
mac := hmac.New(sha1.New, key)
mac.Write(buf[:])
sum := mac.Sum(nil)
offset := sum[len(sum)-1] & 0x0f
code := (uint32(sum[offset])&0x7f)<<24 |
(uint32(sum[offset+1]))<<16 |
(uint32(sum[offset+2]))<<8 |
uint32(sum[offset+3])
secondsLeft := 30 - (t.Unix() % 30)
return fmt.Sprintf("%06d", code%1000000), secondsLeft, nil
}