212 lines
6.5 KiB
Go
212 lines
6.5 KiB
Go
package acp
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"xworkmate-bridge/internal/gatewayruntime"
|
|
)
|
|
|
|
var bridgeGatewayIdentity = struct {
|
|
sync.Mutex
|
|
value gatewayruntime.DeviceIdentity
|
|
deviceToken string
|
|
}{}
|
|
|
|
type storedBridgeGatewayIdentity struct {
|
|
Version int `json:"version"`
|
|
DeviceID string `json:"deviceId"`
|
|
PublicKeyBase64URL string `json:"publicKeyBase64Url"`
|
|
PrivateKeyBase64URL string `json:"privateKeyBase64Url"`
|
|
CreatedAtMs int64 `json:"createdAtMs"`
|
|
DeviceToken string `json:"deviceToken,omitempty"`
|
|
}
|
|
|
|
func newBridgeGatewayIdentity() gatewayruntime.DeviceIdentity {
|
|
identity, _ := bridgeGatewayOpenClawCredentials()
|
|
return identity
|
|
}
|
|
|
|
func bridgeGatewayOpenClawCredentials() (gatewayruntime.DeviceIdentity, string) {
|
|
bridgeGatewayIdentity.Lock()
|
|
defer bridgeGatewayIdentity.Unlock()
|
|
if strings.TrimSpace(bridgeGatewayIdentity.value.DeviceID) != "" {
|
|
return bridgeGatewayIdentity.value, bridgeGatewayIdentity.deviceToken
|
|
}
|
|
identity, deviceToken, ok := loadBridgeGatewayIdentity()
|
|
if !ok {
|
|
identity = generateBridgeGatewayIdentity()
|
|
deviceToken = ""
|
|
}
|
|
bridgeGatewayIdentity.value = identity
|
|
bridgeGatewayIdentity.deviceToken = deviceToken
|
|
return bridgeGatewayIdentity.value, bridgeGatewayIdentity.deviceToken
|
|
}
|
|
|
|
func generateBridgeGatewayIdentity() gatewayruntime.DeviceIdentity {
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return gatewayruntime.DeviceIdentity{}
|
|
}
|
|
return gatewayruntime.DeviceIdentity{
|
|
DeviceID: deriveOpenClawDeviceID(publicKey),
|
|
PublicKeyBase64URL: base64.RawURLEncoding.EncodeToString(publicKey),
|
|
PrivateKeyBase64URL: base64.RawURLEncoding.EncodeToString(privateKey),
|
|
}
|
|
}
|
|
|
|
func loadBridgeGatewayIdentity() (gatewayruntime.DeviceIdentity, string, bool) {
|
|
path := bridgeGatewayIdentityPath()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
identity := generateBridgeGatewayIdentity()
|
|
return identity, "", persistBridgeGatewayIdentity(path, identity, "")
|
|
}
|
|
var stored storedBridgeGatewayIdentity
|
|
if err := json.Unmarshal(data, &stored); err != nil {
|
|
identity := generateBridgeGatewayIdentity()
|
|
return identity, "", persistBridgeGatewayIdentity(path, identity, "")
|
|
}
|
|
identity := gatewayruntime.DeviceIdentity{
|
|
DeviceID: strings.TrimSpace(stored.DeviceID),
|
|
PublicKeyBase64URL: strings.TrimSpace(stored.PublicKeyBase64URL),
|
|
PrivateKeyBase64URL: strings.TrimSpace(stored.PrivateKeyBase64URL),
|
|
}
|
|
if normalized, ok := normalizeBridgeGatewayIdentity(identity); ok {
|
|
if normalized.DeviceID != identity.DeviceID {
|
|
_ = persistBridgeGatewayIdentity(path, normalized, strings.TrimSpace(stored.DeviceToken))
|
|
}
|
|
return normalized, strings.TrimSpace(stored.DeviceToken), true
|
|
}
|
|
identity = generateBridgeGatewayIdentity()
|
|
return identity, "", persistBridgeGatewayIdentity(path, identity, "")
|
|
}
|
|
|
|
func normalizeBridgeGatewayIdentity(identity gatewayruntime.DeviceIdentity) (gatewayruntime.DeviceIdentity, bool) {
|
|
publicKey, err := decodeRawBase64URL(identity.PublicKeyBase64URL)
|
|
if err != nil || len(publicKey) != ed25519.PublicKeySize {
|
|
return gatewayruntime.DeviceIdentity{}, false
|
|
}
|
|
privateKey, err := decodeRawBase64URL(identity.PrivateKeyBase64URL)
|
|
if err != nil {
|
|
return gatewayruntime.DeviceIdentity{}, false
|
|
}
|
|
var edPrivateKey ed25519.PrivateKey
|
|
switch len(privateKey) {
|
|
case ed25519.PrivateKeySize:
|
|
edPrivateKey = ed25519.PrivateKey(privateKey)
|
|
case ed25519.SeedSize:
|
|
edPrivateKey = ed25519.NewKeyFromSeed(privateKey)
|
|
default:
|
|
return gatewayruntime.DeviceIdentity{}, false
|
|
}
|
|
derivedPublic, ok := edPrivateKey.Public().(ed25519.PublicKey)
|
|
if !ok || subtle.ConstantTimeCompare(derivedPublic, publicKey) != 1 {
|
|
return gatewayruntime.DeviceIdentity{}, false
|
|
}
|
|
identity.DeviceID = deriveOpenClawDeviceID(publicKey)
|
|
identity.PublicKeyBase64URL = base64.RawURLEncoding.EncodeToString(publicKey)
|
|
identity.PrivateKeyBase64URL = base64.RawURLEncoding.EncodeToString(edPrivateKey)
|
|
return identity, true
|
|
}
|
|
|
|
func saveBridgeGatewayDeviceToken(deviceToken string) {
|
|
deviceToken = strings.TrimSpace(deviceToken)
|
|
if deviceToken == "" {
|
|
return
|
|
}
|
|
bridgeGatewayIdentity.Lock()
|
|
defer bridgeGatewayIdentity.Unlock()
|
|
if strings.TrimSpace(bridgeGatewayIdentity.value.DeviceID) == "" {
|
|
return
|
|
}
|
|
bridgeGatewayIdentity.deviceToken = deviceToken
|
|
_ = persistBridgeGatewayIdentity(
|
|
bridgeGatewayIdentityPath(),
|
|
bridgeGatewayIdentity.value,
|
|
deviceToken,
|
|
)
|
|
}
|
|
|
|
func clearBridgeGatewayDeviceToken() {
|
|
bridgeGatewayIdentity.Lock()
|
|
defer bridgeGatewayIdentity.Unlock()
|
|
if strings.TrimSpace(bridgeGatewayIdentity.value.DeviceID) == "" {
|
|
return
|
|
}
|
|
bridgeGatewayIdentity.deviceToken = ""
|
|
_ = persistBridgeGatewayIdentity(
|
|
bridgeGatewayIdentityPath(),
|
|
bridgeGatewayIdentity.value,
|
|
"",
|
|
)
|
|
}
|
|
|
|
func persistBridgeGatewayIdentity(
|
|
path string,
|
|
identity gatewayruntime.DeviceIdentity,
|
|
deviceToken string,
|
|
) bool {
|
|
if strings.TrimSpace(identity.DeviceID) == "" ||
|
|
strings.TrimSpace(identity.PublicKeyBase64URL) == "" ||
|
|
strings.TrimSpace(identity.PrivateKeyBase64URL) == "" {
|
|
return false
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return false
|
|
}
|
|
_ = os.Chmod(filepath.Dir(path), 0o700)
|
|
payload := storedBridgeGatewayIdentity{
|
|
Version: 1,
|
|
DeviceID: identity.DeviceID,
|
|
PublicKeyBase64URL: identity.PublicKeyBase64URL,
|
|
PrivateKeyBase64URL: identity.PrivateKeyBase64URL,
|
|
CreatedAtMs: time.Now().UnixMilli(),
|
|
DeviceToken: strings.TrimSpace(deviceToken),
|
|
}
|
|
data, err := json.MarshalIndent(payload, "", " ")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
data = append(data, '\n')
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
return false
|
|
}
|
|
_ = os.Chmod(path, 0o600)
|
|
return true
|
|
}
|
|
|
|
func bridgeGatewayIdentityPath() string {
|
|
if path := strings.TrimSpace(os.Getenv("XWORKMATE_BRIDGE_OPENCLAW_IDENTITY_PATH")); path != "" {
|
|
return path
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil || strings.TrimSpace(home) == "" {
|
|
home = "."
|
|
}
|
|
return filepath.Join(home, ".xworkmate-bridge", "openclaw-device.json")
|
|
}
|
|
|
|
func deriveOpenClawDeviceID(publicKey ed25519.PublicKey) string {
|
|
sum := sha256.Sum256(publicKey)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func isOpenClawMode(mode string) bool {
|
|
return strings.TrimSpace(strings.ToLower(mode)) == "openclaw"
|
|
}
|
|
|
|
func decodeRawBase64URL(value string) ([]byte, error) {
|
|
return base64.RawURLEncoding.DecodeString(strings.TrimSpace(value))
|
|
}
|