86 lines
2.3 KiB
Go
86 lines
2.3 KiB
Go
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)
|
|
}
|