1
0
Fork 0
main
Jordan Hotmann 2024-05-13 14:42:52 -06:00
parent a9cb361e6b
commit e0fecbb317
No known key found for this signature in database
GPG Key ID: 01B504170C2A2EA3
5 changed files with 279 additions and 35 deletions

17
go.mod
View File

@ -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
)

42
go.sum
View File

@ -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=

View File

@ -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)

145
pkg/passgen/generator.go Normal file
View File

@ -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)
}

79
pkg/passgen/options.go Normal file
View File

@ -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
}