Files

86 lines
2.3 KiB
Go
Raw Permalink Normal View History

2026-08-29 21:56:51 +08:00
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"data-terminal-cloud/internal/deviceauth"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
if err := run(context.Background(), os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(ctx context.Context, args []string) error {
if len(args) != 2 || (args[0] != "provision" && args[0] != "rotate" && args[0] != "revoke") {
return errors.New("usage: device-credential <provision|rotate|revoke> <device-public-uuid>")
}
if _, err := uuid.Parse(args[1]); err != nil {
return errors.New("device public id must be a UUID")
}
databaseURL, err := secretEnvironment("DATABASE_URL")
if err != nil {
return err
}
masterKey, err := secretEnvironment("DEVICE_CREDENTIAL_MASTER_KEY")
if err != nil {
return err
}
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return fmt.Errorf("open database: %w", err)
}
defer pool.Close()
resolver, err := deviceauth.NewDatabaseSecretResolver(pool, masterKey)
if err != nil {
return err
}
if args[0] == "revoke" {
if err := resolver.Revoke(ctx, args[1]); err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(map[string]any{"device_id": args[1], "status": "revoked"})
}
random := make([]byte, 32)
if _, err := rand.Read(random); err != nil {
return fmt.Errorf("generate device credential: %w", err)
}
secret := hex.EncodeToString(random)
version, err := resolver.Provision(ctx, args[1], []byte(secret), args[0] == "rotate")
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "device credential plaintext is shown once; transfer it through a controlled channel")
return json.NewEncoder(os.Stdout).Encode(map[string]any{
"device_id": args[1], "key_version": version, "secret": secret,
})
}
func secretEnvironment(name string) (string, error) {
if file := strings.TrimSpace(os.Getenv(name + "_FILE")); file != "" {
value, err := os.ReadFile(file)
if err != nil {
return "", fmt.Errorf("read %s_FILE: %w", name, err)
}
if result := strings.TrimSpace(string(value)); result != "" {
return result, nil
}
}
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value, nil
}
return "", fmt.Errorf("%s or %s_FILE is required", name, name)
}