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  }