github.com/GGP1/kure@v0.8.4/commands/gen/phrase/phrase.go (about)

     1  package phrase
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"strings"
     7  
     8  	cmdutil "github.com/GGP1/kure/commands"
     9  	"github.com/GGP1/kure/terminal"
    10  
    11  	"github.com/GGP1/atoll"
    12  
    13  	"github.com/awnumar/memguard"
    14  	"github.com/pkg/errors"
    15  	"github.com/spf13/cobra"
    16  )
    17  
    18  var (
    19  	thousand    = math.Pow(10, 3)
    20  	million     = math.Pow(10, 6)
    21  	billion     = math.Pow(10, 9)
    22  	trillion    = math.Pow(10, 12)
    23  	quadrillion = math.Pow(10, 15)
    24  	quintillion = math.Pow(10, 18)
    25  	sextillion  = math.Pow(10, 21)
    26  )
    27  
    28  const (
    29  	minute     = 60
    30  	hour       = minute * 60
    31  	day        = hour * 24
    32  	month      = day * 30
    33  	year       = month * 12
    34  	decade     = year * 10
    35  	century    = decade * 10
    36  	millennium = century * 10
    37  )
    38  
    39  const example = `
    40  * Generate a random passphrase
    41  kure gen phrase -l 8 -L WordList -s &
    42  
    43  * Generate and show QR code
    44  kure gen phrase -l 5 -q
    45  
    46  * Generate, copy and mute standard output
    47  kure gen -l 7 -cm`
    48  
    49  type phraseOptions struct {
    50  	list, separator string
    51  	incl, excl      []string
    52  	copy, mute, qr  bool
    53  	length          uint64
    54  }
    55  
    56  // NewCmd returns a new command.
    57  func NewCmd() *cobra.Command {
    58  	opts := phraseOptions{}
    59  	cmd := &cobra.Command{
    60  		Use:   "phrase",
    61  		Short: "Generate a random passphrase",
    62  		Long: `Generate a random passphrase.
    63  		
    64  Keyspace is the number of possible combinations of the passphrase.
    65  Average time taken to crack is based on a brute force attack scenario where the guesses per second is 1 trillion.`,
    66  		Aliases: []string{"passphrase"},
    67  		Example: example,
    68  		RunE:    runPhrase(&opts),
    69  		PostRun: func(cmd *cobra.Command, args []string) {
    70  			// Reset variables (session)
    71  			opts = phraseOptions{
    72  				separator: " ",
    73  			}
    74  		},
    75  	}
    76  
    77  	f := cmd.Flags()
    78  	f.BoolVarP(&opts.copy, "copy", "c", false, "copy the passphrase to the clipboard")
    79  	f.Uint64VarP(&opts.length, "length", "l", 0, "number of words")
    80  	f.StringVarP(&opts.separator, "separator", "s", " ", "character that separates each word")
    81  	f.StringVarP(&opts.list, "list", "L", "WordList", "passphrase list used {NoList|WordList|SyllableList}")
    82  	f.StringSliceVarP(&opts.incl, "include", "i", nil, "words to include in the passphrase")
    83  	f.StringSliceVarP(&opts.excl, "exclude", "e", nil, "words to exclude from the passphrase")
    84  	f.BoolVarP(&opts.qr, "qr", "q", false, "show QR code on terminal")
    85  	f.BoolVarP(&opts.mute, "mute", "m", false, "mute standard output when the passphrase is copied")
    86  
    87  	return cmd
    88  }
    89  
    90  func runPhrase(opts *phraseOptions) cmdutil.RunEFunc {
    91  	return func(cmd *cobra.Command, args []string) error {
    92  		if opts.length < 1 || opts.length > math.MaxUint64 {
    93  			return cmdutil.ErrInvalidLength
    94  		}
    95  
    96  		// Use Wordlist list as default
    97  		l := atoll.WordList
    98  		if opts.list != "" {
    99  			opts.list = strings.ReplaceAll(opts.list, " ", "")
   100  
   101  			switch strings.ToLower(opts.list) {
   102  			case "nolist", "no":
   103  				l = atoll.NoList
   104  
   105  			case "wordlist", "word":
   106  				// Do nothing as it's the default
   107  
   108  			case "syllablelist", "syllable":
   109  				l = atoll.SyllableList
   110  
   111  			default:
   112  				return errors.Errorf("invalid list: %q", opts.list)
   113  			}
   114  		}
   115  
   116  		p := &atoll.Passphrase{
   117  			Length:    opts.length,
   118  			Separator: opts.separator,
   119  			Include:   opts.incl,
   120  			Exclude:   opts.excl,
   121  			List:      l,
   122  		}
   123  
   124  		passphrase, err := atoll.NewSecret(p)
   125  		if err != nil {
   126  			return err
   127  		}
   128  
   129  		phraseBuf := memguard.NewBufferFromBytes([]byte(passphrase))
   130  		memguard.WipeBytes([]byte(passphrase))
   131  		defer phraseBuf.Destroy()
   132  
   133  		if opts.qr {
   134  			if err := terminal.DisplayQRCode(phraseBuf.String()); err != nil {
   135  				return err
   136  			}
   137  		}
   138  
   139  		entropy := p.Entropy()
   140  		keyspace, timeToCrack := FormatSecretSecurity(atoll.Keyspace(p), atoll.SecondsToCrack(p)/2)
   141  
   142  		if !opts.mute || !opts.copy {
   143  			fmt.Printf(`Passphrase: %s
   144  	
   145  Entropy: %.2f bits
   146  Keyspace: %s
   147  Average time taken to crack: %s
   148  `, phraseBuf.String(), entropy, keyspace, timeToCrack)
   149  		}
   150  
   151  		if opts.copy {
   152  			if err := cmdutil.WriteClipboard(cmd, 0, "Passphrase", phraseBuf.String()); err != nil {
   153  				return err
   154  			}
   155  		}
   156  		return nil
   157  	}
   158  }
   159  
   160  // FormatSecretSecurity makes the data passed easier to read.
   161  func FormatSecretSecurity(keyspace, avgTimeToCrack float64) (space, time string) {
   162  	if math.IsInf(keyspace, 1) {
   163  		return "+Inf", "+Inf"
   164  	}
   165  
   166  	space = fmt.Sprintf("%.2f", keyspace)
   167  	time = fmt.Sprintf("%.2f seconds", avgTimeToCrack)
   168  
   169  	switch {
   170  	case keyspace >= sextillion:
   171  		space = fmt.Sprintf("%.2f sextillion", keyspace/sextillion)
   172  
   173  	case keyspace >= quintillion:
   174  		space = fmt.Sprintf("%.2f quintillion", keyspace/quintillion)
   175  
   176  	case keyspace >= quadrillion:
   177  		space = fmt.Sprintf("%.2f quadrillion", keyspace/quadrillion)
   178  
   179  	case keyspace >= trillion:
   180  		space = fmt.Sprintf("%.2f trillion", keyspace/trillion)
   181  
   182  	case keyspace >= billion:
   183  		space = fmt.Sprintf("%.2f billion", keyspace/billion)
   184  
   185  	case keyspace >= million:
   186  		space = fmt.Sprintf("%.2f million", keyspace/million)
   187  
   188  	case keyspace >= thousand:
   189  		space = fmt.Sprintf("%.2f thousand", keyspace/thousand)
   190  	}
   191  
   192  	switch {
   193  	case avgTimeToCrack >= millennium:
   194  		time = fmt.Sprintf("%.2f millenniums", avgTimeToCrack/millennium)
   195  
   196  	case avgTimeToCrack >= century:
   197  		time = fmt.Sprintf("%.2f centuries", avgTimeToCrack/century)
   198  
   199  	case avgTimeToCrack >= decade:
   200  		time = fmt.Sprintf("%.2f decades", avgTimeToCrack/decade)
   201  
   202  	case avgTimeToCrack >= year:
   203  		time = fmt.Sprintf("%.2f years", avgTimeToCrack/year)
   204  
   205  	case avgTimeToCrack >= month:
   206  		time = fmt.Sprintf("%.2f months", avgTimeToCrack/month)
   207  
   208  	case avgTimeToCrack >= day:
   209  		time = fmt.Sprintf("%.2f days", avgTimeToCrack/day)
   210  
   211  	case avgTimeToCrack >= hour:
   212  		time = fmt.Sprintf("%.2f hours", avgTimeToCrack/hour)
   213  
   214  	case avgTimeToCrack >= minute:
   215  		time = fmt.Sprintf("%.2f minutes", avgTimeToCrack/minute)
   216  	}
   217  
   218  	return prettify(space), prettify(time)
   219  }
   220  
   221  // prettify adds commas to the number inside the string to make it easier for humans to read.
   222  //
   223  // Example: 51334.21 -> 51,334.21
   224  func prettify(str string) string {
   225  	idx := strings.IndexByte(str, '.')
   226  	num := str[:idx]
   227  
   228  	sb := strings.Builder{}
   229  	j := 0
   230  	for i := len(num) - 1; i >= 0; i-- {
   231  		sb.WriteByte(num[j])
   232  		if i%3 == 0 && i != 0 {
   233  			sb.WriteByte(',')
   234  		}
   235  		j++
   236  	}
   237  
   238  	// Append remaining characters
   239  	sb.WriteString(str[idx:])
   240  	return sb.String()
   241  }