package util import ( "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/json" "errors" "fmt" "strings" ) var ( ErrInvalidLicenseToken = errors.New("invalid license token") ErrInvalidLicenseSignature = errors.New("invalid license token signature") ) // LicensePayload is the verified content extracted from a signed license token. type LicensePayload struct { ShopID uint64 `json:"shop_id"` LicenseID uint64 `json:"license_id,omitempty"` Type string `json:"type"` // trial | monthly | annual | lifetime IssuedAt int64 `json:"issued_at"` ExpiresAt *int64 `json:"expires_at,omitempty"` // unix seconds; nil = perpetual MaxDevices int `json:"max_devices"` Features map[string]any `json:"features,omitempty"` } // GenerateEd25519KeyPair generates a new Ed25519 keypair. // Returns standard base64-encoded private key (64 bytes) and public key (32 bytes). // The private key must be stored securely (Bitwarden); the public key goes in config. func GenerateEd25519KeyPair() (privKeyB64, pubKeyB64 string, err error) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return "", "", err } return base64.StdEncoding.EncodeToString(priv), base64.StdEncoding.EncodeToString(pub), nil } // IssueLicenseToken signs a LicensePayload with the Ed25519 private key and returns // a compact token: base64url(header).base64url(payload).base64url(signature). // privKeyB64 is the standard base64-encoded 64-byte Ed25519 private key. func IssueLicenseToken(payload LicensePayload, privKeyB64 string) (string, error) { privKeyBytes, err := base64.StdEncoding.DecodeString(privKeyB64) if err != nil { return "", fmt.Errorf("decode private key: %w", err) } if len(privKeyBytes) != ed25519.PrivateKeySize { return "", fmt.Errorf("private key must be %d bytes, got %d", ed25519.PrivateKeySize, len(privKeyBytes)) } privKey := ed25519.PrivateKey(privKeyBytes) header := rawB64([]byte(`{"alg":"EdDSA","typ":"LIC"}`)) payloadJSON, err := json.Marshal(payload) if err != nil { return "", err } body := rawB64(payloadJSON) signingInput := header + "." + body sig := ed25519.Sign(privKey, []byte(signingInput)) return signingInput + "." + rawB64(sig), nil } // VerifyLicenseToken verifies the Ed25519 signature of a license token and returns // the decoded payload. Does NOT check expiry — callers must check ExpiresAt themselves. // pubKeyB64 is the standard base64-encoded 32-byte Ed25519 public key. func VerifyLicenseToken(token, pubKeyB64 string) (*LicensePayload, error) { parts := strings.Split(token, ".") if len(parts) != 3 { return nil, ErrInvalidLicenseToken } pubKeyBytes, err := base64.StdEncoding.DecodeString(pubKeyB64) if err != nil { return nil, fmt.Errorf("decode public key: %w", err) } if len(pubKeyBytes) != ed25519.PublicKeySize { return nil, fmt.Errorf("public key must be %d bytes, got %d", ed25519.PublicKeySize, len(pubKeyBytes)) } pubKey := ed25519.PublicKey(pubKeyBytes) signingInput := parts[0] + "." + parts[1] sigBytes, err := base64.RawURLEncoding.DecodeString(parts[2]) if err != nil { return nil, ErrInvalidLicenseToken } if !ed25519.Verify(pubKey, []byte(signingInput), sigBytes) { return nil, ErrInvalidLicenseSignature } payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { return nil, ErrInvalidLicenseToken } var p LicensePayload if err := json.Unmarshal(payloadJSON, &p); err != nil { return nil, ErrInvalidLicenseToken } return &p, nil } func rawB64(data []byte) string { return base64.RawURLEncoding.EncodeToString(data) }