diff --git a/cmd/key_generate.go b/cmd/key_generate.go index 87e3905..0a8a3a3 100644 --- a/cmd/key_generate.go +++ b/cmd/key_generate.go @@ -1,10 +1,13 @@ package cmd import ( + "bytes" stdcrypto "crypto" "errors" "fmt" + "os/exec" "os/user" + "strconv" "strings" "time" @@ -21,9 +24,14 @@ type KeyGenerate struct { ktype string comment string bits int - ttl time.Duration + ttl string } +const ( + day = 24 * time.Hour + year = 8766 * time.Hour // average year is 365.25 days +) + func (cmd KeyGenerate) Command() *cobra.Command { c := &cobra.Command{ Use: "generate", @@ -40,17 +48,21 @@ func (cmd KeyGenerate) Command() *cobra.Command { c.Flags().StringVarP(&cmd.comment, "comment", "c", "", "a note to add to the key") c.Flags().StringVarP(&cmd.ktype, "type", "t", "rsa", "type of the key") c.Flags().IntVarP(&cmd.bits, "bits", "b", 4096, "size of RSA key in bits") - year := 24 * time.Hour * 365 * 2 - // TODO: use another duration parser to support days, months, and years - c.Flags().DurationVar(&cmd.ttl, "ttl", year, "validity period of the key") + c.Flags().StringVar( + &cmd.ttl, "ttl", "1y", + "validity period of the key. Can be a date (2020-12-30) or duration (4y30d, 24h)", + ) return c } func (cmd KeyGenerate) run() error { username := cmd.Username() - if username == "" { - return errors.New("--name is required") + email := cmd.Email() + ttl, err := ParseDuration(cmd.ttl) + if err != nil { + return fmt.Errorf("cannot parse --ttl: %v", err) } + alg := cmd.algorithm() if alg == 0 { return fmt.Errorf("unsupported key type: %v", cmd.ktype) @@ -61,14 +73,14 @@ func (cmd KeyGenerate) run() error { cfg := &packet.Config{ Algorithm: alg, RSABits: cmd.bits, - KeyLifetimeSecs: uint32(cmd.ttl.Seconds()), + KeyLifetimeSecs: uint32(ttl.Seconds()), Time: crypto.GetTime, DefaultHash: stdcrypto.SHA256, DefaultCipher: packet.CipherAES256, DefaultCompressionAlgo: packet.CompressionZLIB, } - entity, err := openpgp.NewEntity(username, cmd.comment, cmd.email, cfg) + entity, err := openpgp.NewEntity(username, cmd.comment, email, cfg) if err != nil { return fmt.Errorf("cannot create entity: %v", err) } @@ -96,6 +108,20 @@ func (cmd KeyGenerate) Username() string { return "" } +func (cmd KeyGenerate) Email() string { + if cmd.email != "" { + return cmd.email + } + c := exec.Command("git", "config", "user.email") + var stdout bytes.Buffer + c.Stdout = &stdout + err := c.Run() + if err == nil { + return strings.TrimSpace(stdout.String()) + } + return "" +} + func (cmd KeyGenerate) algorithm() packet.PublicKeyAlgorithm { switch strings.ToLower(cmd.ktype) { case "rsa": @@ -114,3 +140,46 @@ func (cmd KeyGenerate) algorithm() packet.PublicKeyAlgorithm { return 0 } } + +func ParseDuration(ttl string) (time.Duration, error) { + if ttl == "" { + return 0, nil + } + t, err := time.Parse("2006-01-02", ttl) + if err == nil { + return t.Sub(time.Now()), nil + } + + var shift time.Duration + + // parse years + parts := strings.Split(ttl, "y") + if len(parts) == 2 { + years, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, fmt.Errorf("parse year: %v", err) + } + shift += time.Duration(years) * year + ttl = parts[1] + } + + // parse days + parts = strings.Split(ttl, "d") + if len(parts) == 2 { + days, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, fmt.Errorf("parse year: %v", err) + } + shift += time.Duration(days) * day + ttl = parts[1] + } + + if ttl == "" { + return shift, nil + } + d, err := time.ParseDuration(ttl) + if err != nil { + return 0, err + } + return d + shift, nil +} diff --git a/cmd/key_generate_test.go b/cmd/key_generate_test.go index a800028..e3965a0 100644 --- a/cmd/key_generate_test.go +++ b/cmd/key_generate_test.go @@ -3,7 +3,9 @@ package cmd_test import ( "bytes" "testing" + "time" + "github.com/life4/enc/cmd" "github.com/matryer/is" ) @@ -21,3 +23,27 @@ func TestKeyGenerate_IsRandom(t *testing.T) { out2 := CallHappy(t, "key generate", nil) is.True(!bytes.Equal(out1, out2)) } + +func TestParseDuration(t *testing.T) { + t.Parallel() + testCases := []struct { + given string + expected time.Duration + }{ + {"4h", 4 * time.Hour}, + {"5d", 5 * 24 * time.Hour}, + {"5d4h", (5*24 + 4) * time.Hour}, + {"2y", 2 * 8766 * time.Hour}, + {"4h20m", 4*time.Hour + 20*time.Minute}, + } + for _, tCase := range testCases { + tc := tCase + t.Run(tc.given, func(t *testing.T) { + is := is.New(t) + t.Parallel() + actual, err := cmd.ParseDuration(tc.given) + is.NoErr(err) + is.Equal(actual, tc.expected) + }) + } +} diff --git a/cmd/key_info.go b/cmd/key_info.go index eaf025f..f785c2d 100644 --- a/cmd/key_info.go +++ b/cmd/key_info.go @@ -42,11 +42,13 @@ func (cmd KeyInfo) run() error { expiration := prim.CreationTime.Add(time.Duration(*ttl) * time.Second) expirationStr = expiration.Format(time.RFC3339) } + bits, _ := prim.BitLength() result := map[string]interface{}{ // basic key info "id": key.GetHexKeyID(), "fingerprint": key.GetFingerprint(), "algorithm": cmd.algorithm(key), + "bits": bits, "created_at": prim.CreationTime.Format(time.RFC3339), "expires_at": expirationStr, "fingerprints": key.GetSHA256Fingerprints(),