diff --git a/cmd/keyctl/main.go b/cmd/keyctl/main.go index 022a56a..7178a4b 100644 --- a/cmd/keyctl/main.go +++ b/cmd/keyctl/main.go @@ -53,8 +53,6 @@ func main() { arg := os.Args[1] switch arg { case "new", "get", "ls", "req", "approve", "reject", "del": - //flag.StringVar(&config, "config", "/etc/keyctl/url.conf", "path to client config file") - //flag.Parse() cli.Run(arg) // does not return case "server": fs := flag.NewFlagSet("", flag.ExitOnError) diff --git a/go.mod b/go.mod index a081426..8fe6d7a 100644 --- a/go.mod +++ b/go.mod @@ -19,8 +19,9 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/hashicorp/vault v1.13.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect diff --git a/go.sum b/go.sum index 6074709..09a33ee 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -141,6 +142,8 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/vault v1.13.0 h1:/J/ldGnqRHKDlaSznOQAXy4STah76tCYy2fsUSsvYwk= +github.com/hashicorp/vault v1.13.0/go.mod h1:W1VOUMJ1klrGAHFnTJYDw2N2RlBh+kdwmCyYZTSGap0= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -164,6 +167,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM= github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -176,6 +180,7 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= diff --git a/pkg/api/server.go b/pkg/api/server.go index 0d36150..c34aaa5 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -51,7 +51,6 @@ func NewServer(log logr.Logger, listen, socket string) (*Server, error) { s.listen = l sock, err := unixListener(socket) - net.Listen("unix", socket) if err != nil { return nil, err } diff --git a/pkg/api/v1/client/client.go b/pkg/api/v1/client/client.go index 686e151..d566ea8 100644 --- a/pkg/api/v1/client/client.go +++ b/pkg/api/v1/client/client.go @@ -83,14 +83,14 @@ func (c *Client) GetKey(ctx context.Context, id string) (key schema.Key, err err return } -func (c *Client) CreateKey(ctx context.Context, key schema.NewKey) (err error) { +func (c *Client) CreateKey(ctx context.Context, newKey schema.NewKey) (key schema.Key, err error) { buf := new(bytes.Buffer) path := keyPath - err = key.Validate() + err = newKey.Validate() if err != nil { return } - err = json.NewEncoder(buf).Encode(key) + err = json.NewEncoder(buf).Encode(newKey) if err != nil { return } @@ -98,7 +98,7 @@ func (c *Client) CreateKey(ctx context.Context, key schema.NewKey) (err error) { if err != nil { return } - _, err = c.c.Do(req, nil) + _, err = c.c.Do(req, &key) return } diff --git a/pkg/api/v1/schema/schema.go b/pkg/api/v1/schema/schema.go index 136e75c..732cb4e 100644 --- a/pkg/api/v1/schema/schema.go +++ b/pkg/api/v1/schema/schema.go @@ -3,7 +3,10 @@ package schema import ( + "encoding/hex" "errors" + "fmt" + "time" "git.giftfish.de/ston1th/keyctl/pkg/api/types" "git.giftfish.de/ston1th/keyctl/pkg/core" @@ -22,6 +25,7 @@ type NewKey struct { Name string `json:"name"` Size int `json:"size"` Encoding core.Encoding `json:"encoding"` + Type core.Type `json:"type"` } var ( @@ -58,13 +62,52 @@ type Key struct { Created int64 `json:"created"` Size int `json:"size"` Encoding core.Encoding `json:"encoding"` + Type core.Type `json:"type"` Key string `json:"key,omitempty"` } +func (k Key) Print(share string) (string, error) { + switch k.Type { + case core.Shamir: + part1, err := k.Encoding.Decode(k.Key) + if err != nil { + return "", err + } + part2, err := hex.DecodeString(share) + if err != nil { + return "", err + } + dec, err := k.Type.Decode(append(part1, part2...)) + if err != nil { + return "", err + } + return k.Encoding.EncodeToString(dec), nil + } + return k.Key, nil +} + type Keys []Key func (Keys) Less(a, b Key) bool { return a.Name < b.Name } func (k Keys) Sort() { slices.SortStableFunc(k, k.Less) } +func (k Keys) Len() int { return len(k) } +func (k Keys) Fields() string { + return "ID\t Name\t Created\t Size\t Encoding\t Type" +} +func (k Keys) Values() []string { + vals := make([]string, len(k)) + for i, v := range k { + vals[i] = fmt.Sprintf("%s\t %s\t %s\t %d\t %s\t %s", + v.ID, + v.Name, + timeFmt(v.Created), + v.Size, + v.Encoding, + v.Type, + ) + } + return vals +} func NewKeys(keys core.Keys) (k Keys) { for _, key := range keys { @@ -87,6 +130,24 @@ type Approvals []Approval func (Approvals) Less(a, b Approval) bool { return a.Name < b.Name } func (a Approvals) Sort() { slices.SortStableFunc(a, a.Less) } +func (a Approvals) Len() int { return len(a) } +func (a Approvals) Fields() string { + return "ID\t Approval ID\t Name\t Status\t Created\t IP" +} +func (a Approvals) Values() []string { + vals := make([]string, len(a)) + for i, v := range a { + vals[i] = fmt.Sprintf("%s\t %s\t %s\t %s\t %s\t %s", + v.ID, + v.AID, + v.Name, + v.Status, + timeFmt(v.Created), + v.IP, + ) + } + return vals +} func NewApprovals(m types.ApprovalMap) (a Approvals) { for k, v := range m { @@ -102,3 +163,7 @@ func NewApprovals(m types.ApprovalMap) (a Approvals) { a.Sort() return } + +func timeFmt(t int64) string { + return time.Unix(t, 0).Format(time.RFC3339) +} diff --git a/pkg/api/v1/server/handler.go b/pkg/api/v1/server/handler.go index 550cba2..cf758ef 100644 --- a/pkg/api/v1/server/handler.go +++ b/pkg/api/v1/server/handler.go @@ -56,7 +56,7 @@ func newHandler(ctx *types.Context) { ctx.Err(types.ErrInvalid) return } - key, err := ctx.Data.DB.CreateKey(newKey.Name, newKey.Size, newKey.Encoding) + key, err := ctx.Data.DB.CreateKey(newKey.Name, 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 5a0def7..dbf109d 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -5,9 +5,11 @@ package cli import ( "context" "encoding/json" + "errors" "flag" "fmt" "os" + "text/tabwriter" apiclient "git.giftfish.de/ston1th/keyctl/pkg/api/client" clientv1 "git.giftfish.de/ston1th/keyctl/pkg/api/v1/client" @@ -27,33 +29,64 @@ func Run(arg string) { ctx := context.Background() switch arg { case "new": - name, encoding, size := newKeyFlags() + name, encoding, t, size, err := newKeyFlags() + if err != nil { + exit(err) + } newKey := schema.NewKey{ Name: name, Size: size, Encoding: core.EncodingFromString(encoding), + Type: core.TypeFromString(t), } c := client() - err = c.CreateKey(ctx, newKey) + k, err := c.CreateKey(ctx, newKey) + if err != nil { + exit(err) + } + switch k.Type { + case core.Plain: + fmt.Println("Key ID:", k.ID) + case core.Shamir: + fmt.Println("Key ID:", k.ID) + fmt.Println("Key Share:", k.Key) + } case "del": - id := idFlags("key id") + id, err := idFlags("key id") + if err != nil { + exit(err) + } c := client() err = c.DeleteKey(ctx, id) case "approve": - id := idFlags("approval id") + id, err := idFlags("approval id") + if err != nil { + exit(err) + } c := client() err = c.AcceptApproval(ctx, id) case "reject": - id := idFlags("approval id") + id, err := idFlags("approval id") + if err != nil { + exit(err) + } c := client() err = c.RejectApproval(ctx, id) case "get": - id, json := getFlags() + id, share, json, err := getFlags() + if err != nil { + exit(err) + } c := client() key, err := c.GetKey(ctx, id) if err != nil { exit(err) } + out, err := key.Print(share) + if err != nil { + exit(err) + } + key.Key = out if json { jsonOutput(key) // does not return } @@ -68,15 +101,7 @@ func Run(arg string) { if json { jsonOutput(keys) // does not return } - for _, k := range keys { - fmt.Printf("%s %s %d %s %d\n", - k.ID, - k.Name, - k.Size, - k.Encoding, - k.Created, - ) - } + print(keys) case "req": json := outputFlags() c := client() @@ -87,16 +112,7 @@ func Run(arg string) { if json { jsonOutput(approvals) // does not return } - for _, a := range approvals { - fmt.Printf("%s %s %s %s %d %s\n", - a.ID, - a.AID, - a.Name, - a.Status, - a.Created, - a.IP, - ) - } + print(approvals) } if err != nil { exit(err) @@ -126,16 +142,23 @@ func jsonOutput(v any) { os.Exit(0) } -func idFlags(desc string) (id string) { +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 getFlags() (id string, json bool) { +func getFlags() (id, share string, json bool, err error) { fs.StringVar(&id, "id", "", "key id") + fs.StringVar(&share, "share", "", "shamir key share") fs.BoolVar(&json, "json", false, "json output") fs.Parse(os.Args[2:]) + if id == "" { + err = errors.New("missing flag: id") + } return } @@ -145,10 +168,26 @@ func outputFlags() (json bool) { return } -func newKeyFlags() (name, encoding string, size int) { +func newKeyFlags() (name, encoding, t string, size int, err error) { fs.StringVar(&name, "name", "", "key name") fs.StringVar(&encoding, "enc", "hex", "key encoding scheme [hex|base32|base64|base64url]") + fs.StringVar(&t, "type", "plain", "key type [plain|shamir]") 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() +} diff --git a/pkg/core/encoding.go b/pkg/core/encoding.go index 429922d..4e2df70 100644 --- a/pkg/core/encoding.go +++ b/pkg/core/encoding.go @@ -36,6 +36,18 @@ func (e Encoding) EncodeToString(src []byte) string { return hex.EncodeToString(src) } +func (e Encoding) Decode(src string) ([]byte, error) { + switch e { + case Base32: + return b32raw.DecodeString(src) + case Base64: + return base64.RawStdEncoding.DecodeString(src) + case Base64URL: + return base64.RawURLEncoding.DecodeString(src) + } + return hex.DecodeString(src) +} + func EncodingFromString(e string) Encoding { switch e { case "base32": diff --git a/pkg/core/key.go b/pkg/core/key.go index ef45192..2da47e7 100644 --- a/pkg/core/key.go +++ b/pkg/core/key.go @@ -20,6 +20,7 @@ type Key struct { Created int64 Size int Encoding Encoding + Type Type Key string } diff --git a/pkg/core/tablewriter.go b/pkg/core/tablewriter.go new file mode 100644 index 0000000..805828e --- /dev/null +++ b/pkg/core/tablewriter.go @@ -0,0 +1,9 @@ +// Copyright (C) 2023 Marius Schellenberger + +package core + +type TableWriter interface { + Len() int + Fields() string + Values() []string +} diff --git a/pkg/core/type.go b/pkg/core/type.go new file mode 100644 index 0000000..ee8eb00 --- /dev/null +++ b/pkg/core/type.go @@ -0,0 +1,55 @@ +// Copyright (C) 2023 Marius Schellenberger + +package core + +import ( + "bytes" + + "github.com/hashicorp/vault/shamir" +) + +type Type int + +func (t Type) String() string { + switch t { + case Shamir: + return "shamir" + } + return "plain" +} + +func (t Type) Decode(src []byte) ([]byte, error) { + switch t { + case Shamir: + plen := len(src) / 2 + part1 := src[0:plen] + part2 := src[plen:] + return shamir.Combine([][]byte{part1, part2}) + } + return src, nil +} + +func (t Type) Encode(src []byte) ([]byte, error) { + switch t { + case Shamir: + parts, err := shamir.Split(src, 2, 2) + if err != nil { + return nil, err + } + return bytes.Join(parts, nil), nil + } + return src, nil +} + +func TypeFromString(t string) Type { + switch t { + case "shamir": + return Shamir + } + return Plain +} + +const ( + Plain Type = iota + Shamir +) diff --git a/pkg/db/key.go b/pkg/db/key.go index 1b48041..513755f 100644 --- a/pkg/db/key.go +++ b/pkg/db/key.go @@ -31,12 +31,12 @@ func (db *DB) GetKeyWithSecret(id string) (k core.Key, err error) { return } -func (db *DB) CreateKey(name string, size int, enc core.Encoding) (k core.Key, err error) { - k, err = key.Generate(name, size, enc) +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) if err != nil { return } - err = db.store.Set(keyPrefix+k.ID, &k) + err = db.store.Set(keyPrefix+k.ID, &kstore) return } diff --git a/pkg/key/key.go b/pkg/key/key.go index 591aa84..07ac991 100644 --- a/pkg/key/key.go +++ b/pkg/key/key.go @@ -27,7 +27,7 @@ func GenerateID() (string, error) { return hex.EncodeToString(id), nil } -func Generate(name string, size int, enc core.Encoding) (k core.Key, err error) { +func Generate(name string, size int, enc core.Encoding, t core.Type) (kstore, k core.Key, err error) { id, err := GenerateID() if err != nil { return @@ -36,13 +36,24 @@ func Generate(name string, size int, enc core.Encoding) (k core.Key, err error) if err != nil { return } - k = core.Key{ + key, err = t.Encode(key) + if err != nil { + return + } + kstore = core.Key{ Name: name, ID: id, Size: size, Encoding: enc, - Key: enc.EncodeToString(key), + Type: t, Created: time.Now().Unix(), } + k = kstore + if t == core.Shamir { + klen := len(key) / 2 + k.Key = core.Hex.EncodeToString(key[klen:]) + key = key[0:klen] + } + kstore.Key = enc.EncodeToString(key) return }