Skip to content

Commit

Permalink
Implementation
Browse files Browse the repository at this point in the history
- Standard features like pwgen
- Create LICENSE
- Add goroutines, optimize duplicated naming for CLI options
- Create content & GitAction badge for README
- Create go.yml
  • Loading branch information
vuon9 committed Dec 5, 2020
1 parent 996a311 commit 5cf68a8
Show file tree
Hide file tree
Showing 5 changed files with 492 additions and 21 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -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 ./...
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <chars> or --remove-chars=<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
```
246 changes: 225 additions & 21 deletions main.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 <chars> or --remove-chars=<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
}
Expand All @@ -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
}
Loading

0 comments on commit 5cf68a8

Please sign in to comment.