client: Windows 客户端(WinUI3 壳 + agent-core;调试用 Electron 壳存档);localserver token 改为常数时间比较
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Identity 设备身份:Ed25519 密钥对 + 服务器分配的设备 ID
|
||||
type Identity struct {
|
||||
DeviceID uint64 `json:"deviceId"`
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
priv ed25519.PrivateKey `json:"-"`
|
||||
}
|
||||
|
||||
// LoadOrCreate 加载或生成本机设备身份(私钥 hex 文件 0600)
|
||||
func LoadOrCreate(dir string, name string) (*Identity, error) {
|
||||
keyPath := filepath.Join(dir, "device.key")
|
||||
idPath := filepath.Join(dir, "identity.json")
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var priv ed25519.PrivateKey
|
||||
if raw, err := os.ReadFile(keyPath); err == nil {
|
||||
seed, err := hex.DecodeString(string(raw))
|
||||
if err != nil || len(seed) != ed25519.SeedSize {
|
||||
return nil, errors.New("device key corrupted")
|
||||
}
|
||||
priv = ed25519.NewKeyFromSeed(seed)
|
||||
} else {
|
||||
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = os.WriteFile(keyPath, []byte(hex.EncodeToString(priv.Seed())), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
id := &Identity{
|
||||
Name: name,
|
||||
PublicKey: base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)),
|
||||
priv: priv,
|
||||
}
|
||||
if raw, err := os.ReadFile(idPath); err == nil {
|
||||
_ = json.Unmarshal(raw, id)
|
||||
id.priv = priv
|
||||
if id.Name == "" {
|
||||
id.Name = name
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Save 持久化设备 ID(配对成功后调用)
|
||||
func (i *Identity) Save(dir string) error {
|
||||
raw, err := json.MarshalIndent(i, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(dir, "identity.json"), raw, 0o600)
|
||||
}
|
||||
|
||||
// Sign 对载荷签名(hello 握手用)
|
||||
func (i *Identity) Sign(payload []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(ed25519.Sign(i.priv, payload))
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadOrCreatePersist(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
id1, err := LoadOrCreate(dir, "test-device")
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
// 再次加载应得到同一公钥
|
||||
id2, err := LoadOrCreate(dir, "test-device")
|
||||
if err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if id1.PublicKey != id2.PublicKey {
|
||||
t.Fatal("public key should persist across reloads")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignVerify(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
id, _ := LoadOrCreate(dir, "d")
|
||||
payload := []byte("nonce-1|123456")
|
||||
sigB64 := id.Sign(payload)
|
||||
sig, _ := base64.StdEncoding.DecodeString(sigB64)
|
||||
pub, _ := base64.StdEncoding.DecodeString(id.PublicKey)
|
||||
if !ed25519.Verify(ed25519.PublicKey(pub), payload, sig) {
|
||||
t.Fatal("signature should verify")
|
||||
}
|
||||
if ed25519.Verify(ed25519.PublicKey(pub), []byte("tampered"), sig) {
|
||||
t.Fatal("tampered payload should fail verify")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user