Changeset 0.28.6 (#192)
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/crypto"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
@@ -334,3 +339,98 @@ func (h *GitCredentialHandler) Delete(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
// Generate creates a new ED25519 SSH keypair server-side.
|
||||
// The private key is vault-encrypted before storage. Only the public key
|
||||
// and fingerprint are returned — the private key never leaves the server.
|
||||
//
|
||||
// POST /git-credentials/generate
|
||||
func (h *GitCredentialHandler) Generate(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
PersonaID *string `json:"persona_id,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate ED25519 keypair
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "key generation failed"})
|
||||
return
|
||||
}
|
||||
|
||||
// Marshal public key to authorized_keys format
|
||||
sshPub, err := ssh.NewPublicKey(pubKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "public key encoding failed"})
|
||||
return
|
||||
}
|
||||
authorizedKey := string(ssh.MarshalAuthorizedKey(sshPub))
|
||||
|
||||
// Fingerprint (SHA256)
|
||||
fingerprint := ssh.FingerprintSHA256(sshPub)
|
||||
|
||||
// Marshal private key to OpenSSH PEM format
|
||||
privPEM, err := ssh.MarshalPrivateKey(privKey, "")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "private key encoding failed"})
|
||||
return
|
||||
}
|
||||
privPEMBytes := pem.EncodeToMemory(privPEM)
|
||||
|
||||
// Encrypt private key via vault
|
||||
credData := map[string]string{"private_key": string(privPEMBytes)}
|
||||
plaintext, _ := json.Marshal(credData)
|
||||
|
||||
ciphertext, nonce, err := h.vault.EncryptForScope(string(plaintext), "global", userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "encryption failed"})
|
||||
return
|
||||
}
|
||||
|
||||
cred := &models.GitCredential{
|
||||
UserID: userID,
|
||||
Name: req.Name,
|
||||
AuthType: "ssh_key",
|
||||
EncryptedData: ciphertext,
|
||||
Nonce: nonce,
|
||||
PublicKey: authorizedKey,
|
||||
Fingerprint: fingerprint,
|
||||
PersonaID: req.PersonaID,
|
||||
}
|
||||
|
||||
if err := h.stores.GitCredentials.Create(c.Request.Context(), cred); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, cred.Summary())
|
||||
}
|
||||
|
||||
// GetPublicKey returns the public key for a credential (for copy-to-clipboard).
|
||||
//
|
||||
// GET /git-credentials/:id/public-key
|
||||
func (h *GitCredentialHandler) GetPublicKey(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
credID := c.Param("id")
|
||||
|
||||
cred, err := h.stores.GitCredentials.GetByID(c.Request.Context(), credID)
|
||||
if err != nil || cred.UserID != userID {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "credential not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if cred.PublicKey == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "no public key for this credential type"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"public_key": cred.PublicKey,
|
||||
"fingerprint": cred.Fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user