diff --git a/go.mod b/go.mod index 754dc4d..601de7d 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module code.jhot.me/jhot/hats go 1.21.1 require ( + ekyu.moe/base91 v0.2.3 + github.com/aead/skein v0.0.0-20160722084837-9365ae6e95d2 github.com/gorilla/websocket v1.5.0 - github.com/nats-io/nats.go v1.30.2 + github.com/nats-io/nats.go v1.34.1 ) require ( @@ -24,16 +26,13 @@ require ( github.com/go-chi/render v1.0.3 github.com/go-co-op/gocron v1.35.2 github.com/go-resty/resty/v2 v2.9.1 - github.com/golang/protobuf v1.5.3 // indirect github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd github.com/icholy/digest v0.1.22 - github.com/klauspost/compress v1.17.0 // indirect - github.com/nats-io/nats-server/v2 v2.10.2 // indirect - github.com/nats-io/nkeys v0.4.5 // indirect + github.com/klauspost/compress v1.17.2 // indirect + github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/samber/lo v1.38.1 - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index bfe3cec..dce6699 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +ekyu.moe/base91 v0.2.3 h1:1jCZrrpWjDSMMjjU9LANfQV+n7EHeW0OQ0MO9fpDRHg= +ekyu.moe/base91 v0.2.3/go.mod h1:/qmmaFUj5d0p9xcpj8beZDj33yXrc54eGU+hO/V5vuo= +github.com/aead/skein v0.0.0-20160722084837-9365ae6e95d2 h1:q5TSngwXJdajCyZPQR+eKyRRgI3/ZXC/Nq1ZxZ4Zxu8= +github.com/aead/skein v0.0.0-20160722084837-9365ae6e95d2/go.mod h1:4JBZEId5BaLqvA2DGU53phvwkn2WpeLhNSF79/uKBPs= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -16,13 +20,9 @@ github.com/go-co-op/gocron v1.35.2 h1:lG3rdA9TqBBC/PtT2ukQqgLm6jEepnAzz3+OQetvPT github.com/go-co-op/gocron v1.35.2/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= github.com/go-resty/resty/v2 v2.9.1 h1:PIgGx4VrHvag0juCJ4dDv3MiFRlDmP0vicBucwf+gLM= github.com/go-resty/resty/v2 v2.9.1/go.mod h1:4/GYJVjh9nhkhGR6AUNW3XhpDYNUr+Uvy9gV/VGZIy4= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd h1:PppHBegd3uPZ3Y/Iax/2mlCFJm1w4Qf/zP1MdW4ju2o= github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= @@ -31,24 +31,18 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/icholy/digest v0.1.22 h1:dRIwCjtAcXch57ei+F0HSb5hmprL873+q7PoVojdMzM= github.com/icholy/digest v0.1.22/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= -github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= -github.com/nats-io/jwt/v2 v2.5.2 h1:DhGH+nKt+wIkDxM6qnVSKjokq5t59AZV5HRcFW0zJwU= -github.com/nats-io/jwt/v2 v2.5.2/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= -github.com/nats-io/nats-server/v2 v2.10.2 h1:2o/OOyc/dxeMCQtrF1V/9er0SU0A3LKhDlv/+rqreBM= -github.com/nats-io/nats-server/v2 v2.10.2/go.mod h1:lzrskZ/4gyMAh+/66cCd+q74c6v7muBypzfWhP/MAaM= -github.com/nats-io/nats.go v1.30.2 h1:aloM0TGpPorZKQhbAkdCzYDj+ZmsJDyeo3Gkbr72NuY= -github.com/nats-io/nats.go v1.30.2/go.mod h1:dcfhUgmQNN4GJEfIb2f9R7Fow+gzBF4emzDHrVBd5qM= -github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk= -github.com/nats-io/nkeys v0.4.5/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= +github.com/nats-io/nats.go v1.34.1 h1:syWey5xaNHZgicYBemv0nohUPPmaLteiBEUT6Q5+F/4= +github.com/nats-io/nats.go v1.34.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -77,8 +71,8 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -103,8 +97,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -115,8 +109,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -126,11 +121,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/api/api.go b/internal/api/api.go index fec0234..85b1a66 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "time" @@ -14,6 +15,7 @@ import ( "code.jhot.me/jhot/hats/pkg/config" "code.jhot.me/jhot/hats/pkg/homeassistant" ntfyPkg "code.jhot.me/jhot/hats/pkg/ntfy" + "code.jhot.me/jhot/hats/pkg/passgen" "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -72,6 +74,35 @@ func Listen(parentLogger *slog.Logger, parentConfig *config.HatsConfig, readme [ render.PlainText(w, r, "OK") }) + router.Get("/passgen", func(w http.ResponseWriter, r *http.Request) { + versionString := r.URL.Query().Get("version") + salt := r.URL.Query().Get("salt") + passphrase := r.URL.Query().Get("passphrase") + lengthString := r.URL.Query().Get("length") + + length, err := strconv.ParseInt(lengthString, 10, 64) + if err != nil { + length = 40 + } + + version := 2 + if versionString == "1" { + version = 1 + } + + pg := passgen.NewPasswordGenerator( + passgen.WithVersion(version), + passgen.WithSalt(salt), + passgen.WithPassphrase(passphrase), + passgen.WithLength(int(length))) + + if customSpecials := r.URL.Query().Get("specials"); customSpecials != "" { + pg.CustomSpecials = customSpecials + } + + render.PlainText(w, r, pg.Generate()) + }) + router.Route("/api", func(r chi.Router) { r.Use(authMiddleware) r.Get(`/state/{entityId}`, getEntityStateHandler) diff --git a/pkg/passgen/generator.go b/pkg/passgen/generator.go new file mode 100644 index 0000000..1c3575a --- /dev/null +++ b/pkg/passgen/generator.go @@ -0,0 +1,145 @@ +package passgen + +import ( + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" + "math/big" + "regexp" + "strconv" + "strings" + "unicode/utf16" + + "ekyu.moe/base91" + "github.com/aead/skein" +) + +func (p *Passgen) Generate() string { + switch p.Version { + case 1: + return p.V1() + default: + return p.V2() + } +} + +func (p *Passgen) V2() string { + charset := p.createCharset() + var out string + + for i := 0; true; i++ { + if len(out) >= p.Length { // password is long enough + if p.containsNecessaryCharacters(out[0:p.Length]) { // password contains all necessary character classes + break + } else { // password is missing character classes, keep second half and keep generating if necessary + out = out[int(len(out)/2):] + } + } + + passphrasePart := strings.Repeat(p.Passphrase, i+1) + hash := sha256.Sum256([]byte(passphrasePart + p.Salt)) + + // convert hash array of bytes into bigint + num := new(big.Int) + num.SetString(hex.EncodeToString(hash[:]), 16) + // encode bigint in custom character set + encoded := encodeToCustomCharset(num, charset) + + out = fmt.Sprintf("%s%s", out, encoded) + } + + return out[0:p.Length] +} + +func (p *Passgen) createCharset() string { + charset := "" + if !p.NoUppers { + charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + } + charset += "abcdefghijklmnopqrstuvwxyz" + if !p.NoNumbers { + charset += "0123456789" + } + if !p.NoSpecials { + charset += p.CustomSpecials + } + + return charset +} + +func (p *Passgen) containsNecessaryCharacters(password string) bool { + if !p.NoNumbers { + if matches, _ := regexp.MatchString("[0-9]", password); !matches { + return false + } + } + if !p.NoUppers { + if matches, _ := regexp.MatchString("[A-Z]", password); !matches { + return false + } + } + if !p.NoSpecials { + customSpecials := p.CustomSpecials + escapeChars := []string{"-", "[", "]", "^"} + for _, e := range escapeChars { + customSpecials = strings.ReplaceAll(customSpecials, e, "\\"+e) + } + if matches, _ := regexp.MatchString(fmt.Sprintf("[%s]", customSpecials), password); !matches { + return false + } + } + if matches, _ := regexp.MatchString("[a-z]", password); !matches { + return false + } + return true +} + +func encodeToCustomCharset(v *big.Int, charset string) string { + charsetLength := big.NewInt(int64(len(charset))) + var ret string + for { + if v.Cmp(big.NewInt(0)) == 0 { + break + } + remainder := new(big.Int).Mod(v, charsetLength) + v = new(big.Int).Div(v, charsetLength) + + ret = fmt.Sprintf("%c%s", charset[remainder.Uint64()], ret) + } + return ret +} + +func (p *Passgen) V1() string { + encoded := utf16.Encode([]rune(p.Passphrase + p.Salt)) + var hashOut [64]byte + skein.Sum512(&hashOut, convertUTF16ToLittleEndianBytes(encoded), nil) + hashHex := hex.EncodeToString(hashOut[:]) + + start := hexToInt(string(hashHex[0])) + hexToInt(string(hashHex[1])) + hexToInt(string(hashHex[2])) + end := start + p.Length + + encoded91 := base91.EncodeToString([]byte(hashHex)) + + for { + if len(encoded91) > end { + break + } + encoded91 = encoded91 + encoded91 + } + + return encoded91[start:end] +} + +func convertUTF16ToLittleEndianBytes(u []uint16) []byte { + b := make([]byte, 2*len(u)) + for index, value := range u { + binary.LittleEndian.PutUint16(b[index*2:], value) + } + return b +} + +func hexToInt(hexString string) int { + i, _ := strconv.ParseInt(hexString, 16, 0) + return int(i) +} diff --git a/pkg/passgen/options.go b/pkg/passgen/options.go new file mode 100644 index 0000000..2fab709 --- /dev/null +++ b/pkg/passgen/options.go @@ -0,0 +1,79 @@ +package passgen + +type Passgen struct { + Version int + Passphrase string + Salt string + Length int + NoUppers bool + NoNumbers bool + NoSpecials bool + CustomSpecials string +} + +type PassgenOption func(*Passgen) + +func WithVersion(version int) PassgenOption { + return func(p *Passgen) { + p.Version = version + } +} + +func WithPassphrase(passphrase string) PassgenOption { + return func(p *Passgen) { + p.Passphrase = passphrase + } +} + +func WithSalt(salt string) PassgenOption { + return func(p *Passgen) { + p.Salt = salt + } +} + +func WithLength(length int) PassgenOption { + return func(p *Passgen) { + p.Length = length + } +} + +func WithoutSpecials() PassgenOption { + return func(p *Passgen) { + p.NoSpecials = true + } +} + +func WithoutNumbers() PassgenOption { + return func(p *Passgen) { + p.NoNumbers = true + } +} + +func WithoutUppers() PassgenOption { + return func(p *Passgen) { + p.NoUppers = true + } +} + +func WithCustomSpecials(specials string) PassgenOption { + return func(p *Passgen) { + p.CustomSpecials = specials + } +} + +func NewPasswordGenerator(opts ...PassgenOption) *Passgen { + p := &Passgen{ + Version: 2, + Length: 40, + NoUppers: false, + NoNumbers: false, + NoSpecials: false, + CustomSpecials: "!@#$%^&*", + } + + for _, opt := range opts { + opt(p) + } + + return p +}