This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user