waveterm/pkg/wavejwt/wavejwt.go
Mike Sawka ae3e9f05b7
new job manager / framework for creating persistent remove sessions (#2779)
lots of stuff here.

introduces a streaming framework for the RPC system with flow control.
new authentication primitives for the RPC system. this is used to create
a persistent "job manager" process (via wsh) that can survive
disconnects. and then a jobcontroller in the main server that can
create, reconnect, and manage these new persistent jobs.

code is currently not actively hooked up to anything minus some new
debugging wsh commands, and a switch in the term block that lets me test
viewing the output.

after PRing this change the next steps are more testing and then
integrating this functionality into the product.
2026-01-21 16:54:18 -08:00

142 lines
3.2 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wavejwt
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
)
const (
IssuerWaveTerm = "waveterm"
)
var (
globalLock sync.Mutex
publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey
)
type WaveJwtClaims struct {
jwt.RegisteredClaims
MainServer bool `json:"mainserver,omitempty"`
Sock string `json:"sock,omitempty"`
RouteId string `json:"routeid,omitempty"`
BlockId string `json:"blockid,omitempty"`
JobId string `json:"jobid,omitempty"`
Conn string `json:"conn,omitempty"`
Router bool `json:"router,omitempty"`
}
type KeyPair struct {
PublicKey []byte
PrivateKey []byte
}
func GenerateKeyPair() (*KeyPair, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate key pair: %w", err)
}
return &KeyPair{
PublicKey: pubKey,
PrivateKey: privKey,
}, nil
}
func SetPublicKey(keyData []byte) error {
if len(keyData) != ed25519.PublicKeySize {
return fmt.Errorf("invalid public key size: expected %d, got %d", ed25519.PublicKeySize, len(keyData))
}
globalLock.Lock()
defer globalLock.Unlock()
publicKey = ed25519.PublicKey(keyData)
return nil
}
func GetPublicKey() []byte {
globalLock.Lock()
defer globalLock.Unlock()
return publicKey
}
func GetPublicKeyBase64() string {
pubKey := GetPublicKey()
if len(pubKey) == 0 {
return ""
}
return base64.StdEncoding.EncodeToString(pubKey)
}
func SetPrivateKey(keyData []byte) error {
if len(keyData) != ed25519.PrivateKeySize {
return fmt.Errorf("invalid private key size: expected %d, got %d", ed25519.PrivateKeySize, len(keyData))
}
globalLock.Lock()
defer globalLock.Unlock()
privateKey = ed25519.PrivateKey(keyData)
return nil
}
func ValidateAndExtract(tokenStr string) (*WaveJwtClaims, error) {
globalLock.Lock()
pubKey := publicKey
globalLock.Unlock()
if pubKey == nil {
return nil, fmt.Errorf("public key not set")
}
token, err := jwt.ParseWithClaims(tokenStr, &WaveJwtClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return pubKey, nil
})
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
claims, ok := token.Claims.(*WaveJwtClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return claims, nil
}
func Sign(claims *WaveJwtClaims) (string, error) {
globalLock.Lock()
privKey := privateKey
globalLock.Unlock()
if privKey == nil {
return "", fmt.Errorf("private key not set")
}
if claims.IssuedAt == nil {
claims.IssuedAt = jwt.NewNumericDate(time.Now())
}
if claims.Issuer == "" {
claims.Issuer = IssuerWaveTerm
}
if claims.ExpiresAt == nil {
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365))
}
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
tokenStr, err := token.SignedString(privKey)
if err != nil {
return "", fmt.Errorf("error signing token: %w", err)
}
return tokenStr, nil
}