566 lines
14 KiB
Go
566 lines
14 KiB
Go
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("内容已安全擦除。"))
|
|
}
|