github.com/GGP1/kure@v0.8.4/commands/2fa/2fa.go (about) 1 // Package tfa handles two-factor authentication codes. 2 package tfa 3 4 import ( 5 "crypto/hmac" 6 "crypto/sha1" 7 "encoding/base32" 8 "encoding/binary" 9 "fmt" 10 "math" 11 "os" 12 "strings" 13 "time" 14 15 "github.com/GGP1/kure/auth" 16 cmdutil "github.com/GGP1/kure/commands" 17 "github.com/GGP1/kure/commands/2fa/add" 18 "github.com/GGP1/kure/commands/2fa/rm" 19 "github.com/GGP1/kure/db/totp" 20 "github.com/GGP1/kure/orderedmap" 21 "github.com/GGP1/kure/pb" 22 "github.com/GGP1/kure/terminal" 23 "github.com/GGP1/kure/tree" 24 25 "github.com/spf13/cobra" 26 bolt "go.etcd.io/bbolt" 27 ) 28 29 const example = ` 30 * List one and copy to the clipboard 31 kure 2fa Sample -c 32 33 * List all 34 kure 2fa 35 36 * Display information about the setup key 37 kure 2fa Sample -i` 38 39 type tfaOptions struct { 40 copy, info bool 41 timeout time.Duration 42 } 43 44 // NewCmd returns a new command. 45 func NewCmd(db *bolt.DB) *cobra.Command { 46 opts := tfaOptions{} 47 cmd := &cobra.Command{ 48 Use: "2fa <name>", 49 Short: "List two-factor authentication codes", 50 Long: `List two-factor authentication codes. 51 52 Use the [-i info] flag to display information about the setup key, it also generates a QR code with the key in URL format that can be scanned by any authenticator.`, 53 Example: example, 54 Args: cmdutil.MustExistLs(db, cmdutil.TOTP), 55 PreRunE: auth.Login(db), 56 RunE: run2FA(db, &opts), 57 PostRun: func(cmd *cobra.Command, args []string) { 58 // Reset variables (session) 59 opts = tfaOptions{} 60 }, 61 } 62 63 cmd.AddCommand(add.NewCmd(db, os.Stdin), rm.NewCmd(db, os.Stdin)) 64 65 f := cmd.Flags() 66 f.BoolVarP(&opts.copy, "copy", "c", false, "copy code to clipboard") 67 f.BoolVarP(&opts.info, "info", "i", false, "display information about the setup key") 68 f.DurationVarP(&opts.timeout, "timeout", "t", 0, "clipboard clearing timeout") 69 70 return cmd 71 } 72 73 func run2FA(db *bolt.DB, opts *tfaOptions) cmdutil.RunEFunc { 74 return func(cmd *cobra.Command, args []string) error { 75 name := strings.Join(args, " ") 76 name = cmdutil.NormalizeName(name) 77 78 if name == "" { 79 totps, err := totp.ListNames(db) 80 if err != nil { 81 return err 82 } 83 84 tree.Print(totps) 85 return nil 86 } 87 88 t, err := totp.Get(db, name) 89 if err != nil { 90 return err 91 } 92 93 if opts.info { 94 return printKeyInfo(t) 95 } 96 97 code := GenerateTOTP(t.Raw, time.Now(), int(t.Digits)) 98 if opts.copy { 99 return cmdutil.WriteClipboard(cmd, opts.timeout, "TOTP", code) 100 } 101 102 fmt.Println(strings.Title(t.Name), code) 103 return nil 104 } 105 } 106 107 // GenerateTOTP returns a Time-based One-Time Password code. 108 func GenerateTOTP(key string, t time.Time, digits int) string { 109 // Do not check error as the key was validated when added 110 keyBytes, _ := base32.StdEncoding.DecodeString(key) 111 h := hmac.New(sha1.New, keyBytes) 112 113 // 30 is the default time-step size in seconds (recommended 114 // as per https://tools.ietf.org/html/rfc6238#section-5.2) 115 counter := math.Floor(float64(t.Unix()) / 30) 116 buf := make([]byte, 8) 117 binary.BigEndian.PutUint64(buf, uint64(counter)) 118 h.Write(buf) 119 sum := h.Sum(nil) 120 121 // "Dynamic truncation" in RFC 4226 122 // http://tools.ietf.org/html/rfc4226#section-5.4 123 offset := sum[len(sum)-1] & 0xf 124 value := int64(((int(sum[offset]) & 0x7f) << 24) | 125 ((int(sum[offset+1] & 0xff)) << 16) | 126 ((int(sum[offset+2] & 0xff)) << 8) | 127 (int(sum[offset+3]) & 0xff)) 128 129 mod := int32(value % int64(math.Pow10(digits))) 130 format := fmt.Sprintf("%%0%dd", digits) 131 132 return fmt.Sprintf(format, mod) 133 } 134 135 func printKeyInfo(t *pb.TOTP) error { 136 // https://github.com/google/google-authenticator/wiki/Key-Uri-Format 137 URL := fmt.Sprintf("otpauth://totp/%s?secret=%s&digits=%d", strings.Title(t.Name), t.Raw, t.Digits) 138 139 if err := terminal.DisplayQRCode(URL); err != nil { 140 return err 141 } 142 mp := orderedmap.New() 143 mp.Set("URL", URL) 144 mp.Set("Key", t.Raw) 145 mp.Set("Digits", fmt.Sprint(t.Digits)) 146 147 box := cmdutil.BuildBox(t.Name, mp) 148 fmt.Println(box) 149 return nil 150 }