pax_global_header00006660000000000000000000000064146364062200014515gustar00rootroot0000000000000052 comment=631000f5f9d993c3242b416d0119823549c970c8 go-password-0.3.1/000077500000000000000000000000001463640622000137635ustar00rootroot00000000000000go-password-0.3.1/.github/000077500000000000000000000000001463640622000153235ustar00rootroot00000000000000go-password-0.3.1/.github/CONTRIBUTING.md000066400000000000000000000006241463640622000175560ustar00rootroot00000000000000# Contributing We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. go-password-0.3.1/.github/workflows/000077500000000000000000000000001463640622000173605ustar00rootroot00000000000000go-password-0.3.1/.github/workflows/test.yml000066400000000000000000000020721463640622000210630ustar00rootroot00000000000000name: Test on: push: branches: - main pull_request: branches: - main concurrency: group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' cancel-in-progress: true jobs: lint: runs-on: 'ubuntu-latest' steps: - uses: 'actions/checkout@v4' - uses: 'actions/setup-go@v5' with: cache: false go-version-file: 'go.mod' - uses: 'golangci/golangci-lint-action@v4' with: version: 'v1.57.2' skip-cache: true test: strategy: matrix: platform: - 'macos-latest' - 'ubuntu-latest' - 'windows-latest' fail-fast: false runs-on: '${{ matrix.platform }}' steps: - uses: 'actions/checkout@v4' - uses: 'actions/setup-go@v5' with: cache: false go-version-file: 'go.mod' - shell: 'bash' run: |- go test \ -count=1 \ -race \ -shuffle=on \ -timeout=5m \ -vet=all \ ./... go-password-0.3.1/.golangci.yml000066400000000000000000000052211463640622000163470ustar00rootroot00000000000000# Copyright 2023 The Authors (see AUTHORS file) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. run: # default: '1m' timeout: '5m' # default: [] build-tags: - 'all' # default: [] exclude-dirs: - 'third_party' # default: true skip-dirs-use-default: false # default: '' modules-download-mode: 'readonly' # default: false allow-parallel-runners: true linters: enable: - 'asasalint' - 'asciicheck' - 'bidichk' - 'bodyclose' - 'containedctx' - 'dupword' - 'durationcheck' - 'errcheck' - 'errchkjson' - 'errname' - 'errorlint' - 'execinquery' - 'exhaustive' - 'exportloopref' - 'forcetypeassert' - 'gci' - 'gocheckcompilerdirectives' - 'godot' - 'gofmt' - 'gofumpt' - 'goheader' - 'goimports' - 'goprintffuncname' - 'gosec' - 'gosimple' - 'govet' - 'importas' - 'ineffassign' - 'loggercheck' - 'makezero' - 'mirror' - 'misspell' - 'nilerr' - 'noctx' - 'nolintlint' - 'nosprintfhostport' - 'paralleltest' - 'prealloc' - 'predeclared' - 'protogetter' - 'rowserrcheck' - 'sloglint' - 'spancheck' - 'sqlclosecheck' - 'staticcheck' - 'stylecheck' - 'tenv' - 'thelper' - 'typecheck' - 'unconvert' - 'unused' - 'wastedassign' - 'whitespace' - 'wrapcheck' issues: # default: [] exclude: - '^G102:' # gosec: we have to bind to all ifaces in Cloud Run services # default: [] exclude-rules: # Exclude test files from certain linters - path: '_test.go' linters: - 'wrapcheck' # default: 50 max-issues-per-linter: 0 # default: 3 max-same-issues: 0 gci: sections: - 'standard' - 'default' - 'blank' - 'dot' skip-generated: true custom-order: true gofumpt: # default: false extra-rules: true sloglint: # default: false context-only: true # default: false static-msg: false # default: '' (snake, kebab, camel, pascal) key-naming-case: 'snake' # default: false args-on-sep-lines: true severity: # default: '' default-severity: 'error' go-password-0.3.1/LICENSE000066400000000000000000000020571463640622000147740ustar00rootroot00000000000000Copyright 2017 Seth Vargo 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. go-password-0.3.1/README.md000066400000000000000000000047631463640622000152540ustar00rootroot00000000000000## Golang Password Generator [![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/sethvargo/go-password/password) [![GitHub Actions](https://img.shields.io/github/workflow/status/sethvargo/go-password/Test?style=flat-square)](https://github.com/sethvargo/go-password/actions?query=workflow%3ATest) This library implements generation of random passwords with provided requirements as described by [AgileBits 1Password](https://discussions.agilebits.com/discussion/23842/how-random-are-the-generated-passwords) in pure Golang. The algorithm is commonly used when generating website passwords. The library uses crypto/rand for added randomness. Sample example passwords this library may generate: ```text 0N[k9PhDqmmfaO`p_XHjVv`HTq|zsH4XiH8umjg9JAGJ#\Qm6lZ,28XF4{X?3sHj 7@90|0H7!4p\,c Since these are completely randomized, it's possible that they may generate passwords that don't comply with some custom password policies, such as ones that require both upper case AND lower case letters. If your particular use case needs a mix of casing, then you can either increase the number of characters in the password or check the output and regenerate if it fails a particular constraint, such as requiring both upper and lower case. ## Installation ```sh $ go get -u github.com/sethvargo/go-password/password ``` ## Usage ```golang package main import ( "log" "github.com/sethvargo/go-password/password" ) func main() { // Generate a password that is 64 characters long with 10 digits, 10 symbols, // allowing upper and lower case letters, disallowing repeat characters. res, err := password.Generate(64, 10, 10, false, false) if err != nil { log.Fatal(err) } log.Printf(res) } ``` See the [GoDoc](https://godoc.org/github.com/sethvargo/go-password) for more information. ## Testing For testing purposes, instead of accepted a `*password.Generator` struct, accept a `password.PasswordGenerator` interface: ```go // func MyFunc(p *password.Generator) func MyFunc(p password.PasswordGenerator) { // ... } ``` Then, in tests, use a mocked password generator with stubbed data: ```go func TestMyFunc(t *testing.T) { gen := password.NewMockGenerator("canned-response", false) MyFunc(gen) } ``` In this example, the mock generator will always return the value "canned-response", regardless of the provided parameters. ## License This code is licensed under the MIT license. go-password-0.3.1/go.mod000066400000000000000000000001061463640622000150660ustar00rootroot00000000000000module github.com/sethvargo/go-password toolchain go1.21.11 go 1.21 go-password-0.3.1/password/000077500000000000000000000000001463640622000156255ustar00rootroot00000000000000go-password-0.3.1/password/generate.go000066400000000000000000000155521463640622000177560ustar00rootroot00000000000000// 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 } go-password-0.3.1/password/generate_test.go000066400000000000000000000075301463640622000210120ustar00rootroot00000000000000package password import ( "errors" "io" "strings" "sync/atomic" "testing" ) type ( MockReader struct { Counter int64 } ) const ( N = 10000 ) func (mr *MockReader) Read(data []byte) (int, error) { for i := 0; i < len(data); i++ { data[i] = byte(atomic.AddInt64(&mr.Counter, 1)) } return len(data), nil } func testHasDuplicates(tb testing.TB, s string) bool { tb.Helper() found := make(map[rune]struct{}, len(s)) for _, ch := range s { if _, ok := found[ch]; ok { return true } found[ch] = struct{}{} } return false } func testGeneratorGenerate(t *testing.T, reader io.Reader) { t.Helper() gen, err := NewGenerator(nil) if reader != nil { gen.reader = reader } if err != nil { t.Fatal(err) } t.Run("exceeds_length", func(t *testing.T) { t.Parallel() if _, err := gen.Generate(0, 1, 0, false, false); !errors.Is(err, ErrExceedsTotalLength) { t.Errorf("expected %q to be %q", err, ErrExceedsTotalLength) } if _, err := gen.Generate(0, 0, 1, false, false); !errors.Is(err, ErrExceedsTotalLength) { t.Errorf("expected %q to be %q", err, ErrExceedsTotalLength) } }) t.Run("exceeds_letters_available", func(t *testing.T) { t.Parallel() if _, err := gen.Generate(1000, 0, 0, false, false); !errors.Is(err, ErrLettersExceedsAvailable) { t.Errorf("expected %q to be %q", err, ErrLettersExceedsAvailable) } }) t.Run("exceeds_digits_available", func(t *testing.T) { t.Parallel() if _, err := gen.Generate(52, 11, 0, false, false); !errors.Is(err, ErrDigitsExceedsAvailable) { t.Errorf("expected %q to be %q", err, ErrDigitsExceedsAvailable) } }) t.Run("exceeds_symbols_available", func(t *testing.T) { t.Parallel() if _, err := gen.Generate(52, 0, 31, false, false); !errors.Is(err, ErrSymbolsExceedsAvailable) { t.Errorf("expected %q to be %q", err, ErrSymbolsExceedsAvailable) } }) t.Run("gen_lowercase", func(t *testing.T) { t.Parallel() for i := 0; i < N; i++ { res, err := gen.Generate(i%len(LowerLetters), 0, 0, true, true) if err != nil { t.Error(err) } if res != strings.ToLower(res) { t.Errorf("%q is not lowercase", res) } } }) t.Run("gen_uppercase", func(t *testing.T) { t.Parallel() res, err := gen.Generate(1000, 0, 0, false, true) if err != nil { t.Error(err) } if res == strings.ToLower(res) { t.Errorf("%q does not include uppercase", res) } }) t.Run("gen_no_repeats", func(t *testing.T) { t.Parallel() for i := 0; i < N; i++ { res, err := gen.Generate(52, 10, 30, false, false) if err != nil { t.Error(err) } if testHasDuplicates(t, res) { t.Errorf("%q should not have duplicates", res) } } }) } func TestGeneratorGenerate(t *testing.T) { t.Parallel() testGeneratorGenerate(t, nil) } func TestGenerator_Reader_Generate(t *testing.T) { t.Parallel() testGeneratorGenerate(t, &MockReader{}) } func testGeneratorGenerateCustom(t *testing.T, reader io.Reader) { t.Helper() gen, err := NewGenerator(&GeneratorInput{ LowerLetters: "abcde", UpperLetters: "ABCDE", Symbols: "!@#$%", Digits: "01234", Reader: reader, }) if err != nil { t.Fatal(err) } for i := 0; i < N; i++ { res, err := gen.Generate(52, 10, 10, false, true) if err != nil { t.Error(err) } if strings.Contains(res, "f") { t.Errorf("%q should only contain lower letters abcde", res) } if strings.Contains(res, "F") { t.Errorf("%q should only contain upper letters ABCDE", res) } if strings.Contains(res, "&") { t.Errorf("%q should only include symbols !@#$%%", res) } if strings.Contains(res, "5") { t.Errorf("%q should only contain digits 01234", res) } } } func TestGeneratorGenerateCustom(t *testing.T) { t.Parallel() testGeneratorGenerateCustom(t, nil) } func TestGenerator_Reader_Generate_Custom(t *testing.T) { t.Parallel() testGeneratorGenerateCustom(t, &MockReader{}) } go-password-0.3.1/password/mock.go000066400000000000000000000021771463640622000171140ustar00rootroot00000000000000package 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 } go-password-0.3.1/password/password_doc_test.go000066400000000000000000000026451463640622000217110ustar00rootroot00000000000000package password_test import ( "fmt" "log" "github.com/sethvargo/go-password/password" ) func ExampleGenerate() { res, err := password.Generate(64, 10, 10, false, false) if err != nil { log.Fatal(err) } log.Print(res) } func ExampleMustGenerate() { // Will panic on error res := password.MustGenerate(64, 10, 10, false, false) log.Print(res) } func ExampleGenerator_Generate() { gen, err := password.NewGenerator(nil) if err != nil { log.Fatal(err) } res, err := gen.Generate(64, 10, 10, false, false) if err != nil { log.Fatal(err) } log.Print(res) } func ExampleNewGenerator_nil() { // This is exactly the same as calling "Generate" directly. It will use all // the default values. gen, err := password.NewGenerator(nil) if err != nil { log.Fatal(err) } _ = gen // gen.Generate(...) } func ExampleNewGenerator_custom() { // Customize the list of symbols. gen, err := password.NewGenerator(&password.GeneratorInput{ Symbols: "!@#$%^()", }) if err != nil { log.Fatal(err) } _ = gen // gen.Generate(...) } func ExampleNewMockGenerator_testing() { // Accept a password.PasswordGenerator interface instead of a // password.Generator struct. f := func(g password.PasswordGenerator) string { // These values don't matter return g.MustGenerate(1, 2, 3, false, false) } // In tests gen := password.NewMockGenerator("canned-response", nil) fmt.Print(f(gen)) // Output: canned-response }