From 5cf68a8772a329bfe9cf40cc9cf280c484ae067e Mon Sep 17 00:00:00 2001 From: Vuong <3168632+vuongggggg@users.noreply.github.com> Date: Sun, 6 Sep 2020 21:18:00 +0700 Subject: [PATCH] Implementation - Standard features like pwgen - Create LICENSE - Add goroutines, optimize duplicated naming for CLI options - Create content & GitAction badge for README - Create go.yml --- .github/workflows/go.yml | 36 ++++++ LICENSE | 21 ++++ README.md | 50 ++++++++ main.go | 246 +++++++++++++++++++++++++++++++++++---- subcommand.go | 160 +++++++++++++++++++++++++ 5 files changed, 492 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/go.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 subcommand.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..0d9e1dd --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,36 @@ +name: Go + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + build: + name: Build + runs-on: ubuntu-latest + steps: + + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: ^1.13 + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Get dependencies + run: | + go get -v -t -d ./... + if [ -f Gopkg.toml ]; then + curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + dep ensure + fi + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7638613 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 ѵµσɳɠ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8d36d7 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# pwgen-go +Password generator practice in Go. It is inspired by https://github.com/jbernard/pwgen + +![Go](https://github.com/vuon9/pwgen-go/workflows/Go/badge.svg) + +## The manual + +This is following pwgen's manual: https://linux.die.net/man/1/pwgen +or can follow the usages by `pwgen-go -help` or `pwgen-go -h`. + +## Download + +```bash +go get -u github.com/vuon9/pwgen-go +``` + +## Usages + +```md +Usage: pwgen-go [ OPTIONS ] [pw_length] [num_pw] +Options supported by pwgen-go: + -h or -help + Get help + -c or -capitalize + Include at least one capital letter in the password + -A or -no-capitalize + Don't include capital letters in the password + -n or -numerals + Include at least one number in the password + -0 or -no-numerals + Don't include numbers in the password + -y or -symbol + Include at least one special symbol in the password + -r or --remove-chars= + Remove characters from the set of characters to generate passwords + -H or -sha1=path/to/file[#seed] + Use sha1 hash of given file as a (not so) random generator + -B or -ambigous + Don't include ambiguous characters in the password + -v or -no-vowels + Do not use any vowels so as to avoid accidental nasty words + -s or -secure + Generate completely random passwords + -column + Print the generated passwords in columns + -no-column + Don't print the generated passwords in columns + -vvv or -debug + Enable debug mode +``` diff --git a/main.go b/main.go index 193e7f6..aa27ebf 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,21 @@ package main import ( + "crypto/hmac" + "crypto/sha1" + "errors" "fmt" + "io" "math/rand" + "os" + "strconv" + "strings" + "sync" "time" ) const ( + // TODO: Investigate how to use these below CONSONANT = 0x0001 VOWEL = 0x0002 DIPTHONG = 0x0004 @@ -21,30 +30,206 @@ const ( pwDigits = "0123456789" pwUppers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" pwLowers = "abcdefghijklmnopqrstuvwxyz" - pwSymbols = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + pwSymbols = "!\"#$%&'()*+,-./:<=>?@[\\]^_`{|}~" pwAmbiguous = "B8G6I1l0OQDS5Z2" pwVowels = "01aeiouyAEIOUY" ) +func defaultPwOptions() *pwOptions { + return &pwOptions{ + pwLen: 20, + numPw: 1, + } +} + +type pwOptions struct { + pwLen int + numPw int +} + +func filterValidArgs(allOsArgs []string) []int { + // The valid argument should be int + isValidArgument := func(arg string) (int, bool) { + isFlag := strings.HasPrefix("-", arg) + if isFlag { + return 0, false + } + + val, err := strconv.Atoi(arg) + return val, err == nil + } + + validArgs := make([]int, 0) + for _, rawArg := range allOsArgs { + if val, ok := isValidArgument(rawArg); ok && val > 0 { + validArgs = append(validArgs, val) + } + } + + return validArgs +} + +func getOptions(pwArgs []int, pwOptions *pwOptions) *pwOptions { + if len(pwArgs) >= 1 { + pwOptions.pwLen = pwArgs[0] + } + + if len(pwArgs) >= 2 { + pwOptions.numPw = pwArgs[1] + } + + return pwOptions +} + func main() { - rand.Seed(time.Now().UnixNano()) - pwRand(nil, 16, PW_DIGITS|PW_UPPERS, nil) + cmdCapitalize := "capitalize" + cmdNoCapitalize := "no-capitalize" + cmdHelp := "help" + cmdNumerals := "numerals" + cmdNoNumerals := "no-numerals" + cmdSymbol := "symbol" + cmdRemoveChars := "remove-chars" + cmdSha1 := "sha1" + cmdAmbigous := "ambigous" + cmdNoVowels := "no-vowels" + cmdSecure := "secure" + cmdColumn := "column" + cmdNoColumn := "no-column" + cmdDebug := "debug" + + commands := NewCommandController( + NewItems( + NewBoolCommand(Option{cmdHelp, "h", "", "Get help"}), + NewBoolCommand(Option{cmdCapitalize, "c", "", "Include at least one capital letter in the password"}), + NewBoolCommand(Option{cmdNoCapitalize, "A", "", "Don't include capital letters in the password"}), + NewBoolCommand(Option{cmdNumerals, "n", "", "Include at least one number in the password"}), + NewBoolCommand(Option{cmdNoNumerals, "0", "", "Don't include numbers in the password"}), + NewBoolCommand(Option{cmdSymbol, "y", "", "Include at least one special symbol in the password"}), + NewStringCommand(Option{cmdRemoveChars, "r", "-r or --remove-chars=", "Remove characters from the set of characters to generate passwords"}), + NewStringCommand(Option{cmdSha1, "H", "-H or -sha1=path/to/file[#seed]", "Use sha1 hash of given file as a (not so) random generator"}), + NewBoolCommand(Option{cmdAmbigous, "B", "", "Don't include ambiguous characters in the password"}), + NewBoolCommand(Option{cmdNoVowels, "v", "", "Do not use any vowels so as to avoid accidental nasty words"}), + NewBoolCommand(Option{cmdSecure, "s", "", "Generate completely random passwords"}), + NewBoolCommand(Option{cmdColumn, "", "", "Print the generated passwords in columns"}), + NewBoolCommand(Option{cmdNoColumn, "", "", "Don't print the generated passwords in columns"}), + NewBoolCommand(Option{cmdDebug, "vvv", "", "Enable debug mode"}), + ), + WithUsageHeader("Usage: pwgen-go [ OPTIONS ] [pw_length] [num_pw]\nOptions supported by pwgen-go:"), + ) + + commands.Ready() + + var hasSha1 string = commands.GetString("sha1") + if hasSha1 != "" { + splitted := strings.Split(hasSha1, "#") + if len(splitted) != 2 { + println("err: Sha1 filepath and seed are invalid, should be path/sub_path/file.extension#seed") + os.Exit(0) + } + + filePath, seed := splitted[0], splitted[1] + sha1File(filePath, seed) + os.Exit(0) + } + + pwOptions := getOptions( + filterValidArgs(os.Args[0:]), + defaultPwOptions(), + ) + + var pwFlags byte + var withColumn bool + var removeChars = "" + var debug bool + + switch { + case commands.GetBool(cmdCapitalize): + pwFlags |= PW_UPPERS + case commands.GetBool(cmdNoCapitalize): + pwFlags &^= PW_UPPERS + case commands.GetBool(cmdNumerals): + pwFlags |= PW_DIGITS + case commands.GetBool(cmdNoNumerals): + pwFlags ^= PW_DIGITS + case commands.GetBool(cmdSecure): + pwFlags = PW_DIGITS | PW_UPPERS + case commands.GetBool(cmdSymbol): + pwFlags |= PW_SYMBOLS + case commands.GetBool(cmdAmbigous): + pwFlags |= PW_AMBIGUOUS + case commands.GetBool(cmdNoVowels): + pwFlags |= PW_NO_VOWELS | PW_DIGITS | PW_UPPERS + case commands.GetBool(cmdNoColumn): + withColumn = false + case commands.GetBool(cmdColumn): + withColumn = true + case commands.GetString(cmdRemoveChars) != "": + removeChars = commands.GetString(cmdRemoveChars) + case commands.GetBool(cmdDebug): + debug = true + case commands.GetBool(cmdHelp): + commands.Usage() + os.Exit(0) + } + + // Randomize passwords by flags & eligible chars + var t1 time.Time + if debug { + t1 = time.Now() + } + + passwords, err := pwRand(nil, pwOptions, eligibleChars(pwFlags, removeChars)) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + // Print passwords by column or no column + const itemsPerColumn = 4 + for i, pwd := range passwords { + fmt.Printf("%s\t", pwd) + if withColumn && i+1 >= itemsPerColumn && (i+1)%itemsPerColumn == 0 { + fmt.Print("\n") + } + } + + if debug { + fmt.Println("\nElapsed time: ", time.Since(t1)) + } + os.Exit(0) +} + +func sha1File(filePath string, seed string) { + f, err := os.Open(filePath) + if err != nil { + println("err: Couldn't open file") + os.Exit(0) + } + defer f.Close() + + h := hmac.New(sha1.New, []byte(seed)) + if _, err := io.Copy(h, f); err != nil { + println("err: Couldn't has file content") + os.Exit(0) + } + + var s string + _, _ = h.Write([]byte(s)) + bs := h.Sum(nil) + fmt.Printf("%x\n", bs) } -func randomize(size int, chars string, t int) string { +func randomize(size int, chars string) []byte { newPw := make([]byte, size) for i := range newPw { newPw[i] = chars[rand.Int63()%int64(len(chars))] } - return string(newPw) + return newPw } -func pwRand(buf *string, size int, pwFlags byte, remove *string) { - // var ch, chars, wChars string - // var i, len, featureFlags int - - chars := "" +func eligibleChars(pwFlags byte, removeChars string) string { + chars := pwLowers if (pwFlags & PW_DIGITS) != 0 { chars += pwDigits } @@ -57,20 +242,39 @@ func pwRand(buf *string, size int, pwFlags byte, remove *string) { chars += pwSymbols } - chars += pwLowers + if (pwFlags & PW_AMBIGUOUS) != 0 { + chars += pwAmbiguous + } - pwds := make([]string, 16) + if (pwFlags & PW_NO_VOWELS) == 0 { + chars += pwVowels + } - for i := range pwds { - pwds[i] = randomize(size, chars, i) + for _, rChar := range removeChars { + chars = strings.ReplaceAll(chars, string(rChar), "") } - fmt.Println(pwFlags, pwFlags&PW_DIGITS, pwFlags&PW_UPPERS, pwFlags&PW_SYMBOLS) + return chars +} - fmt.Printf("%v\n%v\n%v\n%v", - pwds[0:4], - pwds[4:8], - pwds[8:12], - pwds[12:16], - ) +func pwRand(buf *string, pwOptions *pwOptions, chars string) ([]string, error) { + if len(chars) == 0 { + return nil, errors.New("no available chars for generating password") + } + + rand.Seed(time.Now().UnixNano()) + + var wg sync.WaitGroup + wg.Add(pwOptions.numPw) + + passwords := make([]string, pwOptions.numPw) + for i := range passwords { + go func(i int) { + defer wg.Done() + passwords[i] = string(randomize(pwOptions.pwLen, chars)) + }(i) + } + wg.Wait() + + return passwords, nil } diff --git a/subcommand.go b/subcommand.go new file mode 100644 index 0000000..51a847b --- /dev/null +++ b/subcommand.go @@ -0,0 +1,160 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +type cmdOption func(c *baseCommand) +type baseCommand struct { + cmds cmdableSlice + cmdsMap map[string]interface{} + usage func() +} + +type cmdableSlice []cmdable + +func NewItems(cmds ...cmdable) []cmdable { + return cmds +} + +func NewCommandController(cmds []cmdable, opts ...cmdOption) *baseCommand { + c := &baseCommand{cmds: cmds, cmdsMap: make(map[string]interface{})} + + for i, opt := range cmds { + switch targetOpt := opt.(type) { + case *boolCmd: + targetOpt.ptr = flag.Bool(targetOpt.name, targetOpt.value, targetOpt.description) + if targetOpt.alias != "" { + flag.BoolVar(targetOpt.ptr, targetOpt.alias, *targetOpt.ptr, "") + } + case *stringCmd: + targetOpt.ptr = flag.String(targetOpt.name, targetOpt.value, targetOpt.description) + if targetOpt.alias != "" { + flag.StringVar(targetOpt.ptr, targetOpt.alias, *targetOpt.ptr, "") + } + } + c.cmds[i] = opt + } + + for _, opt := range opts { + opt(c) + } + + return c +} + +func WithUsageHeader(header string) cmdOption { + return func(c *baseCommand) { + c.usage = func() { + fmt.Fprintln(os.Stderr, header) + c.cmds.PrintDefaults() + os.Exit(0) + } + } +} + +func (c *baseCommand) Usage() { + c.usage() +} + +func (c *baseCommand) Ready() { + flag.Parse() + + for _, opt := range c.cmds { + switch targetOpt := opt.(type) { + case *boolCmd: + c.cmdsMap[opt.Name()] = *targetOpt.ptr + case *stringCmd: + c.cmdsMap[opt.Name()] = *targetOpt.ptr + } + } +} + +func (c *baseCommand) GetBool(n string) bool { + return c.cmdsMap[n].(bool) +} + +func (c *baseCommand) GetString(n string) string { + return c.cmdsMap[n].(string) +} + +func (c cmdableSlice) GetBool(n string) bool { + return false +} + +func (c cmdableSlice) GetString(n string) string { + return "" +} + +func (s cmdableSlice) PrintDefaults() cmdableSlice { + for _, opt := range s { + fmt.Printf(" %s\n", opt.Usage()) + } + + return s +} + +func (s cmdableSlice) AndExit(code int) { + os.Exit(code) +} + +type cmdable interface { + Name() string + Usage() string +} + +type Option struct { + name string + alias string + usage string + description string +} + +func (c *Option) Name() string { + return c.name +} + +func (c *Option) Alias() string { + return c.alias +} + +func (c *Option) Usage() string { + switch { + case c.usage != "": + return fmt.Sprintf("%s\n %s", c.usage, c.description) + case c.alias != "": + return fmt.Sprintf("-%s or -%s\n %s", c.alias, c.name, c.description) + default: + return fmt.Sprintf("-%s\n %s", c.name, c.description) + } +} + +func (c *Option) Description() string { + return c.description +} + +type boolCmd struct { + Option + value bool + ptr *bool +} + +func NewBoolCommand(opt Option) cmdable { + return &boolCmd{ + Option: opt, + } +} + +type stringCmd struct { + Option + value string + ptr *string +} + +func NewStringCommand(opt Option) cmdable { + return &stringCmd{ + Option: opt, + } +}