add password generator

This commit is contained in:
2025-03-07 12:33:40 -05:00
parent 116be25489
commit 0d386e2d9f
27 changed files with 17895 additions and 42 deletions

View File

@@ -7,6 +7,7 @@ import (
"maxwarden/users"
"sort"
"strings"
"time"
)
type Secret struct {
@@ -16,6 +17,8 @@ type Secret struct {
Notes string
Username string
Password string
Created time.Time
Modified time.Time
}
type EntryFilter struct {
@@ -123,3 +126,53 @@ func DeleteSecret(userId int32, masterKey string, secretId string) error {
return userErr
}
func Update(userId int32, masterKey string, secret Secret) error {
user, _ := users.FetchById(userId)
secrets, _ := security.DecryptDataWithKey[[]Secret](user.Data, masterKey)
if secrets == nil {
return errors.New("user secrets are null")
}
secret.Modified = time.Now()
// linear search and replace
for i, v := range *secrets {
if v.ID == secret.ID {
created := (*secrets)[i].Created
secret.Created = created
(*secrets)[i] = secret
}
}
enc, _ := security.EncryptDataWithKey(secrets, masterKey)
user.Data = enc
_, updateErr := users.Update(user)
return updateErr
}
func Add(userId int32, masterKey string, secret Secret) error {
user, _ := users.FetchById(userId)
secrets, _ := security.DecryptDataWithKey[[]Secret](user.Data, masterKey)
if secrets == nil {
return errors.New("user secrets are null")
}
secret.ID = security.RandBase58String(32)
secret.Modified = time.Now()
secret.Created = time.Now()
*secrets = append(*secrets, secret)
enc, _ := security.EncryptDataWithKey(secrets, masterKey)
user.Data = enc
_, updateErr := users.Update(user)
return updateErr
}

13
generator/passphrase.go Normal file
View File

@@ -0,0 +1,13 @@
package generator
import (
"strings"
"github.com/sethvargo/go-diceware/diceware"
)
func GeneratePassphrase(words int) string {
list, _ := diceware.Generate(words)
return strings.Join(list, " ")
}

26
generator/password.go Normal file
View File

@@ -0,0 +1,26 @@
package generator
import "github.com/sethvargo/go-password/password"
func GeneratePassword(length int, digits int, symbols int, disableUpper bool, allowRepeats bool) string {
if length <= 0 {
length = 16
}
if digits <= 0 {
digits = 0
}
if symbols <= 0 {
symbols = 0
}
result, _ := password.Generate(length, digits, symbols, disableUpper, allowRepeats)
return result
}
func GenerateDefault() string {
result, _ := password.Generate(24, 5, 5, false, false)
return result
}

2
go.mod
View File

@@ -30,6 +30,8 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sethvargo/go-diceware v0.5.0 // indirect
github.com/sethvargo/go-password v0.3.1 // indirect
github.com/stretchr/testify v1.10.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect

4
go.sum
View File

@@ -70,6 +70,10 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sethvargo/go-diceware v0.5.0 h1:exrQ7GpaBo00GqRVM1N8ChXSsi3oS7tjQiIehsD+yR0=
github.com/sethvargo/go-diceware v0.5.0/go.mod h1:Lg1SyPS7yQO6BBgTN5r4f2MUDkqGfLWsOjHPY0kA8iw=
github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=

View File

@@ -2,9 +2,7 @@ package app
import (
"maxwarden/entries"
"maxwarden/security"
. "maxwarden/ui"
"maxwarden/users"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
@@ -56,6 +54,7 @@ func EditorHandler(w http.ResponseWriter, r *http.Request) {
url := r.FormValue("url")
secret = entries.Secret{
ID: r.PathValue("id"),
Description: desc,
URL: url,
Notes: notes,
@@ -63,42 +62,26 @@ func EditorHandler(w http.ResponseWriter, r *http.Request) {
Username: username,
}
user, _ := users.FetchById(identity.UserID)
// Get current secret store
secrets, _ := security.DecryptDataWithKey[[]entries.Secret](user.Data, identity.MasterKey)
if secrets == nil {
http.Redirect(w, r, "/app", http.StatusFound)
return
}
if editorType == EDITOR_TYPE_ADD {
secret.ID = security.RandBase58String(32)
*secrets = append(*secrets, secret)
entries.Add(identity.UserID, identity.MasterKey, secret)
} else {
secret.ID = r.PathValue("id")
// linear search and replace
for i, v := range *secrets {
if v.ID == secret.ID {
(*secrets)[i] = secret
entries.Update(identity.UserID, identity.MasterKey, secret)
}
}
}
// Serialize and encrypt modified store using master key
enc, _ := security.EncryptDataWithKey(secrets, identity.MasterKey)
user.Data = enc
users.Update(user)
http.Redirect(w, r, "/app", http.StatusFound)
return
}
AppLayout(title, *identity, session,
Modal(
"password_generator",
Text("Passkey Generator"),
HxLoad("/app/generator-hx"),
[]Node {
ButtonUIOutline(ModalCloser(), Text("Close")),
},
),
If(editorType == EDITOR_TYPE_EDIT,
Group{
Modal(
@@ -106,7 +89,7 @@ func EditorHandler(w http.ResponseWriter, r *http.Request) {
Text("Warning!"),
Text("Are you sure you want to delete this entry? This action cannot be undone."),
[]Node{
A(Href("/app/delete/" + secret.ID), ButtonUIDanger(Text("Delete"))),
A(Href("/app/delete/"+secret.ID), ButtonUIDanger(Text("Delete"))),
ButtonUIOutline(ModalCloser(), Text("Close")),
},
),
@@ -119,16 +102,41 @@ func EditorHandler(w http.ResponseWriter, r *http.Request) {
Form(
AutoComplete("off"),
Method("POST"),
FormLabel(Text("Description")),
FormInput(Type("text"), Name("description"), Value(secret.Description)),
FormInput(Type("text"), Name("description"), Value(secret.Description), AutoFocus(), Required()),
Br(),
FormLabel(Text("Username")),
FormInput(Type("text"), Name("un"), Value(secret.Username)),
Br(),
Div(
FlexLeftRight(
FormLabel(Text("Password")),
FormInput(Type("password"), Name("pas"), Value(secret.Password)),
ModalActuator("password_generator", ButtonUI(Type("button"), Text("Generate a secure password"))),
),
Br(),
FormInput(Class("password"), Type("password"), Name("pas"), Value(secret.Password)),
Br(),
FormLabel(Text("Show password?"), For("show")),
FormCheck(Class("checkbox"), ID("show")),
InlineScript(`
let check = me(".checkbox", me());
let passInput = me(".password", me());
check.on("click", () => {
if (passInput.type === "password") {
passInput.type = "text";
} else if (passInput.type === "text") {
passInput.type = "password";
}
});
`),
),
Br(),
FormLabel(Text("URL")),
@@ -144,6 +152,15 @@ func EditorHandler(w http.ResponseWriter, r *http.Request) {
ButtonUISuccess(Text(btnLabel), Type("submit")),
A(Href("/app"), ButtonUIOutline(Text("Close"), Type("button"))),
),
If(editorType == EDITOR_TYPE_EDIT,
Div(
InlineStyle("$me { margin-top: $3; color: $color(neutral-400); font-size: var(--text-sm);}"),
Text("Last modified: "), FormatDateTime(secret.Modified),
Br(),
Text("Created: "), FormatDateTime(secret.Created),
),
),
),
).Render(w)
}

17
handlers/app/generator.go Normal file
View File

@@ -0,0 +1,17 @@
package app
import (
"net/http"
"maxwarden/middleware"
. "maxwarden/ui"
)
func GeneratorHandler(w http.ResponseWriter, r *http.Request) {
identity := middleware.GetIdentity(r)
session := middleware.GetSession(r)
AppLayout("Passkey Generator", *identity, session,
HxLoad("/app/generator-hx"),
).Render(w)
}

View File

@@ -0,0 +1,114 @@
package app
import (
"maxwarden/generator"
. "maxwarden/ui"
"net/http"
"strconv"
. "maxwarden/basic"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
hx "maragu.dev/gomponents-htmx"
)
func GeneratorHxHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
wordCount, _ := strconv.Atoi(r.FormValue("wordCount"))
if wordCount <= 0 {
wordCount = 6
}
phraseOutput := generator.GeneratePassphrase(wordCount)
length, _ := strconv.Atoi(r.FormValue("length"))
if length <= 0 {
length = 24
}
numbers, _ := strconv.Atoi(r.FormValue("numbers"))
if numbers <= 0 {
numbers = 5
}
symbols, _ := strconv.Atoi(r.FormValue("symbols"))
if symbols <= 0 {
symbols = 5
}
passwordOutput := generator.GeneratePassword(length, numbers, symbols, false, true)
customFormInput := func(children ...Node) Node {
return Div(
InlineStyle(`
$me {
border: 1px solid $color(neutral-300);
margin-bottom: $3;
}
`),
FormInput(
Group(children),
),
)
}
Div(ID("generator"),
Card(
Heading("Generate Passphrase"),
Form(
hx.Post(r.URL.Path),
hx.Swap("outerHTML"),
hx.Target("#generator"),
FormLabel(Text("Word Count")),
customFormInput(Type("number"), Name("wordCount"), Value(ToString(wordCount))),
ButtonUI(Text("Generate"), Type("submit")),
),
Br(),
FormLabel(Text("Output")),
customFormInput(
Value(phraseOutput),
),
),
Br(),
Card(
Heading("Generate Password"),
Form(
hx.Post(r.URL.Path),
hx.Swap("outerHTML"),
hx.Target("#generator"),
Flex(
Div(
FormLabel(Text("Password length")),
customFormInput(Type("number"), Name("length"), Value(ToString(length))),
),
Div(
FormLabel(Text("Number Count")),
customFormInput(Type("number"), Name("numbers"), Value(ToString(numbers))),
),
Div(
FormLabel(Text("Symbol Count")),
customFormInput(Type("number"), Name("symbols"), Value(ToString(symbols))),
),
),
ButtonUI(Text("Generate"), Type("submit")),
),
Br(),
FormLabel(Text("Output")),
customFormInput(
Value(passwordOutput),
),
),
).Render(w)
}

View File

@@ -25,7 +25,7 @@ func VaultHandler(w http.ResponseWriter, r *http.Request) {
margin-bottom: $5;
}
`),
A(Href("/app/editor/add"), ButtonUI(Text("+ Add Item"))),
A(Href("/app/editor/add"), ButtonUI(Text("+ Add Credentials"))),
),
HxLoad("/app/vault-hx"),
).Render(w)

View File

@@ -72,10 +72,58 @@ func VaultHxHandler(w http.ResponseWriter, r *http.Request) {
func(entry entries.Secret) Node {
return Tr(
TdLeft(Text(entry.Description)),
TdLeft(Text(entry.Username)),
TdLeft(Text("********")),
TdLeft(PageLink(security.SanitizationPolicy.Sanitize(entry.URL), Text(entry.URL), false)),
TdCenter(A(Href("/app/editor/edit/" + entry.ID), ButtonUIOutline(Icon(ICON_PENCIL, 16)))),
TdLeft(
IfElse(entry.Username != "",
Flex(
P(Text(entry.Username)),
Span(
Title("Click to copy username"),
InlineStyle("$me { cursor: pointer; }"),
Icon(ICON_COPY, 16),
),
InlineScript(`
let btn = me("span", me());
let text = me("p", me());
btn.on("click", () => {
navigator.clipboard.writeText(text.innerHTML);
});
`),
),
Text("---"),
),
),
TdLeft(
IfElse(entry.Password != "",
Flex(
P(Text("•••••••")),
Input(Type("hidden"), Value(entry.Password)),
Span(
Class("copy"),
Title("Click to copy password"),
InlineStyle("$me { cursor: pointer; }"),
Icon(ICON_COPY, 16),
),
InlineScript(`
let copyBtn = me(".copy", me());
let password = me("input", me()).value;
copyBtn.on("click", () => {
navigator.clipboard.writeText(password);
});
`),
),
Text("---"),
),
),
TdLeft(
IfElse(entry.URL != "",
PageLink(security.SanitizationPolicy.Sanitize(entry.URL), Text(entry.URL), true),
Text("---"),
),
),
TdCenter(A(Href("/app/editor/edit/"+entry.ID), ButtonUIOutline(Icon(ICON_PENCIL, 16)))),
)
},
nil,

View File

@@ -27,7 +27,7 @@ var NavGroups = []NavGroup{
{
Title: "Tools",
SubGroup: []NavGroup{
{SectionId: LAYOUT_SECTION_TOOLS, Title: "Password Generator", URL: "/app/examples/forms"},
{SectionId: LAYOUT_SECTION_TOOLS, Title: "Passkey Generator", URL: "/app/generator"},
},
},
}

View File

@@ -1,6 +1,8 @@
package ui
import (
"strconv"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
@@ -74,3 +76,23 @@ func FormTextarea(children ...Node) Node {
func FormLabel(children ...Node) Node {
return Label(InlineStyle("$me { color: $color(neutral-900); font-size: var(--text-sm); } "), Group(children))
}
func FormCheck(children ...Node) Node {
return Input(
InlineStyle(`
$me {
margin-left: $3;
}
`),
Type("checkbox"),
Group(children),
)
}
func FormSlider(min int, max int, children ...Node) Node {
return Input(
Type("range"),
Min(strconv.Itoa(min)),
Max(strconv.Itoa(max)),
)
}

View File

@@ -59,6 +59,13 @@ func Flex(n ...Node) Node {
)
}
func FlexLeftRight(n ...Node) Node {
return Div(
InlineStyle("$me { display: flex; align-items: center; flex-direction: row; gap: $3; justify-content: space-between; }"),
Group(n),
)
}
func CardNoPadding(body ...Node) Node {
return Div(
InlineStyle(`
@@ -109,6 +116,12 @@ func ToText(i interface{}) Node {
}
// TEXT
func Heading(text string) Node {
return H1(
InlineStyle("$me { font-weight: bold; font-size: var(--text-2xl); letter-spacing: var(--tracking-tight); color: $color(black); }"),
Text(text),
)
}
func PageLink(location string, display Node, newPage bool) Node {
return A(
Href(location),

View File

@@ -38,6 +38,7 @@ const (
ICON_X_DIALOG_CLOSE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="1.4" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>`
ICON_PENCIL = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>`
ICON_LOCK_KEYHOLE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lock-keyhole"><circle cx="12" cy="16" r="1"/><rect x="3" y="10" width="18" height="12" rx="2"/><path d="M7 10V7a5 5 0 0 1 10 0v3"/></svg>`
ICON_COPY = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`
//material
ICON_SEARCH = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" class="w-8 h-8 text-slate-600"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /></svg>`

20
vendor/github.com/sethvargo/go-diceware/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,20 @@
Copyright 2017 Seth Vargo <seth@sethvargo.com>
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.

View File

@@ -0,0 +1,195 @@
package diceware
import (
"crypto/rand"
"fmt"
"io"
"math"
"math/big"
)
// sides is the number of sides on a die.
var sides = big.NewInt(6)
var _ DicewareGenerator = (*Generator)(nil)
// Generator is the stateful generator which can be used to customize the word
// list and other generation options.
type Generator struct {
wordList WordList
randReader io.Reader
}
// GeneratorInput is used as input to the NewGenerator function.
type GeneratorInput struct {
// WordList is the word list to use. There are built-in word lists like
// WordListEffBig (default), WordListEffSmall, and WordListOriginal. You can
// also bring your own word list by implementing the WordList interface.
WordList WordList
// RandReader is an optional reader to use in place of the default
// (crypto/rand.Reader), which can be used to generate repeatable sets of
// words
RandReader io.Reader
}
// NewGenerator creates a new Generator from the specified configuration. If no
// input is given, all the default values are used. This function is safe for
// concurrent use.
func NewGenerator(i *GeneratorInput) (*Generator, error) {
if i == nil {
i = new(GeneratorInput)
}
if i.WordList == nil {
i.WordList = WordListEffLarge()
}
gen := &Generator{
wordList: i.WordList,
randReader: i.RandReader,
}
if gen.randReader == nil {
gen.randReader = rand.Reader
}
return gen, nil
}
// Generate generates a collection of diceware words, specified by the numWords
// parameter.
//
// The algorithm is fast, but it's not designed to be performant, favoring
// entropy over speed.
//
// This function is safe for concurrent use, but there is a possibility of
// concurrent invocations generating overlapping words. To generate multiple
// non-overlapping words, use a single invocation of the function and split the
// resulting string list.
func (g *Generator) Generate(numWords int) ([]string, error) {
if typ, ok := g.wordList.(WordListNumWordser); ok {
if l := typ.NumWords(); numWords > l {
return nil, fmt.Errorf("number of requested words (%d) cannot exceed the size of the wordlist (%d)",
numWords, l)
}
}
list := make([]string, 0, numWords)
seen := make(map[string]struct{}, numWords)
for i := 0; i < numWords; i++ {
n, err := g.RollWord(g.wordList.Digits())
if err != nil {
return nil, err
}
word := g.wordList.WordAt(n)
if _, ok := seen[word]; ok {
i--
continue
}
list = append(list, word)
seen[word] = struct{}{}
}
return list, nil
}
// MustGenerate is the same as Generate, but panics on error.
func (g *Generator) MustGenerate(numWords int) []string {
list, err := g.Generate(numWords)
if err != nil {
panic(err)
}
return list
}
// Generate - see Generator.Generate for usage.
func Generate(numWords int) ([]string, error) {
gen, err := NewGenerator(nil)
if err != nil {
return nil, err
}
return gen.Generate(numWords)
}
// MustGenerate - see Generator.MustGenerate for usage.
func MustGenerate(numWords int) []string {
gen, err := NewGenerator(nil)
if err != nil {
panic(err)
}
return gen.MustGenerate(numWords)
}
// GenerateWithWordList generates a list of the given number of words from the
// given word list.
func GenerateWithWordList(numWords int, wordList WordList) ([]string, error) {
gen, err := NewGenerator(&GeneratorInput{
WordList: wordList,
})
if err != nil {
return nil, err
}
return gen.Generate(numWords)
}
// WordAt retrieves the word at the given index from EFF's large wordlist.
//
// Deprecated: Use WordList.WordAt instead.
func WordAt(i int) string {
return WordListEffLarge().WordAt(i)
}
// RollDie rolls a single 6-sided die and returns a value between [1,6].
//
// Internally this creates a new Generator with a nil configuration and calls
// Generator.RollDie.
func RollDie() (int, error) {
gen, err := NewGenerator(nil)
if err != nil {
return 0, err
}
return gen.RollDie()
}
// RollWord rolls and aggregates dice to represent one word in the list. The
// result is the index of the word in the list.
//
// Internally this creates a new Generator with a nil configuration and calls
// Generator.RollWord.
func RollWord(d int) (int, error) {
gen, err := NewGenerator(nil)
if err != nil {
return 0, err
}
return gen.RollWord(d)
}
// RollDie rolls a single 6-sided die and returns a value between [1,6].
func (g *Generator) RollDie() (int, error) {
r, err := rand.Int(g.randReader, sides)
if err != nil {
return 0, fmt.Errorf("failed to generate a random number: %w", err)
}
return int(r.Int64()) + 1, nil
}
// RollWord rolls and aggregates dice to represent one word in the list. The
// result is the index of the word in the list.
func (g *Generator) RollWord(d int) (int, error) {
var final int
for i := d; i > 0; i-- {
res, err := g.RollDie()
if err != nil {
return 0, err
}
final += res * int(math.Pow(10, float64(i-1)))
}
return final, nil
}

View File

@@ -0,0 +1,6 @@
package diceware
type DicewareGenerator interface {
Generate(int) ([]string, error)
MustGenerate(int) []string
}

View File

@@ -0,0 +1,38 @@
package diceware
var _ DicewareGenerator = (*mockGenerator)(nil)
type mockGenerator struct {
result []string
err error
}
// NewMockGenerator creates a new generator that satisfies the DicewareGenerator
// interface. If an error is provided, the error is returned. If a result if
// provided, the result is always returned, regardless of what parameters are
// passed into the Generate or MustGenerate methods.
//
// This function is most useful for tests where you want to have predicable
// results for a transitive resource that depends on go-diceware.
func NewMockGenerator(result []string, err error) *mockGenerator {
return &mockGenerator{
result: result,
err: err,
}
}
// Generate returns the mocked result or error.
func (g *mockGenerator) Generate(int) ([]string, error) {
if g.err != nil {
return nil, g.err
}
return g.result, nil
}
// MustGenerate returns the mocked result or panics if an error was given.
func (g *mockGenerator) MustGenerate(int) []string {
if g.err != nil {
panic(g.err)
}
return g.result
}

View File

@@ -0,0 +1,42 @@
package diceware
// WordList is an interface that must be implemented to be considered a word
// list for use in the diceware algorithm. This interface can be implemented by
// other libraries.
type WordList interface {
// Digits is the number of digits for indexes in the word list. This
// determines the number of dice rolls.
Digits() int
// WordAt returns the word at the given integer in the word list.
WordAt(int) string
}
// WordListNumWordser is an auxiliary interface that returns the number of words
// in the list. This is a separate interface for backwards compatibility.
type WordListNumWordser interface {
// NumWords returns the total number of words in the list.
NumWords() int
}
var (
_ WordList = (*wordListInternal)(nil)
_ WordListNumWordser = (*wordListInternal)(nil)
)
type wordListInternal struct {
digits int
words map[int]string
}
func (w *wordListInternal) Digits() int {
return w.digits
}
func (w *wordListInternal) WordAt(i int) string {
return w.words[i]
}
func (w *wordListInternal) NumWords() int {
return len(w.words)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

20
vendor/github.com/sethvargo/go-password/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,20 @@
Copyright 2017 Seth Vargo <seth@sethvargo.com>
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.

View File

@@ -0,0 +1,262 @@
// Package password provides a library for generating high-entropy random
// password strings via the crypto/rand package.
//
// res, err := Generate(64, 10, 10, false, false)
// if err != nil {
// log.Fatal(err)
// }
// log.Printf(res)
//
// Most functions are safe for concurrent use.
package password
import (
"crypto/rand"
"errors"
"fmt"
"io"
"math/big"
"strings"
)
// Built-time checks that the generators implement the interface.
var _ PasswordGenerator = (*Generator)(nil)
// PasswordGenerator is an interface that implements the Generate function. This
// is useful for testing where you can pass this interface instead of a real
// password generator to mock responses for predicability.
type PasswordGenerator interface {
Generate(int, int, int, bool, bool) (string, error)
MustGenerate(int, int, int, bool, bool) string
}
const (
// LowerLetters is the list of lowercase letters.
LowerLetters = "abcdefghijklmnopqrstuvwxyz"
// UpperLetters is the list of uppercase letters.
UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// Digits is the list of permitted digits.
Digits = "0123456789"
// Symbols is the list of symbols.
Symbols = "~!@#$%^&*()_+`-={}|[]\\:\"<>?,./"
)
var (
// ErrExceedsTotalLength is the error returned with the number of digits and
// symbols is greater than the total length.
ErrExceedsTotalLength = errors.New("number of digits and symbols must be less than total length")
// ErrLettersExceedsAvailable is the error returned with the number of letters
// exceeds the number of available letters and repeats are not allowed.
ErrLettersExceedsAvailable = errors.New("number of letters exceeds available letters and repeats are not allowed")
// ErrDigitsExceedsAvailable is the error returned with the number of digits
// exceeds the number of available digits and repeats are not allowed.
ErrDigitsExceedsAvailable = errors.New("number of digits exceeds available digits and repeats are not allowed")
// ErrSymbolsExceedsAvailable is the error returned with the number of symbols
// exceeds the number of available symbols and repeats are not allowed.
ErrSymbolsExceedsAvailable = errors.New("number of symbols exceeds available symbols and repeats are not allowed")
)
// Generator is the stateful generator which can be used to customize the list
// of letters, digits, and/or symbols.
type Generator struct {
lowerLetters string
upperLetters string
digits string
symbols string
reader io.Reader
}
// GeneratorInput is used as input to the NewGenerator function.
type GeneratorInput struct {
LowerLetters string
UpperLetters string
Digits string
Symbols string
Reader io.Reader // rand.Reader by default
}
// NewGenerator creates a new Generator from the specified configuration. If no
// input is given, all the default values are used. This function is safe for
// concurrent use.
func NewGenerator(i *GeneratorInput) (*Generator, error) {
if i == nil {
i = new(GeneratorInput)
}
g := &Generator{
lowerLetters: i.LowerLetters,
upperLetters: i.UpperLetters,
digits: i.Digits,
symbols: i.Symbols,
reader: i.Reader,
}
if g.lowerLetters == "" {
g.lowerLetters = LowerLetters
}
if g.upperLetters == "" {
g.upperLetters = UpperLetters
}
if g.digits == "" {
g.digits = Digits
}
if g.symbols == "" {
g.symbols = Symbols
}
if g.reader == nil {
g.reader = rand.Reader
}
return g, nil
}
// Generate generates a password with the given requirements. length is the
// total number of characters in the password. numDigits is the number of digits
// to include in the result. numSymbols is the number of symbols to include in
// the result. noUpper excludes uppercase letters from the results. allowRepeat
// allows characters to repeat.
//
// The algorithm is fast, but it's not designed to be performant; it favors
// entropy over speed. This function is safe for concurrent use.
func (g *Generator) Generate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) (string, error) {
letters := g.lowerLetters
if !noUpper {
letters += g.upperLetters
}
chars := length - numDigits - numSymbols
if chars < 0 {
return "", ErrExceedsTotalLength
}
if !allowRepeat && chars > len(letters) {
return "", ErrLettersExceedsAvailable
}
if !allowRepeat && numDigits > len(g.digits) {
return "", ErrDigitsExceedsAvailable
}
if !allowRepeat && numSymbols > len(g.symbols) {
return "", ErrSymbolsExceedsAvailable
}
var result string
// Characters
for i := 0; i < chars; i++ {
ch, err := randomElement(g.reader, letters)
if err != nil {
return "", err
}
if !allowRepeat && strings.Contains(result, ch) {
i--
continue
}
result, err = randomInsert(g.reader, result, ch)
if err != nil {
return "", err
}
}
// Digits
for i := 0; i < numDigits; i++ {
d, err := randomElement(g.reader, g.digits)
if err != nil {
return "", err
}
if !allowRepeat && strings.Contains(result, d) {
i--
continue
}
result, err = randomInsert(g.reader, result, d)
if err != nil {
return "", err
}
}
// Symbols
for i := 0; i < numSymbols; i++ {
sym, err := randomElement(g.reader, g.symbols)
if err != nil {
return "", err
}
if !allowRepeat && strings.Contains(result, sym) {
i--
continue
}
result, err = randomInsert(g.reader, result, sym)
if err != nil {
return "", err
}
}
return result, nil
}
// MustGenerate is the same as Generate, but panics on error.
func (g *Generator) MustGenerate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) string {
res, err := g.Generate(length, numDigits, numSymbols, noUpper, allowRepeat)
if err != nil {
panic(err)
}
return res
}
// Generate is the package shortcut for Generator.Generate.
func Generate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) (string, error) {
gen, err := NewGenerator(nil)
if err != nil {
return "", err
}
return gen.Generate(length, numDigits, numSymbols, noUpper, allowRepeat)
}
// MustGenerate is the package shortcut for Generator.MustGenerate.
func MustGenerate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) string {
res, err := Generate(length, numDigits, numSymbols, noUpper, allowRepeat)
if err != nil {
panic(err)
}
return res
}
// randomInsert randomly inserts the given value into the given string.
func randomInsert(reader io.Reader, s, val string) (string, error) {
if s == "" {
return val, nil
}
n, err := rand.Int(reader, big.NewInt(int64(len(s)+1)))
if err != nil {
return "", fmt.Errorf("failed to generate random integer: %w", err)
}
i := n.Int64()
return s[0:i] + val + s[i:], nil
}
// randomElement extracts a random element from the given string.
func randomElement(reader io.Reader, s string) (string, error) {
n, err := rand.Int(reader, big.NewInt(int64(len(s))))
if err != nil {
return "", fmt.Errorf("failed to generate random integer: %w", err)
}
return string(s[n.Int64()]), nil
}

View File

@@ -0,0 +1,39 @@
package password
// Built-time checks that the generators implement the interface.
var _ PasswordGenerator = (*mockGenerator)(nil)
type mockGenerator struct {
result string
err error
}
// NewMockGenerator creates a new generator that satisfies the PasswordGenerator
// interface. If an error is provided, the error is returned. If a result if
// provided, the result is always returned, regardless of what parameters are
// passed into the Generate or MustGenerate methods.
//
// This function is most useful for tests where you want to have predicable
// results for a transitive resource that depends on go-password.
func NewMockGenerator(result string, err error) *mockGenerator {
return &mockGenerator{
result: result,
err: err,
}
}
// Generate returns the mocked result or error.
func (g *mockGenerator) Generate(int, int, int, bool, bool) (string, error) {
if g.err != nil {
return "", g.err
}
return g.result, nil
}
// MustGenerate returns the mocked result or panics if an error was given.
func (g *mockGenerator) MustGenerate(int, int, int, bool, bool) string {
if g.err != nil {
panic(g.err)
}
return g.result
}

6
vendor/modules.txt vendored
View File

@@ -78,6 +78,12 @@ github.com/pmezard/go-difflib/difflib
# github.com/robfig/cron/v3 v3.0.1
## explicit; go 1.12
github.com/robfig/cron/v3
# github.com/sethvargo/go-diceware v0.5.0
## explicit; go 1.22
github.com/sethvargo/go-diceware/diceware
# github.com/sethvargo/go-password v0.3.1
## explicit; go 1.21
github.com/sethvargo/go-password/password
# github.com/stretchr/testify v1.10.0
## explicit; go 1.17
github.com/stretchr/testify/assert

File diff suppressed because one or more lines are too long