270 lines
5.5 KiB
Go
270 lines
5.5 KiB
Go
// Copyright (C) 2023 Marius Schellenberger
|
|
|
|
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"git.giftfish.de/ston1th/godrop/v2"
|
|
apiclient "git.giftfish.de/ston1th/keyctl/pkg/api/client"
|
|
clientv1 "git.giftfish.de/ston1th/keyctl/pkg/api/v1/client"
|
|
"git.giftfish.de/ston1th/keyctl/pkg/api/v1/schema"
|
|
"git.giftfish.de/ston1th/keyctl/pkg/core"
|
|
)
|
|
|
|
var fs = flag.NewFlagSet("", flag.ExitOnError)
|
|
|
|
func exit(err error) {
|
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func Run(arg string) {
|
|
ctx := context.Background()
|
|
pledge := "stdio unveil"
|
|
switch arg {
|
|
case "new", "ls", "req", "approve", "reject", "del":
|
|
pledge += " unix"
|
|
case "get":
|
|
pledge += " unix inet dns"
|
|
}
|
|
err := godrop.PledgePromises(pledge)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
switch arg {
|
|
case "new":
|
|
name, existing, encoding, t, size, err := newKeyFlags()
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
newKey := schema.NewKey{
|
|
Name: name,
|
|
Existing: existing,
|
|
Size: size,
|
|
Encoding: core.EncodingFromString(encoding),
|
|
Type: core.TypeFromString(t),
|
|
}
|
|
c := client()
|
|
k, err := c.CreateKey(ctx, newKey)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
fmt.Println("Key ID:", k.ID)
|
|
if k.Type == core.Shamir || k.Type == core.Xor {
|
|
fmt.Println("Key Type:", k.Type)
|
|
fmt.Println("Key Share:", k.Key)
|
|
}
|
|
case "del":
|
|
id, force, err := delFlags("key id")
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
if !force && !confirm(os.Stdin) {
|
|
exit(errors.New("confirmation failed: secret not deleted"))
|
|
}
|
|
c := client()
|
|
err = c.DeleteKey(ctx, id)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
case "approve":
|
|
id, err := idFlags("approval id")
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
c := client()
|
|
err = c.AcceptApproval(ctx, id)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
case "reject":
|
|
id, err := idFlags("approval id")
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
c := client()
|
|
err = c.RejectApproval(ctx, id)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
case "get":
|
|
id, share, loop, json, newline, err := getFlags()
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
c := client()
|
|
var key schema.Key
|
|
if loop {
|
|
key, err = c.GetKeyLoop(ctx, id)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
} else {
|
|
key, err = c.GetKey(ctx, id)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
}
|
|
out, err := key.Print(share)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
if json {
|
|
key.Key = out
|
|
jsonOutput(key) // does not return
|
|
}
|
|
if newline {
|
|
fmt.Println(out)
|
|
} else {
|
|
fmt.Print(out)
|
|
}
|
|
case "ls":
|
|
json := outputFlags()
|
|
c := client()
|
|
keys, err := c.GetKeys(ctx)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
if json {
|
|
jsonOutput(keys) // does not return
|
|
}
|
|
print(keys)
|
|
case "req":
|
|
json := outputFlags()
|
|
c := client()
|
|
approvals, err := c.GetApprovals(ctx)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
if json {
|
|
jsonOutput(approvals) // does not return
|
|
}
|
|
print(approvals)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
func client() (c *clientv1.Client) {
|
|
opts, err := apiclient.ClientOptionsFromEnv()
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
apic := apiclient.NewClient(opts...)
|
|
if socket, ok := apic.IsUnix(); ok {
|
|
err = unveil(socket)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
}
|
|
c, err = clientv1.NewClient(apic)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func unveil(socket string) error {
|
|
err := godrop.Unveil(socket, "rwc")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return godrop.UnveilBlock()
|
|
}
|
|
|
|
func jsonOutput(v any) {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
exit(err)
|
|
}
|
|
fmt.Println(string(b))
|
|
os.Exit(0)
|
|
}
|
|
|
|
func idFlags(desc string) (id string, err error) {
|
|
fs.StringVar(&id, "id", "", desc)
|
|
fs.Parse(os.Args[2:])
|
|
if id == "" {
|
|
err = errors.New("missing flag: id")
|
|
}
|
|
return
|
|
}
|
|
|
|
func delFlags(desc string) (id string, force bool, err error) {
|
|
fs.StringVar(&id, "id", "", desc)
|
|
fs.BoolVar(&force, "force", false, "do not ask for confirmation")
|
|
fs.Parse(os.Args[2:])
|
|
if id == "" {
|
|
err = errors.New("missing flag: id")
|
|
}
|
|
return
|
|
}
|
|
|
|
func getFlags() (id, share string, loop, json, newline bool, err error) {
|
|
fs.StringVar(&id, "id", "", "key id")
|
|
fs.StringVar(&share, "share", "", "shamir key share")
|
|
fs.BoolVar(&loop, "loop", false, "ignore network errors and loop until key is received")
|
|
fs.BoolVar(&json, "json", false, "json output")
|
|
fs.BoolVar(&newline, "newline", true, "output newline after secret")
|
|
fs.Parse(os.Args[2:])
|
|
if id == "" {
|
|
err = errors.New("missing flag: id")
|
|
}
|
|
if share == "" {
|
|
share = os.Getenv("KEYCTL_SHARE")
|
|
}
|
|
return
|
|
}
|
|
|
|
func outputFlags() (json bool) {
|
|
fs.BoolVar(&json, "json", false, "json output")
|
|
fs.Parse(os.Args[2:])
|
|
return
|
|
}
|
|
|
|
func newKeyFlags() (name, existing, encoding, t string, size int, err error) {
|
|
fs.StringVar(&name, "name", "", "key name")
|
|
fs.StringVar(&existing, "key", "", "use existing secret key (optional; input-format: hex; types: plain, shamir)")
|
|
fs.StringVar(&encoding, "enc", "hex", "key encoding scheme [hex|base32|base64|base64url]")
|
|
fs.StringVar(&t, "type", "plain", "key type [plain|shamir|xor]")
|
|
fs.IntVar(&size, "size", 16, "key size in bytes")
|
|
fs.Parse(os.Args[2:])
|
|
if name == "" {
|
|
err = errors.New("missing flag: name")
|
|
}
|
|
return
|
|
}
|
|
|
|
func print(tw core.TableWriter) {
|
|
if tw.Len() == 0 {
|
|
return
|
|
}
|
|
w := tabwriter.NewWriter(os.Stdout, 1, 0, 1, ' ', tabwriter.Debug)
|
|
fmt.Fprintln(w, tw.Fields())
|
|
for _, v := range tw.Values() {
|
|
fmt.Fprintln(w, v)
|
|
}
|
|
w.Flush()
|
|
}
|
|
|
|
func confirm(r io.Reader) (ok bool) {
|
|
br := bufio.NewReader(r)
|
|
fmt.Printf("confirm delete [y]: ")
|
|
res, err := br.ReadString('\n')
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(res) < 2 {
|
|
return
|
|
}
|
|
return strings.TrimSpace(res)[0] == 'y'
|
|
}
|