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 ") } 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) }