diff --git a/pkg/api/v1/schema/schema.go b/pkg/api/v1/schema/schema.go index 9a0d0ca..b20041a 100644 --- a/pkg/api/v1/schema/schema.go +++ b/pkg/api/v1/schema/schema.go @@ -22,15 +22,18 @@ const ( type NewKey struct { Name string `json:"name"` + Existing string `json:"existing,omitempty"` Size int `json:"size"` Encoding core.Encoding `json:"encoding"` Type core.Type `json:"type"` } var ( - ErrEmptyKeyName = errors.New("empty key name") - ErrInvalidKeyName = errors.New("invalid key name") - ErrInvalidKeySize = errors.New("invalid key size") + ErrEmptyKeyName = errors.New("empty key name") + ErrExistingKeyType = errors.New("invalid key type for existing key") + ErrExistingKeyEncoding = errors.New("invalid key encoding for existing key") + ErrInvalidKeyName = errors.New("invalid key name") + ErrInvalidKeySize = errors.New("invalid key size") ) const ( @@ -42,6 +45,15 @@ func (nk NewKey) Validate() error { if nk.Name == "" { return ErrEmptyKeyName } + if nk.Existing != "" { + if nk.Type == core.Xor { + return ErrExistingKeyType + } + _, err := hex.DecodeString(nk.Existing) + if err != nil { + return ErrExistingKeyEncoding + } + } if core.NameRe.MatchString(nk.Name) { return ErrInvalidKeyName } diff --git a/pkg/api/v1/server/handler.go b/pkg/api/v1/server/handler.go index 672b8cf..d27f634 100644 --- a/pkg/api/v1/server/handler.go +++ b/pkg/api/v1/server/handler.go @@ -43,7 +43,7 @@ func newHandler(ctx *types.Context) { ctx.Err(types.ErrInvalid) return } - key, err := ctx.Data.DB.CreateKey(newKey.Name, newKey.Size, newKey.Encoding, newKey.Type) + key, err := ctx.Data.DB.CreateKey(newKey.Name, newKey.Existing, newKey.Size, newKey.Encoding, newKey.Type) if err != nil { ctx.Log.Error(err, "error writing key", "name", newKey.Name) ctx.Err(types.ErrISE) diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index f520441..f52a408 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -3,12 +3,15 @@ package cli import ( + "bufio" "context" "encoding/json" "errors" "flag" "fmt" + "io" "os" + "strings" "text/tabwriter" apiclient "git.giftfish.de/ston1th/keyctl/pkg/api/client" @@ -28,12 +31,13 @@ func Run(arg string) { ctx := context.Background() switch arg { case "new": - name, encoding, t, size, err := newKeyFlags() + 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), @@ -49,10 +53,13 @@ func Run(arg string) { fmt.Println("Key Share:", k.Key) } case "del": - id, err := idFlags("key id") + 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 { @@ -79,7 +86,7 @@ func Run(arg string) { exit(err) } case "get": - id, share, loop, json, err := getFlags() + id, share, loop, json, newline, err := getFlags() if err != nil { exit(err) } @@ -104,7 +111,11 @@ func Run(arg string) { key.Key = out jsonOutput(key) // does not return } - fmt.Println(out) + if newline { + fmt.Println(out) + } else { + fmt.Print(out) + } case "ls": json := outputFlags() c := client() @@ -162,11 +173,22 @@ func idFlags(desc string) (id string, err error) { return } -func getFlags() (id, share string, loop, json bool, err error) { +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") @@ -183,8 +205,9 @@ func outputFlags() (json bool) { return } -func newKeyFlags() (name, encoding, t string, size int, err error) { +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") @@ -206,3 +229,16 @@ func print(tw core.TableWriter) { } 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' +} diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go new file mode 100644 index 0000000..32050f8 --- /dev/null +++ b/pkg/cli/cli_test.go @@ -0,0 +1,47 @@ +// Copyright (C) 2023 Marius Schellenberger + +package cli + +import ( + "bytes" + "testing" +) + +func TestConfirm(t *testing.T) { + b := new(bytes.Buffer) + + b.WriteString("\n") + if confirm(b) { + t.Error("invalid confirm") + } + b.Reset() + + b.WriteString(" n\n") + if confirm(b) { + t.Error("invalid confirm") + } + b.Reset() + + b.WriteString(" foo\n") + if confirm(b) { + t.Error("invalid confirm") + } + b.Reset() + + b.WriteString("Y\n") + if confirm(b) { + t.Error("invalid confirm") + } + b.Reset() + + b.WriteString(" y\n") + if !confirm(b) { + t.Error("valid confirm") + } + b.Reset() + + b.WriteString("y\n") + if !confirm(b) { + t.Error("valid confirm") + } +} diff --git a/pkg/db/key.go b/pkg/db/key.go index 513755f..604fd92 100644 --- a/pkg/db/key.go +++ b/pkg/db/key.go @@ -31,8 +31,8 @@ func (db *DB) GetKeyWithSecret(id string) (k core.Key, err error) { return } -func (db *DB) CreateKey(name string, size int, enc core.Encoding, t core.Type) (k core.Key, err error) { - kstore, k, err := key.Generate(name, size, enc, t) +func (db *DB) CreateKey(name, existing string, size int, enc core.Encoding, t core.Type) (k core.Key, err error) { + kstore, k, err := key.Generate(name, existing, size, enc, t) if err != nil { return } diff --git a/pkg/key/key.go b/pkg/key/key.go index 39c3871..bb00ac8 100644 --- a/pkg/key/key.go +++ b/pkg/key/key.go @@ -26,13 +26,16 @@ func GenerateID() (string, error) { return hex.EncodeToString(id), nil } -func Generate(name string, size int, enc core.Encoding, t core.Type) (kstore, k core.Key, err error) { +func Generate(name, existing string, size int, enc core.Encoding, t core.Type) (kstore, k core.Key, err error) { id, err := GenerateID() if err != nil { return } var key []byte - if t == core.Xor { + if existing != "" { + key, err = hex.DecodeString(existing) + size = len(key) + } else if t == core.Xor { key, err = generate(size * 2) } else { key, err = generate(size)