From befa6c27232c67981d0e28fbd8a569fd2883de04 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 6 Apr 2023 12:21:20 +0200 Subject: [PATCH 01/53] Remove old code for the v2 refactor --- .cirrus.yml | 23 -- .gitignore | 19 -- CODE_OF_CONDUCT.md | 128 -------- Dockerfile | 20 -- LICENSE | 21 -- README.md | 272 ----------------- SECURITY.md | 6 - buildfiles/arch-linux/PKGBUILD | 32 -- buildfiles/openbsd/Makefile | 21 -- buildfiles/openbsd/distinfo | 2 - buildfiles/openbsd/pkg/DESCR | 14 - buildfiles/openbsd/pkg/PLIST | 2 - chars/chars.go | 64 ---- chars/koremutake.go | 22 -- cmd/apg/apg.go | 148 --------- cmd/apg/apg_test.go | 288 ------------------ config/config.go | 194 ------------ docker-files/group | 1 - docker-files/passwd | 1 - .../simple-password-generator/main.go | 27 -- go.mod | 5 - random/random.go | 80 ----- spelling/spelling.go | 140 --------- 23 files changed, 1530 deletions(-) delete mode 100644 .cirrus.yml delete mode 100644 .gitignore delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 Dockerfile delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 SECURITY.md delete mode 100644 buildfiles/arch-linux/PKGBUILD delete mode 100644 buildfiles/openbsd/Makefile delete mode 100644 buildfiles/openbsd/distinfo delete mode 100644 buildfiles/openbsd/pkg/DESCR delete mode 100644 buildfiles/openbsd/pkg/PLIST delete mode 100644 chars/chars.go delete mode 100644 chars/koremutake.go delete mode 100644 cmd/apg/apg.go delete mode 100644 cmd/apg/apg_test.go delete mode 100644 config/config.go delete mode 100644 docker-files/group delete mode 100644 docker-files/passwd delete mode 100644 example-code/simple-password-generator/main.go delete mode 100644 go.mod delete mode 100644 random/random.go delete mode 100644 spelling/spelling.go diff --git a/.cirrus.yml b/.cirrus.yml deleted file mode 100644 index 7a384ce..0000000 --- a/.cirrus.yml +++ /dev/null @@ -1,23 +0,0 @@ -container: - image: golang:latest - -env: - GOPROXY: https://proxy.golang.org - -lint_task: - name: GolangCI Lint - container: - image: golangci/golangci-lint:latest - run_script: golangci-lint run -v --timeout 5m0s --out-format json > lint-report.json - always: - golangci_artifacts: - path: lint-report.json - type: text/json - format: golangci - -build_task: - modules_cache: - folder: $GOPATH/pkg/mod - get_script: go get github.com/wneessen/apg-go/cmd/apg - build_script: go build github.com/wneessen/apg-go/cmd/apg - test_script: go test github.com/wneessen/apg-go/cmd/apg diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 05d855a..0000000 --- a/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib -apg -bin -.go -build - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index c652c0d..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -github+coc@neessen.net. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a8ebb10..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -## Build first -FROM golang:latest as builder -RUN mkdir /builddir -ADD . /builddir/ -WORKDIR /builddir -RUN CGO_ENABLED=0 go build -a -installsuffix cgo -ldflags '-w -s -extldflags "-static"' -o apg-go \ - github.com/wneessen/apg-go/cmd/apg - -## Create scratch image -FROM scratch -LABEL maintainer="wn@neessen.net" -COPY ["docker-files/passwd", "/etc/passwd"] -COPY ["docker-files/group", "/etc/group"] -COPY --from=builder ["/etc/ssl/certs/ca-certificates.crt", "/etc/ssl/cert.pem"] -COPY --chown=apg-go ["LICENSE", "/apg-go/LICENSE"] -COPY --chown=apg-go ["README.md", "/apg-go/README.md"] -COPY --from=builder --chown=apg-go ["/builddir/apg-go", "/apg-go/apg-go"] -WORKDIR /apg-go -USER apg-go -ENTRYPOINT ["/apg-go/apg-go"] \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index fe50abb..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Winni Neessen - -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. diff --git a/README.md b/README.md deleted file mode 100644 index 59ca941..0000000 --- a/README.md +++ /dev/null @@ -1,272 +0,0 @@ -# A "Automated Password Generator"-clone -[![Go Reference](https://pkg.go.dev/badge/github.com/wneessen/apg-go.svg)](https://pkg.go.dev/github.com/wneessen/apg-go) [![Go Report Card](https://goreportcard.com/badge/github.com/wneessen/apg-go)](https://goreportcard.com/report/github.com/wneessen/apg-go) [![Build Status](https://api.cirrus-ci.com/github/wneessen/apg-go.svg)](https://cirrus-ci.com/github/wneessen/apg-go) ![CodeQL workflow](https://github.com/wneessen/apg-go/actions/workflows/codeql-analysis.yml/badge.svg) buy ma a coffee - -_apg-go_ is a simple APG-like password generator written in Go. It tries to replicate the -functionality of the -"[Automated Password Generator](https://web.archive.org/web/20130313042424/http://www.adel.nursat.kz:80/apg)", -which hasn't been maintained since 2003. Since more and more Unix distributions are abondoning the tool, I was -looking for an alternative. FreeBSD for example recommends "security/makepasswd", which is written in Perl -but requires a lot of dependency packages and doesn't offer the feature-set/flexibility of APG. - -Since FIPS-181 (pronouncable passwords) has been withdrawn in 2015, apg-go does not follow this standard. Instead -it implements the [Koremutake Syllables System](https://shorl.com/koremutake.php) in its pronouncable password mode. - -## Installation - -### Docker -There is a ready-to-use Docker image hosted on Github. - -* Download the image: - ```shell - $ docker pull ghcr.io/wneessen/apg-go:main - ``` -* Run the image: - ```shell - $ docker run ghcr.io/wneessen/apg-go:main - ``` - -### Ports/Packages -#### FreeBSD -apg-go can be found as `/security/apg` in the [FreeBSD ports](https://cgit.freebsd.org/ports/tree/security/apg) -tree. -#### Arch Linux -Find apg-go in [Arch Linux AUR](https://aur.archlinux.org/packages/apg-go/). \ -Alternatively use the [PKGBUILD](https://github.com/wneessen/apg-go/tree/main/buildfiles/arch-linux) file -in this git repository -### Binary releases -#### Linux/BSD/MacOS -* Download release - ```sh - $ curl -LO https://github.com/wneessen/apg-go/releases/download/v/apg-v--.tar.gz - $ curl -LO https://github.com/wneessen/apg-go/releases/download/v/apg-v--.tar.gz.sha256 - ``` -* Verify the checksum - ```sh - $ sha256 apg-v--.tar.gz - $ cat apg-v--.tar.gz.sha256 - ``` - **Make sure the checksum of the downloaded file and the checksum in the .sha256 match** -* Extract archive - ```sh - $ tar xzf apg-v--.tar.gz - ``` -* Execute - ```sh - $ ./apg - ``` -#### Windows -* Download release - ```PowerShell - PS> Invoke-RestMethod -Uri https://github.com/wneessen/apg-go/releases/download/v/apg-v-windows-.zip -OutFile apg-v-windows-.zip - PS> Invoke-RestMethod -Uri https://github.com/wneessen/apg-go/releases/download/v/apg-v-windows-.zip.sha256 -OutFile apg-v-windows-.zip.sha256 - ``` -* Verify the checksum - ```PowerShell - PS> Get-FileHash apg-v-windows-.zip | Format-List - PS> type apg-v-windows-.zip.sha256 - ``` - **Make sure the checksum of the downloaded file and the checksum in the .sha256 match** -* Extract archive - ```PowerShell - PS> Expand-Archive -LiteralPath apg-v-windows- - ``` -* Execute - ```PowerShell - PS> cd apg-v-windows- - PS> apg.exe - ``` - -### Sources -* Download sources - ```sh - $ curl -LO https://github.com/wneessen/apg-go/archive/refs/tags/v.tar.gz - ``` -* Extract source - ```sh - $ tar xzf v.tar.gz - ``` -* Build binary - ```sh - $ cd apg-go- - $ go build -o apg ./... - ``` -* Execute the brand new binary - ```sh - $ ./apg - ``` - -### Systemwide installation -It is recommed to install apg in a directory of your ```$PATH``` environment. To do so run: -(In this example we use ```/usr/local/bin``` as system-wide binary path. YMMV) -```sh -$ sudo cp apg /usr/local/bin/apg -``` - -## Programmatic interface -Since v0.4.0 the CLI and the main package functionality have been separated from each other, which makes -it easier to use the `apg-go` package in other Go code as well. This way you can make of the password -generation in your own code without having to rely on the actual apg-go binary. - -Code examples on how to use the package can be found in the [example-code](example-code) directory. - -## Usage examples -### Default behaviour -By default apg-go will generate 6 passwords, with a minimum length of 12 characters and a -maxiumum length of 20 characters. The generated password will use a character set constructed -from lower case, upper case and numeric characters. -```shell -$ ./apg-go -R8rCC8bw5NvJmTUK2g -cHB9qogTbfdzFgnH -hoHfpWAHHSNa4Q -QyjscIsZkQGh -904YqsU5SnoqLo2w -utdFKXdeiXFzM -``` -### Modifying the character sets -#### Old style -Let's assume you want to generate a single password, constructed out of upper case, numeric -and special characters. Since lower case is part of the default set, you would need to disable them -by setting the `-L` parameter. In addition you would set the `-S` parameter to enable special -characters. Finally the parameter `-n 1` is needed to keep apg-go from generating more than one -password: -```shell -$ ./apg-go -n 1 -L -S -XY7>}H@5U40&_A1*9I$ -``` - -#### New/modern style -Since the old style switches can be kind of confusing, it is recommended to use the "new style" -parameters instead. The new style is all combined in the `-M` parameter. Using the upper case -version of a parameter argument enables a feature, while the lower case version disabled it. The -previous example could be represented like this in new style: -```shell -$ ./apg-go -n 1 -M lUSN -$B|~sudhtyDBu -``` - -### Password spelling -If you need to read out a password, it can be helpful to know the corresponding word for that character in -the phonetic alphabet. By setting the `-l` parameter, agp-go will provide you with the phonetic spelling -(english language) of your newly created password: -```shell -$ ./apg-go -n 1 -M LUSN -H -E : -l -fUTDKeFsU+zn3r= (foxtrot/Uniform/Tango/Delta/Kilo/echo/Foxtrot/sierra/Uniform/PLUS_SIGN/zulu/november/THREE/romeo/EQUAL_SIGN) -``` - -### Pronouncable passwords -Since v0.4.0 apg-go supports pronouncable passwords, anologous to the original c-apg using the `-a 0` -flag. The original c-apg implemented FIPS-181, which was withdrawn in 2015 for generating pronouncable -passwords. Since the standard is not recommended anymore, `apg-go` instead make use of the -[Koremutake Syllables System](https://shorl.com/koremutake.php). Similar to the original apg, `agp-go` -will automatically randomly add special characters and number (from the human-readable pool) to each -generated pronouncable password. Additionally it will perform a "coinflip" for each Koremutake syllable -and decided if it should switch the case of one of the characters to an upper-case character. - -Using the `-t` parameter, `apg-go` will display a spelled out version of the pronouncable password, where -each syllable or number/special character is seperated with a "-" (dash) and if the syllable is not a -Koremutake syllable the character will be spelled out the same was as with activated `-l` in the -non-pronouncable password mode (`-a 1`). - -**Note on password length**: The `-m` and `-x` parameters will work in prouncable password mode, but -please keep in mind, that due to the nature how syllables work, your generated password might exceed -the desired length by one complete syllable (which can be up to 3 characters long). - -**Security consideration:** Please keep in mind, that pronouncable passwords are less secure then truly -randomly created passwords, due to the nature how syllables work. As a rule of thumb, it is recommended -to multiply the length of your generated pronouncable passwords by at least 1.5 times, compared to truly -randomly generated passwords. It might also be helpful to run the pronoucable password mode with enabled -"[HIBP](#have-i-been-pwned)" flag, so that each generated password is automatically checked against "Have I Been Pwned" -database. -```shell -$ ./apg-go -a 0 -n 1 -KebrutinernMy - -$ ./apg-go -a 0 -n 1 -m 15 -x 15 -t -pEnbocydrageT*En (pEn-bo-cy-dra-geT-ASTERISK-En) -``` - -### Have I Been Pwned -Even though, the passwords that apg-go generated for you, are secure, there is a minimal chance, that -someone on the planet used exactly the same password before and that this person was part of an -internet leak or hack, which exposed the password to the public. Such passwords are not considered -secure anymore as they usually land on public available password lists, that are used by crackers. - -To be on the safe side, you can use the `-p` parameter, to enable a HIBP check. When the feature is -enabled, apg-go will check the HIBP database at https://haveibeenpwned.com if that password has been -leaked before and provide you with a warning if that is the case. - -Please be aware, that this is a live check against the HIBP API, which not only requires internet -connectivity, but also might take between 500ms to 1s to complete. When you generating a bigger list -of password `-n 100`, the process could take much longer than without the `-p` feature enabled. - -## CLI parameters -_apg-go_ replicates most of the parameters of the original c-apg. Some parameters are different though: - -- `-a `: Choose password generation algorithm (Default: 1) - - `0`: Pronouncable password generation (Koremutake syllables) - - `1`: Random password generation according to password modes/flags -- `-m `: The minimum length of the password to be generated (Default: 12) -- `-x `: The maximum length of the password to be generated (Default: 20) -- `-n `: The amount of passwords to be generated (Default: 6) -- `-E `: Do not use the specified characters in generated passwords -- `-M <[LUNSHClunshc]>`: New style password parameters (upper-case enables, lower-case disables) -- `-L`: Use lower-case characters in passwords (Default: on) -- `-U`: Use upper-case characters in passwords (Default: on) -- `-N`: Use numeric characters in passwords (Default: on) -- `-S`: Use special characters in passwords (Default: off) -- `-H`: Avoid ambiguous characters in passwords (i. e.: 1, l, I, o, O, 0) (Default: off) -- `-C`: Generate complex passwords (implies -L -U -N -S and disables -H) (Default: off) -- `-l`: Spell generated passwords in random password mode (Default: off) -- `-t`: Spell generated passwords in pronouncable password mode (Default: off) -- `-p`: Check the HIBP database if the generated passwords was found in a leak before (Default: off) // *this feature requires internet connectivity* -- `-h`: Show a CLI help text -- `-v`: Show the version number - -## Contributors -Thanks to the following people for contributing to the apg-go codebase: -* [Romain Tartière](https://github.com/smortex) -* [Abraham Ingersoll](https://github.com/aberoham) -* [Vinícius Zavam](https://github.com/egypcio) (Maintaining the FreeBSD port) diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index d41fcde..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,6 +0,0 @@ -# Security Policy - -## Reporting a Vulnerability - -Please send a mail to apg-go@pebcak.de when you found a security issue in apg-go, even when you are not 100% certain -that it is actually a security issue. Typically, you will receive an answer within a day or even within a few hours. diff --git a/buildfiles/arch-linux/PKGBUILD b/buildfiles/arch-linux/PKGBUILD deleted file mode 100644 index fcdb040..0000000 --- a/buildfiles/arch-linux/PKGBUILD +++ /dev/null @@ -1,32 +0,0 @@ -# Maintainer: "Winni Neessen (https://pebcak.de) - -pkgname=apg-go -pkgver=0.4.1 -pkgrel=1 -pkgdesc='A "Automated Password Generator"-clone' -arch=('i686' 'x86_64' 'armv6h' 'armv7h' 'aarch64') -url='https://github.com/wneessen/apg-go' -license=('MIT') -makedepends=('go') -source=("https://github.com/wneessen/${pkgname}/archive/refs/tags/v${pkgver}.tar.gz") -sha256sums=('64769495843e2c59fc5513a106e58f8751f1649ff8bf6c38cd141322523deea8') - -prepare() { - cd "${pkgname}-${pkgver}" - mkdir -p build/ -} - -build() { - cd "${pkgname}-${pkgver}" - go build -ldflags="-s -w" -o build/${pkgname} github.com/wneessen/apg-go/cmd/apg -} - -package() { - # binary - install -D -m755 "${srcdir}/${pkgname}-${pkgver}/build/${pkgname}" \ - "${pkgdir}/usr/bin/apg" - - # license - install -Dm644 "${srcdir}/${pkgname}-${pkgver}/LICENSE" \ - "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" -} diff --git a/buildfiles/openbsd/Makefile b/buildfiles/openbsd/Makefile deleted file mode 100644 index 08fe24b..0000000 --- a/buildfiles/openbsd/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -# $OpenBSD$ - -COMMENT = "automated password generator" clone written in Go - -GH_ACCOUNT = wneessen -GH_PROJECT = apg-go -GH_TAGNAME = v0.3.2 - -CATEGORIES = security - -MAINTAINER = Winni Neessen - -# MIT -PERMIT_PACKAGE = Yes - -MODULES = lang/go -MODGO_TYPE = bin - -ALL_TARGET = wneessen/apg-go/cmd/apg - -.include \ No newline at end of file diff --git a/buildfiles/openbsd/distinfo b/buildfiles/openbsd/distinfo deleted file mode 100644 index c0bac84..0000000 --- a/buildfiles/openbsd/distinfo +++ /dev/null @@ -1,2 +0,0 @@ -SHA256 (apg-go-0.3.2.tar.gz) = QvCC0vVNHLIOHW1jwdkjJVtxEVHJNwQfZBZBgHWM4OQ= -SIZE (apg-go-0.3.2.tar.gz) = 20114 \ No newline at end of file diff --git a/buildfiles/openbsd/pkg/DESCR b/buildfiles/openbsd/pkg/DESCR deleted file mode 100644 index 0a6857e..0000000 --- a/buildfiles/openbsd/pkg/DESCR +++ /dev/null @@ -1,14 +0,0 @@ -apg-go is a simple APG-like password generator written in Go. - -It tries to replicate the functionality of the "Automated Password Generator", -which hasn't been maintained since 2003. Since more and more Unix distributions -are abondoning the tool, I was looking for an alternative. FreeBSD for example -recommends "security/makepasswd", which is written in Perl but requires a lot -of dependency packages and doesn't offer the feature-set/flexibility of APG. - -Since FIPS-181 (pronouncable passwords) has been withdrawn in 2015, there is -no use in replicating that feature. Therfore apg-go does not support -pronouncable passwords. - -For feature requests or bug reports, please create an issue in the Github -repository at https://github.com/wneessen/apg-go \ No newline at end of file diff --git a/buildfiles/openbsd/pkg/PLIST b/buildfiles/openbsd/pkg/PLIST deleted file mode 100644 index 0b10474..0000000 --- a/buildfiles/openbsd/pkg/PLIST +++ /dev/null @@ -1,2 +0,0 @@ -@comment $OpenBSD: PLIST,v$ -@bin bin/apg-go \ No newline at end of file diff --git a/chars/chars.go b/chars/chars.go deleted file mode 100644 index 90a8011..0000000 --- a/chars/chars.go +++ /dev/null @@ -1,64 +0,0 @@ -package chars - -import ( - "github.com/wneessen/apg-go/config" - "regexp" -) - -// PwLowerCharsHuman is the range of lower-case characters in human-readable mode -const PwLowerCharsHuman string = "abcdefghjkmnpqrstuvwxyz" - -// PwUpperCharsHuman is the range of upper-case characters in human-readable mode -const PwUpperCharsHuman string = "ABCDEFGHJKMNPQRSTUVWXYZ" - -// PwLowerChars is the range of lower-case characters -const PwLowerChars string = "abcdefghijklmnopqrstuvwxyz" - -// PwUpperChars is the range of upper-case characters -const PwUpperChars string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - -// PwSpecialCharsHuman is the range of special characters in human-readable mode -const PwSpecialCharsHuman string = "\"#%*+-/:;=\\_|~" - -// PwSpecialChars is the range of special characters -const PwSpecialChars string = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" - -// PwNumbersHuman is the range of numbers in human-readable mode -const PwNumbersHuman string = "23456789" - -// PwNumbers is the range of numbers -const PwNumbers string = "1234567890" - -// GetRange provides the range of available characters based on configured parameters -func GetRange(config *config.Config) string { - pwUpperChars := PwUpperChars - pwLowerChars := PwLowerChars - pwNumbers := PwNumbers - pwSpecialChars := PwSpecialChars - if config.HumanReadable { - pwUpperChars = PwUpperCharsHuman - pwLowerChars = PwLowerCharsHuman - pwNumbers = PwNumbersHuman - pwSpecialChars = PwSpecialCharsHuman - } - - var charRange string - if config.UseLowerCase { - charRange = charRange + pwLowerChars - } - if config.UseUpperCase { - charRange = charRange + pwUpperChars - } - if config.UseNumber { - charRange = charRange + pwNumbers - } - if config.UseSpecial { - charRange = charRange + pwSpecialChars - } - if config.ExcludeChars != "" { - regExp := regexp.MustCompile("[" + regexp.QuoteMeta(config.ExcludeChars) + "]") - charRange = regExp.ReplaceAllLiteralString(charRange, "") - } - - return charRange -} diff --git a/chars/koremutake.go b/chars/koremutake.go deleted file mode 100644 index 6d98636..0000000 --- a/chars/koremutake.go +++ /dev/null @@ -1,22 +0,0 @@ -package chars - -// KoremutakeSyllables is a slightly modified Koremutake syllables list based on -// the mechanism described on https://shorl.com/koremutake.php -var KoremutakeSyllables = []string{"ba", "be", "bi", "bo", "bu", "by", "da", "de", "di", - "do", "du", "dy", "fe", "fi", "fo", "fu", "fy", "ga", "ge", "gi", "go", "gu", - "gy", "ha", "he", "hi", "ho", "hu", "hy", "ja", "je", "ji", "jo", "ju", "jy", - "ka", "ke", "ko", "ku", "ky", "la", "le", "li", "lo", "lu", "ly", "ma", - "me", "mi", "mo", "mu", "my", "na", "ne", "ni", "no", "nu", "ny", "pa", "pe", - "pi", "po", "pu", "py", "ra", "re", "ri", "ro", "ru", "ry", "sa", "se", - "si", "so", "su", "sy", "ta", "te", "ti", "to", "tu", "ty", "va", "ve", "vi", - "vo", "vu", "vy", "bra", "bre", "bri", "bro", "bru", "bry", "dra", "dre", - "dri", "dro", "dru", "dry", "fra", "fre", "fri", "fro", "fru", "fry", "gra", - "gre", "gri", "gro", "gru", "gry", "pra", "pre", "pri", "pro", "pru", - "pry", "sta", "ste", "sti", "sto", "stu", "sty", "tra", "tre", "er", "ed", - "in", "ex", "al", "en", "an", "ad", "or", "at", "ca", "ap", "el", "ci", "an", - "et", "it", "ob", "of", "af", "au", "cy", "im", "op", "co", "up", "ing", - "con", "ter", "com", "per", "ble", "der", "cal", "man", "est", "for", "mer", - "col", "ful", "get", "low", "son", "tle", "day", "pen", "pre", "ten", - "tor", "ver", "ber", "can", "ple", "fer", "gen", "den", "mag", "sub", "sur", - "men", "min", "out", "tal", "but", "cit", "cle", "cov", "dif", "ern", - "eve", "hap", "ket", "nal", "sup", "ted", "tem", "tin", "tro", "tro"} diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go deleted file mode 100644 index 39b43ec..0000000 --- a/cmd/apg/apg.go +++ /dev/null @@ -1,148 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "github.com/wneessen/apg-go/chars" - "github.com/wneessen/apg-go/config" - "github.com/wneessen/apg-go/random" - "github.com/wneessen/apg-go/spelling" - "github.com/wneessen/go-hibp" - "log" - "os" - "runtime" - "strings" - "time" -) - -// VersionString represents the current version of the apg-go CLI -const VersionString string = "0.4.1" - -// Help text -const usage = `apg-go // A "Automated Password Generator"-clone -Copyright (c) 2021 Winni Neessen - -apg [-a ] [-m ] [-x ] [-L] [-U] [-N] [-S] [-H] [-C] - [-l] [-M mode] [-E char_string] [-n num_of_pass] [-v] [-h] [-t] - -Options: - -a ALGORITH Choose the password generation algorithm (Default: 1) - - 0: pronounceable password generation (koremutake syllables) - - 1: random password generation according to password modes/flags - -m LENGTH Minimum length of the password to be generated (Default: 12) - -x LENGTH Maximum length of the password to be generated (Default: 20) - -n NUMBER Amount of password to be generated (Default: 6) - -E CHARS List of characters to be excluded in the generated password - -M [LUNSHClunshc] New style password parameters (upper case: on, lower case: off) - -L Use lower case characters in passwords (Default: on) - -U Use upper case characters in passwords (Default: on) - -N Use numeric characters in passwords (Default: on) - -S Use special characters in passwords (Default: off) - -H Avoid ambiguous characters in passwords (i. e.: 1, l, I, O, 0) (Default: off) - -C Enable complex password mode (implies -L -U -N -S and disables -H) (Default: off) - -l Spell generated passwords in phonetic alphabet (Default: off) - -p Check the HIBP database if the generated passwords was found in a leak before (Default: off) - - Note: this feature requires internet connectivity - -h Show this help text - -v Show version string` - -// Main function that generated the passwords and returns them -func main() { - // Log configuration - log.SetFlags(log.Ltime | log.Ldate | log.Lshortfile) - - // Read and parse flags - flag.Usage = func() { _, _ = fmt.Fprintf(os.Stderr, "%s\n", usage) } - var cfgObj = config.New() - - // Show version and exit - if cfgObj.ShowVersion { - _, _ = os.Stderr.WriteString(`apg-go // A "Automated Password Generator"-clone v` + VersionString + "\n") - _, _ = os.Stderr.WriteString("OS: " + runtime.GOOS + " // Arch: " + runtime.GOARCH + " \n") - _, _ = os.Stderr.WriteString("(C) 2021 by Winni Neessen\n") - os.Exit(0) - } - - pwList := make([]string, 0) - sylList := map[string][]string{} - - // Choose the type of password generation based on the selected algo - for i := 0; i < cfgObj.NumOfPass; i++ { - pwLength := config.GetPwLengthFromParams(&cfgObj) - - switch cfgObj.PwAlgo { - case 0: - pwString := "" - pwSyls := make([]string, 0) - - charSylSet := chars.KoremutakeSyllables - charSylSet = append(charSylSet, - strings.Split(chars.PwNumbersHuman, "")...) - charSylSet = append(charSylSet, - strings.Split(chars.PwSpecialCharsHuman, "")...) - charSylSetLen := len(charSylSet) - for len(pwString) < pwLength { - randNum, err := random.GetNum(charSylSetLen) - if err != nil { - log.Fatalf("error generating Koremutake syllable: %s", err) - } - nextSyl := charSylSet[randNum] - if random.CoinFlip() { - sylLen := len(nextSyl) - charPos, err := random.GetNum(sylLen) - if err != nil { - log.Fatalf("error generating random number: %s", err) - } - ucChar := string(nextSyl[charPos]) - nextSyl = strings.ReplaceAll(nextSyl, ucChar, strings.ToUpper(ucChar)) - } - - pwString += nextSyl - pwSyls = append(pwSyls, nextSyl) - } - pwList = append(pwList, pwString) - sylList[pwString] = pwSyls - default: - charRange := chars.GetRange(&cfgObj) - pwString, err := random.GetChar(charRange, pwLength) - if err != nil { - log.Fatalf("error generating random character range: %s\n", err) - } - pwList = append(pwList, pwString) - } - } - - for _, p := range pwList { - switch cfgObj.OutputMode { - case 1: - spelledPw, err := spelling.String(p) - if err != nil { - log.Fatalf("error spelling out password: %s\n", err) - } - fmt.Printf("%v (%v)\n", p, spelledPw) - case 2: - fmt.Printf("%s", p) - if cfgObj.SpellPron { - spelledPw, err := spelling.Koremutake(sylList[p]) - if err != nil { - log.Fatalf("error spelling out password: %s", err) - } - fmt.Printf(" (%s)", spelledPw) - } - fmt.Println() - default: - fmt.Println(p) - } - - if cfgObj.CheckHibp { - hc := hibp.New(hibp.WithHTTPTimeout(time.Second*2), hibp.WithPwnedPadding()) - pwnObj, _, err := hc.PwnedPassAPI.CheckPassword(p) - if err != nil { - log.Printf("unable to check HIBP database: %v", err) - } - if pwnObj != nil && pwnObj.Count != 0 { - fmt.Print("^-- !!WARNING: The previously generated password was found in HIBP database. Do not use it!!\n") - } - } - } -} diff --git a/cmd/apg/apg_test.go b/cmd/apg/apg_test.go deleted file mode 100644 index a9febf0..0000000 --- a/cmd/apg/apg_test.go +++ /dev/null @@ -1,288 +0,0 @@ -package main - -import ( - "github.com/wneessen/apg-go/chars" - "github.com/wneessen/apg-go/config" - "github.com/wneessen/apg-go/random" - "github.com/wneessen/apg-go/spelling" - "testing" -) - -var cfgObj config.Config - -// Make sure the flags are initialized -var _ = func() bool { - testing.Init() - cfgObj = config.New() - return true -}() - -// Test getRandNum with max 1000 -func TestGetRandNum(t *testing.T) { - testTable := []struct { - testName string - givenVal int - maxRet int - minRet int - shouldFail bool - }{ - {"randNum up to 1000", 1000, 1000, 0, false}, - {"randNum should be 1", 1, 1, 0, false}, - {"randNum should fail on 0", 0, 0, 0, true}, - {"randNum should fail on negative", -1, 0, 0, true}, - } - - for _, testCase := range testTable { - t.Run(testCase.testName, func(t *testing.T) { - randNum, err := random.GetNum(testCase.givenVal) - if testCase.shouldFail { - if err == nil { - t.Errorf("Random number generation succeeded but was expected to fail. Given: %v, returned: %v", - testCase.givenVal, randNum) - } - } else { - if err != nil { - t.Errorf("Random number generation failed: %v", err.Error()) - } - if randNum > testCase.maxRet { - t.Errorf("Random number generation returned too big value. Given %v, expected max: %v, got: %v", - testCase.givenVal, testCase.maxRet, randNum) - } - if randNum < testCase.minRet { - t.Errorf("Random number generation returned too small value. Given %v, expected max: %v, got: %v", - testCase.givenVal, testCase.minRet, randNum) - } - } - }) - } -} - -// Test Pwlength -func TestGenLength(t *testing.T) { - testTable := []struct { - testName string - minLength int - maxLength int - }{ - {"pwLength defaults", config.DefaultMinLength, config.DefaultMaxLength}, - {"pwLength 0 to 1", 0, 1}, - {"pwLength 1 to 10", 0, 10}, - {"pwLength 10 to 100", 10, 100}, - } - - charRange := chars.GetRange(&cfgObj) - for _, testCase := range testTable { - t.Run(testCase.testName, func(t *testing.T) { - cfgObj.MinPassLen = testCase.minLength - cfgObj.MaxPassLen = testCase.maxLength - pwLength := config.GetPwLengthFromParams(&cfgObj) - for i := 0; i < 1000; i++ { - pwString, err := random.GetChar(charRange, pwLength) - if err != nil { - t.Errorf("getRandChar returned an error: %q", err) - } - retLen := len(pwString) - if retLen > testCase.maxLength { - t.Errorf("Generated password length too long. GivenMin %v, GivenMax: %v, Returned length %v", - testCase.minLength, testCase.maxLength, retLen) - } - if retLen < testCase.minLength { - t.Errorf("Generated password length too short. GivenMin %v, GivenMax: %v, Returned length %v", - testCase.minLength, testCase.maxLength, retLen) - } - } - }) - } -} - -// Test getRandChar -func TestGetRandChar(t *testing.T) { - t.Run("return_value_is_A_B_or_C", func(t *testing.T) { - charRange := "ABC" - randChar, err := random.GetChar(charRange, 1) - if err != nil { - t.Fatalf("Random character generation failed => %v", err.Error()) - } - if randChar != "A" && randChar != "B" && randChar != "C" { - t.Fatalf("Random character generation failed. Expected A, B or C but got: %v", randChar) - } - }) - - t.Run("return_value_has_specific_length", func(t *testing.T) { - charRange := "ABC" - randChar, err := random.GetChar(charRange, 1000) - if err != nil { - t.Fatalf("Random character generation failed => %v", err.Error()) - } - if len(randChar) != 1000 { - t.Fatalf("Generated random characters with 1000 chars returned wrong amount of chars: %v", - len(randChar)) - } - }) - - t.Run("fail", func(t *testing.T) { - charRange := "ABC" - randChar, err := random.GetChar(charRange, -2000) - if err == nil { - t.Fatalf("Generated random characters expected to fail, but returned a value => %v", - randChar) - } - }) -} - -// Test getCharRange() with different cfgObj settings -func TestGetCharRange(t *testing.T) { - lowerCaseBytes := []int{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', - 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'} - lowerCaseHumanBytes := []int{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', - 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'} - upperCaseBytes := []int{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', - 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'} - upperCaseHumanBytes := []int{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', - 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'} - numberBytes := []int{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} - numberHumanBytes := []int{'2', '3', '4', '5', '6', '7', '8', '9'} - specialBytes := []int{'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', - ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'} - specialHumanBytes := []int{'"', '#', '%', '*', '+', '-', '/', ':', ';', '=', '\\', '_', '|', '~'} - testTable := []struct { - testName string - allowedBytes []int - useLowerCase bool - useUpperCase bool - useNumber bool - useSpecial bool - humanReadable bool - }{ - {"lowercase_only", lowerCaseBytes, true, false, false, false, false}, - {"lowercase_only_human", lowerCaseHumanBytes, true, false, false, false, true}, - {"uppercase_only", upperCaseBytes, false, true, false, false, false}, - {"uppercase_only_human", upperCaseHumanBytes, false, true, false, false, true}, - {"number_only", numberBytes, false, false, true, false, false}, - {"number_only_human", numberHumanBytes, false, false, true, false, true}, - {"special_only", specialBytes, false, false, false, true, false}, - {"special_only_human", specialHumanBytes, false, false, false, true, true}, - } - - for _, testCase := range testTable { - t.Run(testCase.testName, func(t *testing.T) { - cfgObj.UseLowerCase = testCase.useLowerCase - cfgObj.UseUpperCase = testCase.useUpperCase - cfgObj.UseNumber = testCase.useNumber - cfgObj.UseSpecial = testCase.useSpecial - cfgObj.HumanReadable = testCase.humanReadable - charRange := chars.GetRange(&cfgObj) - for _, curChar := range charRange { - searchAllowedBytes := containsByte(testCase.allowedBytes, int(curChar), t) - if !searchAllowedBytes { - t.Errorf("Character range returned invalid value: %v", string(curChar)) - } - } - }) - } -} - -// Test Conversions -func TestConvert(t *testing.T) { - testTable := []struct { - testName string - givenVal byte - expVal string - shouldFail bool - }{ - {"convert_A_to_Alfa", 'A', "Alfa", false}, - {"convert_a_to_alfa", 'a', "alfa", false}, - {"convert_0_to_ZERO", '0', "ZERO", false}, - {"convert_/_to_SLASH", '/', "SLASH", false}, - } - - for _, testCase := range testTable { - t.Run(testCase.testName, func(t *testing.T) { - charToString, err := spelling.ConvertCharToName(testCase.givenVal) - if testCase.shouldFail { - if err == nil { - t.Errorf("Character to string conversion succeeded but was expected to fail. Given: %v, returned: %v", - testCase.givenVal, charToString) - } - } else { - if err != nil { - t.Errorf("Character to string conversion failed: %v", err.Error()) - } - if charToString != testCase.expVal { - t.Errorf("Character to String conversion fail. Given: %q, expected: %q, got: %q", - testCase.givenVal, testCase.expVal, charToString) - } - } - }) - } - - t.Run("all_chars_must_return_a_conversion_string", func(t *testing.T) { - cfgObj.UseUpperCase = true - cfgObj.UseLowerCase = true - cfgObj.UseNumber = true - cfgObj.UseSpecial = true - cfgObj.HumanReadable = false - charRange := chars.GetRange(&cfgObj) - for _, curChar := range charRange { - _, err := spelling.ConvertCharToName(byte(curChar)) - if err != nil { - t.Fatalf("Character to string conversion failed: %v", err.Error()) - } - } - }) - t.Run("spell_Ab!_to_strings", func(t *testing.T) { - pwString := "Ab!" - spelledString, err := spelling.String(pwString) - if err != nil { - t.Fatalf("password spelling failed: %v", err.Error()) - } - if spelledString != "Alfa/bravo/EXCLAMATION_POINT" { - t.Fatalf( - "Spelling pwString 'Ab!' is expected to provide 'Alfa/bravo/EXCLAMATION_POINT', but returned: %q", - spelledString) - } - }) -} - -// Benchmark: Random number generation -func BenchmarkGetRandNum(b *testing.B) { - for i := 0; i < b.N; i++ { - _, _ = random.GetNum(100000) - } -} - -// Benchmark: Random char generation -func BenchmarkGetRandChar(b *testing.B) { - charRange := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\"#/!\\$%&+-*.,?=()[]{}:;~^|" - for i := 0; i < b.N; i++ { - _, _ = random.GetChar(charRange, 20) - } -} - -// Benchmark: Random char generation -func BenchmarkConvertChar(b *testing.B) { - - cfgObj.UseUpperCase = true - cfgObj.UseLowerCase = true - cfgObj.UseNumber = true - cfgObj.UseSpecial = true - cfgObj.HumanReadable = false - charRange := chars.GetRange(&cfgObj) - for i := 0; i < b.N; i++ { - charToConv, _ := random.GetChar(charRange, 1) - charBytes := []byte(charToConv) - _, _ = spelling.ConvertCharToName(charBytes[0]) - } -} - -// Contains function to search a given slice for values -func containsByte(allowedBytes []int, currentChar int, t *testing.T) bool { - t.Helper() - - for _, charInt := range allowedBytes { - if charInt == currentChar { - return true - } - } - return false -} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index eaee823..0000000 --- a/config/config.go +++ /dev/null @@ -1,194 +0,0 @@ -package config - -import ( - "flag" - "github.com/wneessen/apg-go/random" - "log" -) - -// Config is a struct that holds the different config parameters for the apg-go -// application -type Config struct { - MinPassLen int // Minimum password length - MaxPassLen int // Maximum password length - NumOfPass int // Number of passwords to be generated - useComplex bool // Force complex password generation (implies all other Use* Options to be true) - UseLowerCase bool // Allow lower-case chars in passwords - UseUpperCase bool // Allow upper-case chars in password - UseNumber bool // Allow numbers in passwords - UseSpecial bool // Allow special chars in passwords - HumanReadable bool // Generated passwords use the "human readable" character set - CheckHibp bool // Check generated are validated against the HIBP API for possible leaks - ExcludeChars string // List of characters to be excluded from the PW generation charset - NewStyleModes string // Use the "new style" parameters instead of the single params - spellPassword bool // Spell out passwords in the output - ShowHelp bool // Display the help message in the CLI - ShowVersion bool // Display the version string in the CLI - OutputMode int // Interal parameter to control the output mode of the CLI - PwAlgo int // PW generation algorithm to use (0: random PW based on flags, 1: pronouncable) - SpellPron bool // Spell out the pronouncable password -} - -// DefaultMinLength reflects the default minimum length of a generated password -const DefaultMinLength int = 12 - -// DefaultMaxLength reflects the default maximum length of a generated password -const DefaultMaxLength int = 20 - -// DefaultPwAlgo reflects the default password generation algorithm -const DefaultPwAlgo int = 1 - -// New parses the CLI flags and returns a new config object -func New() Config { - var switchConf Config - defaultSwitches := Config{ - UseLowerCase: true, - UseUpperCase: true, - UseNumber: true, - UseSpecial: false, - useComplex: false, - HumanReadable: false, - } - config := Config{ - UseLowerCase: defaultSwitches.UseLowerCase, - UseUpperCase: defaultSwitches.UseUpperCase, - UseNumber: defaultSwitches.UseNumber, - UseSpecial: defaultSwitches.UseSpecial, - useComplex: defaultSwitches.useComplex, - HumanReadable: defaultSwitches.HumanReadable, - } - - // Read and set all flags - flag.BoolVar(&switchConf.UseLowerCase, "L", false, "Use lower case characters in passwords") - flag.BoolVar(&switchConf.UseUpperCase, "U", false, "Use upper case characters in passwords") - flag.BoolVar(&switchConf.UseNumber, "N", false, "Use numerich characters in passwords") - flag.BoolVar(&switchConf.UseSpecial, "S", false, "Use special characters in passwords") - flag.BoolVar(&switchConf.useComplex, "C", false, "Generate complex passwords (implies -L -U -N -S, disables -H)") - flag.BoolVar(&switchConf.HumanReadable, "H", false, "Generate human-readable passwords") - flag.BoolVar(&config.spellPassword, "l", false, "Spell generated password") - flag.BoolVar(&config.CheckHibp, "p", false, "Check the HIBP database if the generated password was leaked before") - flag.BoolVar(&config.ShowVersion, "v", false, "Show version") - flag.IntVar(&config.MinPassLen, "m", DefaultMinLength, "Minimum password length") - flag.IntVar(&config.MaxPassLen, "x", DefaultMaxLength, "Maxiumum password length") - flag.IntVar(&config.NumOfPass, "n", 6, "Number of passwords to generate") - flag.StringVar(&config.ExcludeChars, "E", "", "Exclude list of characters from generated password") - flag.StringVar(&config.NewStyleModes, "M", "", - "New style password parameters (higher priority than single parameters)") - flag.IntVar(&config.PwAlgo, "a", DefaultPwAlgo, "Password generation algorithm") - flag.BoolVar(&config.SpellPron, "t", false, "In pronouncable password mode, spell out the password") - flag.Parse() - - // Invert-switch the defaults - if switchConf.UseLowerCase { - config.UseLowerCase = !defaultSwitches.UseLowerCase - } - if switchConf.UseUpperCase { - config.UseUpperCase = !defaultSwitches.UseUpperCase - } - if switchConf.UseNumber { - config.UseNumber = !defaultSwitches.UseNumber - } - if switchConf.UseSpecial { - config.UseSpecial = !defaultSwitches.UseSpecial - } - if switchConf.useComplex { - config.useComplex = !defaultSwitches.useComplex - } - if switchConf.HumanReadable { - config.HumanReadable = !defaultSwitches.HumanReadable - } - - // Parse additional parameters and new-style switches - parseParams(&config) - - return config -} - -// Parse the parameters and set the according config flags -func parseParams(config *Config) { - parseNewStyleParams(config) - - // Complex overrides everything - if config.useComplex { - config.UseUpperCase = true - config.UseLowerCase = true - config.UseSpecial = true - config.UseNumber = true - config.HumanReadable = false - } - - if !config.UseUpperCase && - !config.UseLowerCase && - !config.UseNumber && - !config.UseSpecial { - log.Fatalf("No password mode set. Cannot generate password from empty character set.") - } - - // Set output mode - switch config.PwAlgo { - case 0: - config.OutputMode = 2 - default: - config.OutputMode = 0 - if config.spellPassword { - config.OutputMode = 1 - } - } -} - -// Parse the new style parameters -func parseNewStyleParams(config *Config) { - if config.NewStyleModes == "" { - return - } - - for _, curParam := range config.NewStyleModes { - switch curParam { - case 'S': - config.UseSpecial = true - case 's': - config.UseSpecial = false - case 'N': - config.UseNumber = true - case 'n': - config.UseNumber = false - case 'L': - config.UseLowerCase = true - case 'l': - config.UseLowerCase = false - case 'U': - config.UseUpperCase = true - case 'u': - config.UseUpperCase = false - case 'H': - config.HumanReadable = true - case 'h': - config.HumanReadable = false - case 'C': - config.useComplex = true - case 'c': - config.useComplex = false - default: - log.Fatalf("Unknown password style parameter: %q\n", string(curParam)) - } - } -} - -// GetPwLengthFromParams extracts the password length from the given cli flags and stores -// in the provided config object -func GetPwLengthFromParams(config *Config) int { - if config.MinPassLen > config.MaxPassLen { - config.MaxPassLen = config.MinPassLen - } - lenDiff := config.MaxPassLen - config.MinPassLen + 1 - randAdd, err := random.GetNum(lenDiff) - if err != nil { - log.Fatalf("Failed to generated password length: %v", err) - } - retVal := config.MinPassLen + randAdd - if retVal <= 0 { - return 1 - } - - return retVal -} diff --git a/docker-files/group b/docker-files/group deleted file mode 100644 index 4cbfc65..0000000 --- a/docker-files/group +++ /dev/null @@ -1 +0,0 @@ -apg-go:*:1000:apg-go diff --git a/docker-files/passwd b/docker-files/passwd deleted file mode 100644 index 8efc7d9..0000000 --- a/docker-files/passwd +++ /dev/null @@ -1 +0,0 @@ -apg-go:*:1000:1000:Automated Password Generator User:/apg-go:/usr/bin/false diff --git a/example-code/simple-password-generator/main.go b/example-code/simple-password-generator/main.go deleted file mode 100644 index 7289abb..0000000 --- a/example-code/simple-password-generator/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - "github.com/wneessen/apg-go/chars" - "github.com/wneessen/apg-go/config" - "github.com/wneessen/apg-go/random" -) - -func main() { - c := config.Config{ - UseNumber: true, - UseSpecial: true, - UseUpperCase: true, - UseLowerCase: true, - PwAlgo: 1, - MinPassLen: 15, - MaxPassLen: 15, - } - pl := config.GetPwLengthFromParams(&c) - cs := chars.GetRange(&c) - pw, err := random.GetChar(cs, pl) - if err != nil { - panic(err) - } - fmt.Println("Your Password:", pw) -} diff --git a/go.mod b/go.mod deleted file mode 100644 index a215295..0000000 --- a/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/wneessen/apg-go - -go 1.16 - -require github.com/wneessen/go-hibp v1.0.6 diff --git a/random/random.go b/random/random.go deleted file mode 100644 index ec0fdb9..0000000 --- a/random/random.go +++ /dev/null @@ -1,80 +0,0 @@ -package random - -import ( - "crypto/rand" - "encoding/binary" - "fmt" - "math/big" - "strings" -) - -// Bitmask sizes for the string generators (based on 93 chars total) -const ( - letterIdxBits = 7 // 7 bits to represent a letter index - letterIdxMask = 1<= 0; { - if r == 0 { - _, err := rand.Read(rp) - if err != nil { - return rs.String(), err - } - c, r = binary.BigEndian.Uint64(rp), letterIdxMax - } - if idx := int(c & letterIdxMask); idx < crl { - rs.WriteByte(cr[idx]) - i-- - } - c >>= letterIdxBits - r-- - } - return rs.String(), nil -} - -// GetNum generates a random number with given maximum value -func GetNum(maxNum int) (int, error) { - if maxNum <= 0 { - err := fmt.Errorf("provided maxNum is <= 0: %v", maxNum) - return 0, err - } - maxNumBigInt := big.NewInt(int64(maxNum)) - if !maxNumBigInt.IsUint64() { - err := fmt.Errorf("big.NewInt() generation returned negative value: %v", maxNumBigInt) - return 0, err - } - randNum64, err := rand.Int(rand.Reader, maxNumBigInt) - if err != nil { - return 0, err - } - randNum := int(randNum64.Int64()) - if randNum < 0 { - err := fmt.Errorf("generated random number does not fit as int64: %v", randNum64) - return 0, err - } - return randNum, nil -} - -// CoinFlip performs a simple coinflip based on the rand library and returns true or false -func CoinFlip() bool { - num := big.NewInt(2) - cf, _ := rand.Int(rand.Reader, num) - r := int(cf.Int64()) - return r == 1 -} diff --git a/spelling/spelling.go b/spelling/spelling.go deleted file mode 100644 index b4eb7f6..0000000 --- a/spelling/spelling.go +++ /dev/null @@ -1,140 +0,0 @@ -package spelling - -import ( - "fmt" - "github.com/wneessen/apg-go/chars" - "strings" -) - -var ( - symbNumNames = map[byte]string{ - '1': "ONE", - '2': "TWO", - '3': "THREE", - '4': "FOUR", - '5': "FIVE", - '6': "SIX", - '7': "SEVEN", - '8': "EIGHT", - '9': "NINE", - '0': "ZERO", - 33: "EXCLAMATION_POINT", - 34: "QUOTATION_MARK", - 35: "CROSSHATCH", - 36: "DOLLAR_SIGN", - 37: "PERCENT_SIGN", - 38: "AMPERSAND", - 39: "APOSTROPHE", - 40: "LEFT_PARENTHESIS", - 41: "RIGHT_PARENTHESIS", - 42: "ASTERISK", - 43: "PLUS_SIGN", - 44: "COMMA", - 45: "HYPHEN", - 46: "PERIOD", - 47: "SLASH", - 58: "COLON", - 59: "SEMICOLON", - 60: "LESS_THAN", - 61: "EQUAL_SIGN", - 62: "GREATER_THAN", - 63: "QUESTION_MARK", - 64: "AT_SIGN", - 91: "LEFT_BRACKET", - 92: "BACKSLASH", - 93: "RIGHT_BRACKET", - 94: "CIRCUMFLEX", - 95: "UNDERSCORE", - 96: "GRAVE", - 123: "LEFT_BRACE", - 124: "VERTICAL_BAR", - 125: "RIGHT_BRACE", - 126: "TILDE", - } - alphabetNames = map[byte]string{ - 'A': "Alfa", - 'B': "Bravo", - 'C': "Charlie", - 'D': "Delta", - 'E': "Echo", - 'F': "Foxtrot", - 'G': "Golf", - 'H': "Hotel", - 'I': "India", - 'J': "Juliett", - 'K': "Kilo", - 'L': "Lima", - 'M': "Mike", - 'N': "November", - 'O': "Oscar", - 'P': "Papa", - 'Q': "Quebec", - 'R': "Romeo", - 'S': "Sierra", - 'T': "Tango", - 'U': "Uniform", - 'V': "Victor", - 'W': "Whiskey", - 'X': "X_ray", - 'Y': "Yankee", - 'Z': "Zulu", - } -) - -// String returns an english spelled version of the given string -func String(pwString string) (string, error) { - var returnString []string - for _, curChar := range pwString { - curSpellString, err := ConvertCharToName(byte(curChar)) - if err != nil { - return "", err - } - returnString = append(returnString, curSpellString) - } - return strings.Join(returnString, "/"), nil -} - -// Koremutake returns the spelling of the Koremutake password with numbers and special -// chars spelled out in english language -func Koremutake(sylList []string) (string, error) { - var returnString []string - for _, curSyl := range sylList { - isKore := false - for _, x := range chars.KoremutakeSyllables { - if x == strings.ToLower(curSyl) { - isKore = true - } - } - - if isKore { - returnString = append(returnString, curSyl) - continue - } - - curSpellString, err := ConvertCharToName(curSyl[0]) - if err != nil { - return "", err - } - returnString = append(returnString, curSpellString) - } - return strings.Join(returnString, "-"), nil -} - -// ConvertCharToName converts a given ascii byte into the corresponding english spelled -// name -func ConvertCharToName(charByte byte) (string, error) { - var returnString string - if charByte > 64 && charByte < 91 { - returnString = alphabetNames[charByte] - } else if charByte > 96 && charByte < 123 { - returnString = strings.ToLower(alphabetNames[charByte-32]) - } else { - returnString = symbNumNames[charByte] - } - - if returnString == "" { - err := fmt.Errorf("cannot convert to character to name: %q is an unknown character", charByte) - return "", err - } - return returnString, nil -} From 2975dfea5154d6ccd3f4cd179dc9a55a5d57647f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 6 Apr 2023 12:23:22 +0200 Subject: [PATCH 02/53] Remove old code for the v2 refactor --- .idea/apg-go.iml | 9 --------- .idea/inspectionProfiles/Project_Default.xml | 12 ------------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ go.sum | 2 -- 5 files changed, 37 deletions(-) delete mode 100644 .idea/apg-go.iml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 go.sum diff --git a/.idea/apg-go.iml b/.idea/apg-go.iml deleted file mode 100644 index 5e764c4..0000000 --- a/.idea/apg-go.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 09dbac8..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 88c9fce..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/go.sum b/go.sum deleted file mode 100644 index 6379982..0000000 --- a/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/wneessen/go-hibp v1.0.6 h1:RpV540tVJpxefjCCctzq1cQaIFFlkd3nh+HhPVD6Hys= -github.com/wneessen/go-hibp v1.0.6/go.mod h1:Ldg6DQg4fMCveVKgL+RL9Jy+9TsljjAP704Ix8X3jOw= From e94b1ade5c1439b0749039ba40dd3bf4087ad15e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 18 Apr 2023 11:49:44 +0200 Subject: [PATCH 03/53] v2: Complete rework of the library and the client --- .golangci.toml | 16 +++++ .idea/.gitignore | 8 +++ apg.go | 12 ++++ cmd/apg/apg.go | 20 ++++++ go.mod | 3 + random.go | 127 +++++++++++++++++++++++++++++++++ random_test.go | 181 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 367 insertions(+) create mode 100644 .golangci.toml create mode 100644 .idea/.gitignore create mode 100644 apg.go create mode 100644 cmd/apg/apg.go create mode 100644 go.mod create mode 100644 random.go create mode 100644 random_test.go diff --git a/.golangci.toml b/.golangci.toml new file mode 100644 index 0000000..afd9684 --- /dev/null +++ b/.golangci.toml @@ -0,0 +1,16 @@ +## SPDX-FileCopyrightText: 2022 Winni Neessen +## +## SPDX-License-Identifier: MIT + +[run] +go = "1.20" +tests = true +skip-dirs = ["ui/"] + +[linters] +enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder", + "errname", "errorlint", "gofmt", "gofumpt", "goimports"] + +[linters-settings.goimports] +local-prefixes = "git.cgn.cleverbridge.com/infosec/vulnmon" + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/apg.go b/apg.go new file mode 100644 index 0000000..496a575 --- /dev/null +++ b/apg.go @@ -0,0 +1,12 @@ +package apg + +// Generator is the password generator type of the APG package +type Generator struct { + // charRange is the range of character used for the + charRange string +} + +// New returns a new password Generator type +func New() *Generator { + return &Generator{} +} diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go new file mode 100644 index 0000000..12ec9d0 --- /dev/null +++ b/cmd/apg/apg.go @@ -0,0 +1,20 @@ +// Package main is the APG command line client that makes use of the apg-go library + +package main + +import ( + "fmt" + "os" + + "github.com/wneessen/apg-go" +) + +func main() { + g := apg.New() + rb, err := g.RandomBytes(8) + if err != nil { + fmt.Println("ERROR", err) + os.Exit(1) + } + fmt.Printf("Random: %#v\n", rb) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..10fb223 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/wneessen/apg-go + +go 1.20 diff --git a/random.go b/random.go new file mode 100644 index 0000000..2b84deb --- /dev/null +++ b/random.go @@ -0,0 +1,127 @@ +package apg + +import ( + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "math/big" + "strings" +) + +const ( + // CharRangeAlphaLower represents all lower-case alphabetical characters + CharRangeAlphaLower = "abcdefghijklmnopqrstuvwxyz" + // CharRangeAlphaLowerHuman represents the human-readable lower-case alphabetical characters + CharRangeAlphaLowerHuman = "abcdefghjkmnpqrstuvwxyz" + // CharRangeAlphaUpper represents all upper-case alphabetical characters + CharRangeAlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + // CharRangeAlphaUpperHuman represents the human-readable upper-case alphabetical characters + CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ" + // CharRangeNumber represents all numerical characters + CharRangeNumber = "1234567890" + // CharRangeNumberHuman represents all human-readable numerical characters + CharRangeNumberHuman = "23456789" + // CharRangeSpecial represents all special characters + CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~` + // CharRangeSpecialHuman represents all human-readable special characters + CharRangeSpecialHuman = `#%*+-:;=` +) + +const ( + // 7 bits to represent a letter index + letterIdxBits = 7 + // All 1-bits, as many as letterIdxBits + letterIdxMask = 1<= 0; { + if r == 0 { + _, err := rand.Read(rp) + if err != nil { + return rs.String(), err + } + c, r = binary.BigEndian.Uint64(rp), letterIdxMax + } + if idx := int(c & letterIdxMask); idx < crl { + rs.WriteByte(cr[idx]) + i-- + } + c >>= letterIdxBits + r-- + } + + return rs.String(), nil +} + +// RandNum generates a random, non-negative number with given maximum value +func (g *Generator) RandNum(m int64) (int64, error) { + if m < 1 { + return 0, ErrInvalidLength + } + mbi := big.NewInt(m) + rn, err := rand.Int(rand.Reader, mbi) + if err != nil { + return 0, fmt.Errorf("random number generation failed: %w", err) + } + return rn.Int64(), nil +} + +// CoinFlip performs a simple coinflip based on the rand library and returns 1 or 0 +func (g *Generator) CoinFlip() int64 { + cf, _ := g.RandNum(2) + return cf +} + +// CoinFlipBool performs a simple coinflip based on the rand library and returns true or false +func (g *Generator) CoinFlipBool() bool { + return g.CoinFlip() == 1 +} diff --git a/random_test.go b/random_test.go new file mode 100644 index 0000000..651aeaa --- /dev/null +++ b/random_test.go @@ -0,0 +1,181 @@ +package apg + +import ( + "bytes" + "strings" + "testing" +) + +func TestGenerator_CoinFlip(t *testing.T) { + g := New() + cf := g.CoinFlip() + if cf < 0 || cf > 1 { + t.Errorf("CoinFlip failed(), expected 0 or 1, got: %d", cf) + } +} + +func TestGenerator_CoinFlipBool(t *testing.T) { + g := New() + gt := false + for i := 0; i < 500_000; i++ { + cf := g.CoinFlipBool() + if cf { + gt = true + break + } + } + if !gt { + t.Error("CoinFlipBool likely not working, expected at least one true value in 500k tries, got none") + } +} + +func TestGenerator_RandNum(t *testing.T) { + tt := []struct { + name string + v int64 + max int64 + min int64 + sf bool + }{ + {"RandNum up to 1000", 1000, 1000, 0, false}, + {"RandNum should be 1", 1, 1, 0, false}, + {"RandNum should fail on 1", 0, 0, 0, true}, + {"RandNum should fail on negative", -1, 0, 0, true}, + } + + g := New() + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + rn, err := g.RandNum(tc.v) + if err == nil && tc.sf { + t.Errorf("random number generation was supposed to fail, but didn't, got: %d", rn) + } + if err != nil && !tc.sf { + t.Errorf("random number generation failed: %s", err) + } + if rn > tc.max { + t.Errorf("random number generation returned too big number expected below: %d, got: %d", + tc.max, rn) + } + if rn < tc.min { + t.Errorf("random number generation returned too small number, expected min: %d, got: %d", + tc.min, rn) + } + }) + } +} + +func TestGenerator_RandomBytes(t *testing.T) { + tt := []struct { + name string + l int64 + sf bool + }{ + {"1 bytes of randomness", 1, false}, + {"100 bytes of randomness", 100, false}, + {"1024 bytes of randomness", 1024, false}, + {"4096 bytes of randomness", 4096, false}, + {"-1 bytes of randomness", -1, true}, + } + + g := New() + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + rb, err := g.RandomBytes(tc.l) + if err != nil && !tc.sf { + t.Errorf("random byte generation failed: %s", err) + return + } + if tc.sf { + return + } + bl := len(rb) + if int64(bl) != tc.l { + t.Errorf("lenght of provided bytes does not match requested length: got: %d, expected: %d", + bl, tc.l) + } + if bytes.Equal(rb, make([]byte, tc.l)) { + t.Errorf("random byte generation failed. returned slice is empty") + } + }) + } +} + +func TestGenerator_RandomString(t *testing.T) { + g := New() + l := 32 + tt := []struct { + name string + cr string + nr string + sf bool + }{ + { + "CharRange:AlphaLower", CharRangeAlphaLower, + CharRangeAlphaUpper + CharRangeNumber + CharRangeSpecial, false, + }, + { + "CharRange:AlphaUpper", CharRangeAlphaUpper, + CharRangeAlphaLower + CharRangeNumber + CharRangeSpecial, false, + }, + { + "CharRange:Number", CharRangeNumber, + CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeSpecial, false, + }, + { + "CharRange:Special", CharRangeSpecial, + CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumber, false, + }, + { + "CharRange:Invalid", "", + CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumber + CharRangeSpecial, true, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + rs, err := g.RandomString(l, tc.cr) + if err != nil && !tc.sf { + t.Errorf("RandomString failed: %s", err) + } + if len(rs) != l && !tc.sf { + t.Errorf("RandomString failed. Expected length: %d, got: %d", l, len(rs)) + } + if strings.ContainsAny(rs, tc.nr) { + t.Errorf("RandomString failed. Unexpected character found in returned string: %s", rs) + } + }) + } +} + +func BenchmarkGenerator_CoinFlip(b *testing.B) { + b.ReportAllocs() + g := New() + for i := 0; i < b.N; i++ { + _ = g.CoinFlip() + } +} + +func BenchmarkGenerator_RandomBytes(b *testing.B) { + b.ReportAllocs() + g := New() + var l int64 = 1024 + for i := 0; i < b.N; i++ { + _, err := g.RandomBytes(l) + if err != nil { + b.Errorf("failed to generate random bytes: %s", err) + return + } + } +} + +func BenchmarkGenerator_RandomString(b *testing.B) { + b.ReportAllocs() + g := New() + cr := CharRangeAlphaUpper + CharRangeAlphaLower + CharRangeNumber + CharRangeSpecial + for i := 0; i < b.N; i++ { + _, err := g.RandomString(32, cr) + if err != nil { + b.Errorf("RandomString() failed: %s", err) + } + } +} From ac97b94ec96f13b31d111d6a98f2af954f0ed84b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Aug 2023 16:02:58 +0200 Subject: [PATCH 04/53] Refactor generator and add config options Refactored the generator to include a new config option, changed function signatures to follow the new structure, and renamed the function 'RandomString' to 'RandomStringFromCharRange' for clarity. Also, added a new mode and algorithm feature to enhance password generation. Furthermore, added several tests for new features and configurations. Adapted the CLI to use the new configuration approach. This refactoring was necessary to improve the customizability and clarity of the password generation process. Fixed minor issues and added '.gitignore' for clean commits in the future. --- .gitignore | 29 +++++++++++++++++++++ algo.go | 29 +++++++++++++++++++++ apg.go | 11 ++++++-- cmd/apg/apg.go | 63 +++++++++++++++++++++++++++++++++++++++------ config.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ config_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ mode.go | 51 +++++++++++++++++++++++++++++++++++++ mode_test.go | 40 +++++++++++++++++++++++++++++ random.go | 25 +++--------------- random_test.go | 28 ++++++++++---------- 10 files changed, 366 insertions(+), 46 deletions(-) create mode 100644 .gitignore create mode 100644 algo.go create mode 100644 config.go create mode 100644 config_test.go create mode 100644 mode.go create mode 100644 mode_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f1a16a --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2022 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Local testfiles and auth data +.auth +examples/* + +# SonarQube +.scannerwork/ + +# IDEA specific ignores +.idea/ diff --git a/algo.go b/algo.go new file mode 100644 index 0000000..a1be7be --- /dev/null +++ b/algo.go @@ -0,0 +1,29 @@ +package apg + +// Algorithm is a type wrapper for an int type to represent different +// password generation algorithm +type Algorithm int + +const ( + // Pronouncable represents the algorithm for pronouncable passwords + // (koremutake syllables) + Pronouncable Algorithm = iota + // Random represents the algorithm for purely random passwords according + // to the provided password modes/flags + Random + // Unsupported represents an unsupported algorithm + Unsupported +) + +// IntToAlgo takes an int value as input and returns the corresponding +// Algorithm +func IntToAlgo(a int) Algorithm { + switch a { + case 0: + return Pronouncable + case 1: + return Random + default: + return Unsupported + } +} diff --git a/apg.go b/apg.go index 496a575..9aba57a 100644 --- a/apg.go +++ b/apg.go @@ -1,12 +1,19 @@ package apg +// VERSION represents the version string +const VERSION = "2.0.0" + // Generator is the password generator type of the APG package type Generator struct { // charRange is the range of character used for the charRange string + // config is a pointer to the apg config instance + config *Config } // New returns a new password Generator type -func New() *Generator { - return &Generator{} +func New(c *Config) *Generator { + return &Generator{ + config: c, + } } diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 12ec9d0..b8e361b 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -3,18 +3,65 @@ package main import ( - "fmt" + "flag" "os" "github.com/wneessen/apg-go" ) func main() { - g := apg.New() - rb, err := g.RandomBytes(8) - if err != nil { - fmt.Println("ERROR", err) - os.Exit(1) - } - fmt.Printf("Random: %#v\n", rb) + c := apg.NewConfig() + + // Configure and parse the CLI flags + flag.Int64Var(&c.MinLength, "m", c.MinLength, "") + flag.Int64Var(&c.MaxLength, "x", c.MaxLength, "") + flag.Int64Var(&c.NumberPass, "n", c.NumberPass, "") + flag.Usage = usage + flag.Parse() + + /* + g := apg.New(c) + rb, err := g.RandomBytes(c.MinLength) + if err != nil { + fmt.Println("ERROR", err) + os.Exit(1) + } + fmt.Printf("Random: %#v\n", rb) + + */ +} + +// usage is used by the flag package to display the CLI usage message +func usage() { + // Usage text + const ut = `apg-go // A "Automated Password Generator"-clone +Copyleft (c) 2021-2023 Winni Neessen + +apg [-a ] [-m ] [-x ] [-L] [-U] [-N] [-S] [-H] [-C] + [-l] [-M mode] [-E char_string] [-n num_of_pass] [-v] [-h] [-t] + +Options: + -a ALGORITH Choose the password generation algorithm (Default: 1) + - 0: pronounceable password generation (koremutake syllables) + - 1: random password generation according to password modes/flags + -m LENGTH Minimum length of the password to be generated (Default: 12) + -x LENGTH Maximum length of the password to be generated (Default: 20) + -n NUMBER Amount of password to be generated (Default: 6) + -E CHARS List of characters to be excluded in the generated password + -M [LUNSHClunshc] New style password parameters (upper case: on, lower case: off) + -L Use lower case characters in passwords (Default: on) + -U Use upper case characters in passwords (Default: on) + -N Use numeric characters in passwords (Default: on) + -S Use special characters in passwords (Default: off) + -H Avoid ambiguous characters in passwords (i. e.: 1, l, I, O, 0) (Default: off) + -C Enable complex password mode (implies -L -U -N -S and disables -H) (Default: off) + -l Spell generated passwords in phonetic alphabet (Default: off) + -p Check the HIBP database if the generated passwords was found in a leak before (Default: off) + - Note: this feature requires internet connectivity + -h Show this help text + -v Show version string + +` + + _, _ = os.Stderr.WriteString(ut) } diff --git a/config.go b/config.go new file mode 100644 index 0000000..286dde6 --- /dev/null +++ b/config.go @@ -0,0 +1,67 @@ +package apg + +// List of default values for Config instances +const ( + // DefaultMinLength reflects the default minimum length of a generated password + DefaultMinLength int64 = 12 + // DefaultMaxLength reflects the default maximum length of a generated password + DefaultMaxLength int64 = 20 + // DefaultNumberPass reflects the default amount of passwords returned by the generator + DefaultNumberPass int64 = 6 +) + +// Config represents the apg.Generator config parameters +type Config struct { + // Algo + Algorithm Algorithm + // MaxLength sets the maximum length for a generated password + MaxLength int64 + // MinLength sets the minimum length for a generated password + MinLength int64 + // NumberPass sets the number of passwords that are generated + // and returned by the generator + NumberPass int64 +} + +// Option is a function that can override default Config settings +type Option func(*Config) + +// NewConfig creates a new Config instance and pre-fills it with sane +// default settings. The Config is returned as pointer value +func NewConfig(o ...Option) *Config { + c := &Config{ + MaxLength: DefaultMaxLength, + MinLength: DefaultMinLength, + NumberPass: DefaultNumberPass, + } + + // Override defaults with optionally provided config.Option functions + for _, co := range o { + if co == nil { + continue + } + co(c) + } + return c +} + +// WithMinLength overrides the minimum password length +func WithMinLength(l int64) Option { + return func(c *Config) { + c.MinLength = l + } +} + +// WithMaxLength overrides the maximum password length +func WithMaxLength(l int64) Option { + return func(c *Config) { + c.MaxLength = l + } +} + +// WithNumberPass overrides the number of generated passwords setting +func WithNumberPass(n int64) Option { + return func(c *Config) { + c.NumberPass = n + } +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..71706ee --- /dev/null +++ b/config_test.go @@ -0,0 +1,69 @@ +package apg + +import ( + "testing" +) + +func TestNewConfig(t *testing.T) { + c := NewConfig() + if c == nil { + t.Errorf("NewConfig() failed, expected config pointer but got nil") + return + } + c = NewConfig(nil) + if c == nil { + t.Errorf("NewConfig() failed, expected config pointer but got nil") + return + } + if c.MinLength != DefaultMinLength { + t.Errorf("NewConfig() failed, expected min length: %d, got: %d", DefaultMinLength, + c.MinLength) + } + if c.MaxLength != DefaultMaxLength { + t.Errorf("NewConfig() failed, expected max length: %d, got: %d", DefaultMaxLength, + c.MaxLength) + } + if c.NumberPass != DefaultNumberPass { + t.Errorf("NewConfig() failed, expected number of passwords: %d, got: %d", + DefaultNumberPass, c.NumberPass) + } +} + +func TestWithMaxLength(t *testing.T) { + var e int64 = 123 + c := NewConfig(WithMaxLength(e)) + if c == nil { + t.Errorf("NewConfig(WithMaxLength()) failed, expected config pointer but got nil") + return + } + if c.MaxLength != e { + t.Errorf("NewConfig(WithMaxLength()) failed, expected max length: %d, got: %d", + e, c.MaxLength) + } +} + +func TestWithMinLength(t *testing.T) { + var e int64 = 1 + c := NewConfig(WithMinLength(e)) + if c == nil { + t.Errorf("NewConfig(WithMinLength()) failed, expected config pointer but got nil") + return + } + if c.MinLength != e { + t.Errorf("NewConfig(WithMinLength()) failed, expected min length: %d, got: %d", + e, c.MinLength) + } +} + +func TestWithNumberPass(t *testing.T) { + var e int64 = 123 + c := NewConfig(WithNumberPass(e)) + if c == nil { + t.Errorf("NewConfig(WithNumberPass()) failed, expected config pointer but got nil") + return + } + if c.NumberPass != e { + t.Errorf("NewConfig(WithNumberPass()) failed, expected number of passwords: %d, got: %d", + e, c.NumberPass) + } +} diff --git a/mode.go b/mode.go new file mode 100644 index 0000000..59ef878 --- /dev/null +++ b/mode.go @@ -0,0 +1,51 @@ +package apg + +// Mode represents a mode of characters +type Mode uint8 + +const ( + // ModeNumber sets the bitmask to include numbers in the generated passwords + ModeNumber = 1 << iota + // ModeLowerCase sets the bitmask to include lower case characters in the + // generated passwords + ModeLowerCase + // ModeUpperCase sets the bitmask to include upper case characters in the + // generated passwords + ModeUpperCase + // ModeSpecial sets the bitmask to include special characters in the + // generated passwords + ModeSpecial + // ModeHumanReadable sets the bitmask to generate human readable passwords + ModeHumanReadable +) + +const ( + // CharRangeAlphaLower represents all lower-case alphabetical characters + CharRangeAlphaLower = "abcdefghijklmnopqrstuvwxyz" + // CharRangeAlphaLowerHuman represents the human-readable lower-case alphabetical characters + CharRangeAlphaLowerHuman = "abcdefghjkmnpqrstuvwxyz" + // CharRangeAlphaUpper represents all upper-case alphabetical characters + CharRangeAlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + // CharRangeAlphaUpperHuman represents the human-readable upper-case alphabetical characters + CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ" + // CharRangeNumber represents all numerical characters + CharRangeNumber = "1234567890" + // CharRangeNumberHuman represents all human-readable numerical characters + CharRangeNumberHuman = "23456789" + // CharRangeSpecial represents all special characters + CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~` + // CharRangeSpecialHuman represents all human-readable special characters + CharRangeSpecialHuman = `#%*+-:;=` +) + +// SetMode sets a specific Mode to a given Mode bitmask +func SetMode(b, m Mode) Mode { return b | m } + +// ClearMode clears a specific Mode from a given Mode bitmask +func ClearMode(b, m Mode) Mode { return b &^ m } + +// ToggleMode toggles a specific Mode in a given Mode bitmask +func ToggleMode(b, m Mode) Mode { return b ^ m } + +// HasMode returns true if a given Mode bitmask holds a specific Mode +func HasMode(b, m Mode) bool { return b&m != 0 } diff --git a/mode_test.go b/mode_test.go new file mode 100644 index 0000000..80fa79d --- /dev/null +++ b/mode_test.go @@ -0,0 +1,40 @@ +package apg + +import ( + "testing" +) + +func TestSetClearHasToggleMode(t *testing.T) { + tt := []struct { + name string + mode Mode + }{ + {"ModeNumber", ModeNumber}, + {"ModeLowerCase", ModeLowerCase}, + {"ModeUpperCase", ModeUpperCase}, + {"ModeSpecial", ModeSpecial}, + {"ModeHumanReadable", ModeHumanReadable}, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + var m Mode + m = SetMode(m, tc.mode) + if !HasMode(m, tc.mode) { + t.Errorf("SetMode() failed, mode not found in bitmask") + } + m = ToggleMode(m, tc.mode) + if HasMode(m, tc.mode) { + t.Errorf("ToggleMode() failed, mode found in bitmask") + } + m = ToggleMode(m, tc.mode) + if !HasMode(m, tc.mode) { + t.Errorf("ToggleMode() failed, mode not found in bitmask") + } + m = ClearMode(m, tc.mode) + if HasMode(m, tc.mode) { + t.Errorf("ClearMode() failed, mode found in bitmask") + } + }) + } +} diff --git a/random.go b/random.go index 2b84deb..12f792c 100644 --- a/random.go +++ b/random.go @@ -9,25 +9,6 @@ import ( "strings" ) -const ( - // CharRangeAlphaLower represents all lower-case alphabetical characters - CharRangeAlphaLower = "abcdefghijklmnopqrstuvwxyz" - // CharRangeAlphaLowerHuman represents the human-readable lower-case alphabetical characters - CharRangeAlphaLowerHuman = "abcdefghjkmnpqrstuvwxyz" - // CharRangeAlphaUpper represents all upper-case alphabetical characters - CharRangeAlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - // CharRangeAlphaUpperHuman represents the human-readable upper-case alphabetical characters - CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ" - // CharRangeNumber represents all numerical characters - CharRangeNumber = "1234567890" - // CharRangeNumberHuman represents all human-readable numerical characters - CharRangeNumberHuman = "23456789" - // CharRangeSpecial represents all special characters - CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~` - // CharRangeSpecialHuman represents all human-readable special characters - CharRangeSpecialHuman = `#%*+-:;=` -) - const ( // 7 bits to represent a letter index letterIdxBits = 7 @@ -64,10 +45,10 @@ func (g *Generator) RandomBytes(n int64) ([]byte, error) { return b, nil } -// RandomString returns a random string of length l based of the range of characters given. +// RandomStringFromCharRange returns a random string of length l based of the range of characters given. // The method makes use of the crypto/random package and therfore is // cryptographically secure -func (g *Generator) RandomString(l int, cr string) (string, error) { +func (g *Generator) RandomStringFromCharRange(l int, cr string) (string, error) { if l < 1 { return "", ErrInvalidLength } @@ -85,7 +66,7 @@ func (g *Generator) RandomString(l int, cr string) (string, error) { } for i, c, r := l-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; { if r == 0 { - _, err := rand.Read(rp) + _, err = rand.Read(rp) if err != nil { return rs.String(), err } diff --git a/random_test.go b/random_test.go index 651aeaa..f7d0bca 100644 --- a/random_test.go +++ b/random_test.go @@ -7,7 +7,7 @@ import ( ) func TestGenerator_CoinFlip(t *testing.T) { - g := New() + g := New(NewConfig()) cf := g.CoinFlip() if cf < 0 || cf > 1 { t.Errorf("CoinFlip failed(), expected 0 or 1, got: %d", cf) @@ -15,7 +15,7 @@ func TestGenerator_CoinFlip(t *testing.T) { } func TestGenerator_CoinFlipBool(t *testing.T) { - g := New() + g := New(NewConfig()) gt := false for i := 0; i < 500_000; i++ { cf := g.CoinFlipBool() @@ -43,7 +43,7 @@ func TestGenerator_RandNum(t *testing.T) { {"RandNum should fail on negative", -1, 0, 0, true}, } - g := New() + g := New(NewConfig()) for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { rn, err := g.RandNum(tc.v) @@ -78,7 +78,7 @@ func TestGenerator_RandomBytes(t *testing.T) { {"-1 bytes of randomness", -1, true}, } - g := New() + g := New(NewConfig()) for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { rb, err := g.RandomBytes(tc.l) @@ -102,7 +102,7 @@ func TestGenerator_RandomBytes(t *testing.T) { } func TestGenerator_RandomString(t *testing.T) { - g := New() + g := New(NewConfig()) l := 32 tt := []struct { name string @@ -133,15 +133,15 @@ func TestGenerator_RandomString(t *testing.T) { } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - rs, err := g.RandomString(l, tc.cr) + rs, err := g.RandomStringFromCharRange(l, tc.cr) if err != nil && !tc.sf { - t.Errorf("RandomString failed: %s", err) + t.Errorf("RandomStringFromCharRange failed: %s", err) } if len(rs) != l && !tc.sf { - t.Errorf("RandomString failed. Expected length: %d, got: %d", l, len(rs)) + t.Errorf("RandomStringFromCharRange failed. Expected length: %d, got: %d", l, len(rs)) } if strings.ContainsAny(rs, tc.nr) { - t.Errorf("RandomString failed. Unexpected character found in returned string: %s", rs) + t.Errorf("RandomStringFromCharRange failed. Unexpected character found in returned string: %s", rs) } }) } @@ -149,7 +149,7 @@ func TestGenerator_RandomString(t *testing.T) { func BenchmarkGenerator_CoinFlip(b *testing.B) { b.ReportAllocs() - g := New() + g := New(NewConfig()) for i := 0; i < b.N; i++ { _ = g.CoinFlip() } @@ -157,7 +157,7 @@ func BenchmarkGenerator_CoinFlip(b *testing.B) { func BenchmarkGenerator_RandomBytes(b *testing.B) { b.ReportAllocs() - g := New() + g := New(NewConfig()) var l int64 = 1024 for i := 0; i < b.N; i++ { _, err := g.RandomBytes(l) @@ -170,12 +170,12 @@ func BenchmarkGenerator_RandomBytes(b *testing.B) { func BenchmarkGenerator_RandomString(b *testing.B) { b.ReportAllocs() - g := New() + g := New(NewConfig()) cr := CharRangeAlphaUpper + CharRangeAlphaLower + CharRangeNumber + CharRangeSpecial for i := 0; i < b.N; i++ { - _, err := g.RandomString(32, cr) + _, err := g.RandomStringFromCharRange(32, cr) if err != nil { - b.Errorf("RandomString() failed: %s", err) + b.Errorf("RandomStringFromCharRange() failed: %s", err) } } } From b31219046a128c8e81ad1e51c139d9b84c1e2ffb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Aug 2023 17:14:24 +0200 Subject: [PATCH 05/53] #53 Refactor mode handling and bitmask functions The naming and handling of mode bitmasks have been refactored for improved code readability and maintainability. The term "Mode" was replaced with "ModeMask" for clarity and all associated functions were renamed accordingly (e.g., "SetMode" to "MaskSetMode"). These changes provide better insight into the function of the code and increase understandability for future development efforts. The command-line utility now also supports specifying modes via the "-M" flag. --- cmd/apg/apg.go | 9 +++++++ config.go | 2 ++ mode.go | 72 ++++++++++++++++++++++++++++++++++++++++++++------ mode_test.go | 26 +++++++++--------- 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index b8e361b..32baf53 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -4,6 +4,7 @@ package main import ( "flag" + "fmt" "os" "github.com/wneessen/apg-go" @@ -13,12 +14,20 @@ func main() { c := apg.NewConfig() // Configure and parse the CLI flags + var ms string flag.Int64Var(&c.MinLength, "m", c.MinLength, "") flag.Int64Var(&c.MaxLength, "x", c.MaxLength, "") + flag.StringVar(&ms, "M", "", "") flag.Int64Var(&c.NumberPass, "n", c.NumberPass, "") flag.Usage = usage flag.Parse() + if ms != "" { + c.Modes = apg.ModesFromFlags(ms) + } + for _, m := range []apg.Mode{apg.ModeHumanReadable, apg.ModeLowerCase, apg.ModeNumber, apg.ModeSpecial, apg.ModeUpperCase} { + fmt.Printf("%s: %t\n", m, apg.MaskHasMode(c.Modes, m)) + } /* g := apg.New(c) rb, err := g.RandomBytes(c.MinLength) diff --git a/config.go b/config.go index 286dde6..7a4ea41 100644 --- a/config.go +++ b/config.go @@ -18,6 +18,8 @@ type Config struct { MaxLength int64 // MinLength sets the minimum length for a generated password MinLength int64 + // Modes holds the different character modes for the Random algorithm + Modes ModeMask // NumberPass sets the number of passwords that are generated // and returned by the generator NumberPass int64 diff --git a/mode.go b/mode.go index 59ef878..af7dd1e 100644 --- a/mode.go +++ b/mode.go @@ -1,8 +1,13 @@ package apg +import "strings" + // Mode represents a mode of characters type Mode uint8 +// ModeMask represents a bitmask of character modes +type ModeMask uint8 + const ( // ModeNumber sets the bitmask to include numbers in the generated passwords ModeNumber = 1 << iota @@ -38,14 +43,65 @@ const ( CharRangeSpecialHuman = `#%*+-:;=` ) -// SetMode sets a specific Mode to a given Mode bitmask -func SetMode(b, m Mode) Mode { return b | m } +// MaskSetMode sets a specific Mode to a given Mode bitmask +func MaskSetMode(ma ModeMask, mo Mode) ModeMask { return ModeMask(uint8(ma) | uint8(mo)) } -// ClearMode clears a specific Mode from a given Mode bitmask -func ClearMode(b, m Mode) Mode { return b &^ m } +// MaskClearMode clears a specific Mode from a given Mode bitmask +func MaskClearMode(ma ModeMask, mo Mode) ModeMask { return ModeMask(uint8(ma) &^ uint8(mo)) } -// ToggleMode toggles a specific Mode in a given Mode bitmask -func ToggleMode(b, m Mode) Mode { return b ^ m } +// MaskToggleMode toggles a specific Mode in a given Mode bitmask +func MaskToggleMode(ma ModeMask, mo Mode) ModeMask { return ModeMask(uint8(ma) ^ uint8(mo)) } -// HasMode returns true if a given Mode bitmask holds a specific Mode -func HasMode(b, m Mode) bool { return b&m != 0 } +// MaskHasMode returns true if a given Mode bitmask holds a specific Mode +func MaskHasMode(ma ModeMask, mo Mode) bool { return uint8(ma)&uint8(mo) != 0 } + +func ModesFromFlags(ms string) ModeMask { + var mm ModeMask + for _, m := range strings.Split(ms, "") { + switch m { + case "C": + mm = MaskSetMode(mm, ModeLowerCase|ModeNumber|ModeSpecial|ModeUpperCase) + case "h": + mm = MaskClearMode(mm, ModeHumanReadable) + case "H": + mm = MaskSetMode(mm, ModeHumanReadable) + case "l": + mm = MaskClearMode(mm, ModeLowerCase) + case "L": + mm = MaskSetMode(mm, ModeLowerCase) + case "n": + mm = MaskClearMode(mm, ModeNumber) + case "N": + mm = MaskSetMode(mm, ModeNumber) + case "s": + mm = MaskClearMode(mm, ModeSpecial) + case "S": + mm = MaskSetMode(mm, ModeSpecial) + case "u": + mm = MaskClearMode(mm, ModeUpperCase) + case "U": + mm = MaskSetMode(mm, ModeUpperCase) + } + } + + return mm + +} + +// String satisfies the fmt.Stringer interface for the Mode type +func (m Mode) String() string { + switch m { + case ModeHumanReadable: + return "Human-readable" + case ModeLowerCase: + return "Lower-case" + case ModeNumber: + return "Number" + case ModeSpecial: + return "Special" + case ModeUpperCase: + return "Upper-case" + default: + return "Unknown" + } +} diff --git a/mode_test.go b/mode_test.go index 80fa79d..f4a498b 100644 --- a/mode_test.go +++ b/mode_test.go @@ -18,22 +18,22 @@ func TestSetClearHasToggleMode(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - var m Mode - m = SetMode(m, tc.mode) - if !HasMode(m, tc.mode) { - t.Errorf("SetMode() failed, mode not found in bitmask") + var m ModeMask + m = MaskSetMode(m, tc.mode) + if !MaskHasMode(m, tc.mode) { + t.Errorf("MaskSetMode() failed, mode not found in bitmask") } - m = ToggleMode(m, tc.mode) - if HasMode(m, tc.mode) { - t.Errorf("ToggleMode() failed, mode found in bitmask") + m = MaskToggleMode(m, tc.mode) + if MaskHasMode(m, tc.mode) { + t.Errorf("MaskToggleMode() failed, mode found in bitmask") } - m = ToggleMode(m, tc.mode) - if !HasMode(m, tc.mode) { - t.Errorf("ToggleMode() failed, mode not found in bitmask") + m = MaskToggleMode(m, tc.mode) + if !MaskHasMode(m, tc.mode) { + t.Errorf("MaskToggleMode() failed, mode not found in bitmask") } - m = ClearMode(m, tc.mode) - if HasMode(m, tc.mode) { - t.Errorf("ClearMode() failed, mode found in bitmask") + m = MaskClearMode(m, tc.mode) + if MaskHasMode(m, tc.mode) { + t.Errorf("MaskClearMode() failed, mode found in bitmask") } }) } From 704269d0b8dc72799b5fb4387742ac2e99ad1604 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Aug 2023 18:38:32 +0200 Subject: [PATCH 06/53] #53 Reorder modes and add new test case for mode flags This commit primarily reorders the modes in the "mode_test.go" file for consistency. The order is now; ModeHumanReadable, ModeLowerCase, ModeNumber, ModeSpecial, ModeUpperCase. This now follows a logical order instead of the previous semi arbitrary one. The commit also involves the addition of a new test case 'TestModesFromFlags' in the "mode_test.go" file. This test case aims at increasing the code coverage and ensuring modes obtained from the flags are correct. Lastly, the 'ModesFromFlags' function in the "mode.go" has been slightly refactored for improved readability. This does not change the functionality and thus won't affect the rest of the codebase. --- mode.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mode.go b/mode.go index af7dd1e..595906c 100644 --- a/mode.go +++ b/mode.go @@ -1,6 +1,8 @@ package apg -import "strings" +import ( + "strings" +) // Mode represents a mode of characters type Mode uint8 @@ -56,8 +58,9 @@ func MaskToggleMode(ma ModeMask, mo Mode) ModeMask { return ModeMask(uint8(ma) ^ func MaskHasMode(ma ModeMask, mo Mode) bool { return uint8(ma)&uint8(mo) != 0 } func ModesFromFlags(ms string) ModeMask { + cl := strings.Split(ms, "") var mm ModeMask - for _, m := range strings.Split(ms, "") { + for _, m := range cl { switch m { case "C": mm = MaskSetMode(mm, ModeLowerCase|ModeNumber|ModeSpecial|ModeUpperCase) From f3a4b516d1154b9c155a04c49d851bacfbd5004b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Aug 2023 18:38:55 +0200 Subject: [PATCH 07/53] Reorder modes and add new test for modes The ordering of modes in the existing test was changed to logically group them together. In addition, a new test was added, `TestModesFromFlags`, to cover multiple different conditions including complex modes and cases where certain mode elements are excluded. This expansion in testing enhances our coverage of varied mode settings. --- mode_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/mode_test.go b/mode_test.go index f4a498b..9d5a324 100644 --- a/mode_test.go +++ b/mode_test.go @@ -9,11 +9,11 @@ func TestSetClearHasToggleMode(t *testing.T) { name string mode Mode }{ - {"ModeNumber", ModeNumber}, - {"ModeLowerCase", ModeLowerCase}, - {"ModeUpperCase", ModeUpperCase}, - {"ModeSpecial", ModeSpecial}, {"ModeHumanReadable", ModeHumanReadable}, + {"ModeLowerCase", ModeLowerCase}, + {"ModeNumber", ModeNumber}, + {"ModeSpecial", ModeSpecial}, + {"ModeUpperCase", ModeUpperCase}, } for _, tc := range tt { @@ -38,3 +38,41 @@ func TestSetClearHasToggleMode(t *testing.T) { }) } } + +func TestModesFromFlags(t *testing.T) { + tt := []struct { + name string + ms string + mode []Mode + }{ + {"ModeComplex", "C", []Mode{ModeLowerCase, ModeNumber, ModeSpecial, + ModeUpperCase}}, + {"ModeHumanReadable", "H", []Mode{ModeHumanReadable}}, + {"ModeLowerCase", "L", []Mode{ModeLowerCase}}, + {"ModeNumber", "N", []Mode{ModeNumber}}, + {"ModeUpperCase", "U", []Mode{ModeUpperCase}}, + {"ModeSpecial", "S", []Mode{ModeSpecial}}, + {"ModeLowerSpecialUpper", "LSU", []Mode{ModeLowerCase, ModeSpecial, + ModeUpperCase}}, + {"ModeComplexNoLower", "Cl", []Mode{ModeNumber, ModeSpecial, + ModeUpperCase}}, + {"ModeComplexNoNumber", "Cn", []Mode{ModeLowerCase, ModeSpecial, + ModeUpperCase}}, + {"ModeComplexNoSpecial", "Cs", []Mode{ModeLowerCase, ModeNumber, + ModeUpperCase}}, + {"ModeComplexNoUpper", "Cu", []Mode{ModeLowerCase, ModeNumber, + ModeSpecial}}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + var mm ModeMask + mm = ModesFromFlags(tc.ms) + for _, tm := range tc.mode { + if !MaskHasMode(mm, tm) { + t.Errorf("ModesFromFlags() failed, expected mode %q not found", + tm) + } + } + }) + } +} From 3cccc654983ed385d292e2837bfb78a166ca1697 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Aug 2023 20:29:43 +0200 Subject: [PATCH 08/53] #53 Add ModeHumanReadable and refactor mode tests Updated mode tests to include a new Mode, ModeHumanReadable, and created an additional test case, ModeComplexNoHumanReadable. This was done because a more human readable mode was required for some use cases. Also added a test function TestMode_String to ensure the .String() method of each mode works as intended. --- mode_test.go | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/mode_test.go b/mode_test.go index 9d5a324..7eae4a1 100644 --- a/mode_test.go +++ b/mode_test.go @@ -52,8 +52,10 @@ func TestModesFromFlags(t *testing.T) { {"ModeNumber", "N", []Mode{ModeNumber}}, {"ModeUpperCase", "U", []Mode{ModeUpperCase}}, {"ModeSpecial", "S", []Mode{ModeSpecial}}, - {"ModeLowerSpecialUpper", "LSU", []Mode{ModeLowerCase, ModeSpecial, - ModeUpperCase}}, + {"ModeLowerSpecialUpper", "LSUH", []Mode{ModeHumanReadable, + ModeLowerCase, ModeSpecial, ModeUpperCase}}, + {"ModeComplexNoHumanReadable", "Ch", []Mode{ModeLowerCase, + ModeNumber, ModeSpecial, ModeUpperCase}}, {"ModeComplexNoLower", "Cl", []Mode{ModeNumber, ModeSpecial, ModeUpperCase}}, {"ModeComplexNoNumber", "Cn", []Mode{ModeLowerCase, ModeSpecial, @@ -76,3 +78,26 @@ func TestModesFromFlags(t *testing.T) { }) } } + +func TestMode_String(t *testing.T) { + tt := []struct { + name string + m Mode + e string + }{ + {"ModeHumanReadable", ModeHumanReadable, "Human-readable"}, + {"ModeLowerCase", ModeLowerCase, "Lower-case"}, + {"ModeNumber", ModeNumber, "Number"}, + {"ModeSpecial", ModeSpecial, "Special"}, + {"ModeUpperCase", ModeUpperCase, "Upper-case"}, + {"ModeUnknown", 255, "Unknown"}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + if tc.m.String() != tc.e { + t.Errorf("Mode.String() failed, expected: %s, got: %s", tc.e, + tc.m.String()) + } + }) + } +} From 69bb1e4cb749e77e5073b90d2dcb212370de9d97 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Aug 2023 20:35:31 +0200 Subject: [PATCH 09/53] #53 Add tests and refactor Algorithm constants Test suite `TestIntToAlgo` was added to `algo_test.go` to validate `IntToAlgo` function. Additionally, Algorithm constants in `algo.go` have been prefixed with "Algo". These changes were made in order to make the code clearer and ensure the function returns the expected values for the given inputs. --- algo.go | 18 +++++++++--------- algo_test.go | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 algo_test.go diff --git a/algo.go b/algo.go index a1be7be..5cfc5f4 100644 --- a/algo.go +++ b/algo.go @@ -5,14 +5,14 @@ package apg type Algorithm int const ( - // Pronouncable represents the algorithm for pronouncable passwords + // AlgoPronouncable represents the algorithm for pronouncable passwords // (koremutake syllables) - Pronouncable Algorithm = iota - // Random represents the algorithm for purely random passwords according + AlgoPronouncable Algorithm = iota + // AlgoRandom represents the algorithm for purely random passwords according // to the provided password modes/flags - Random - // Unsupported represents an unsupported algorithm - Unsupported + AlgoRandom + // AlgoUnsupported represents an unsupported algorithm + AlgoUnsupported ) // IntToAlgo takes an int value as input and returns the corresponding @@ -20,10 +20,10 @@ const ( func IntToAlgo(a int) Algorithm { switch a { case 0: - return Pronouncable + return AlgoPronouncable case 1: - return Random + return AlgoRandom default: - return Unsupported + return AlgoUnsupported } } diff --git a/algo_test.go b/algo_test.go new file mode 100644 index 0000000..b0e2e05 --- /dev/null +++ b/algo_test.go @@ -0,0 +1,23 @@ +package apg + +import "testing" + +func TestIntToAlgo(t *testing.T) { + tt := []struct { + name string + a int + e Algorithm + }{ + {"AlgoPronouncable", 0, AlgoPronouncable}, + {"AlgoRandom", 1, AlgoRandom}, + {"AlgoUnsupported", 2, AlgoUnsupported}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + a := IntToAlgo(tc.a) + if a != tc.e { + t.Errorf("IntToAlgo() failed, expected: %d, got: %d", tc.e, a) + } + }) + } +} From 1e1ae45e743abbe5519e2e533d25b1b2b7e9f139 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Aug 2023 14:47:30 +0200 Subject: [PATCH 10/53] #53 Refactor password generator to include additional modes Updated the password generator to include default modes for characters along with options to enable complex password mode and toggle specific character types in passwords from the command line. This allows for greater customization and more user control in password generation, especially useful for applications with unique password requirements." --- cmd/apg/apg.go | 56 +++++++++++++++++++++++++++++++++++++++----------- config.go | 8 ++++++-- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 32baf53..9a0666b 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -14,7 +14,15 @@ func main() { c := apg.NewConfig() // Configure and parse the CLI flags + // See usage() for flag details var ms string + var co, hr, lc, nu, sp, uc bool + flag.BoolVar(&lc, "L", false, "") + flag.BoolVar(&uc, "U", false, "") + flag.BoolVar(&nu, "N", false, "") + flag.BoolVar(&sp, "S", false, "") + flag.BoolVar(&co, "C", false, "") + flag.BoolVar(&hr, "H", false, "") flag.Int64Var(&c.MinLength, "m", c.MinLength, "") flag.Int64Var(&c.MaxLength, "x", c.MaxLength, "") flag.StringVar(&ms, "M", "", "") @@ -22,11 +30,34 @@ func main() { flag.Usage = usage flag.Parse() + // Old style character modes + if hr { + c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeHumanReadable) + } + if lc { + c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeLowerCase) + } + if uc { + c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeUpperCase) + } + if nu { + c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeNumber) + } + if sp { + c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeSpecial) + } + if co { + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeLowerCase|apg.ModeNumber| + apg.ModeSpecial|apg.ModeUpperCase) + c.Mode = apg.MaskClearMode(c.Mode, apg.ModeHumanReadable) + } + + // New style character modes (has higher priority than the old style modes) if ms != "" { - c.Modes = apg.ModesFromFlags(ms) + c.Mode = apg.ModesFromFlags(ms) } for _, m := range []apg.Mode{apg.ModeHumanReadable, apg.ModeLowerCase, apg.ModeNumber, apg.ModeSpecial, apg.ModeUpperCase} { - fmt.Printf("%s: %t\n", m, apg.MaskHasMode(c.Modes, m)) + fmt.Printf("%s: %t\n", m, apg.MaskHasMode(c.Mode, m)) } /* g := apg.New(c) @@ -51,26 +82,27 @@ apg [-a ] [-m ] [-x ] [-L] [-U] [-N] [-S] [-H] [-C] Options: -a ALGORITH Choose the password generation algorithm (Default: 1) - - 0: pronounceable password generation (koremutake syllables) - - 1: random password generation according to password modes/flags + - 0: pronounceable password generation (koremutake syllables) + - 1: random password generation according to password modes/flags -m LENGTH Minimum length of the password to be generated (Default: 12) -x LENGTH Maximum length of the password to be generated (Default: 20) -n NUMBER Amount of password to be generated (Default: 6) -E CHARS List of characters to be excluded in the generated password - -M [LUNSHClunshc] New style password parameters (upper case: on, lower case: off) - -L Use lower case characters in passwords (Default: on) - -U Use upper case characters in passwords (Default: on) - -N Use numeric characters in passwords (Default: on) - -S Use special characters in passwords (Default: off) + -M [LUNSHClunshc] New style password parameters + - Note: new-style flags have higher priority than any of the old-style flags + -L Toggle lower case characters in passwords (Default: on) + -U Toggle upper case characters in passwords (Default: on) + -N Toggle numeric characters in passwords (Default: on) + -S Toggle special characters in passwords (Default: off) -H Avoid ambiguous characters in passwords (i. e.: 1, l, I, O, 0) (Default: off) - -C Enable complex password mode (implies -L -U -N -S and disables -H) (Default: off) + -C Enable complex password mode (implies -L -U -N -S and disables -H) + - Note: this flag has higher priority than the other old-style flags -l Spell generated passwords in phonetic alphabet (Default: off) -p Check the HIBP database if the generated passwords was found in a leak before (Default: off) - - Note: this feature requires internet connectivity + - Note: this feature requires internet connectivity -h Show this help text -v Show version string ` - _, _ = os.Stderr.WriteString(ut) } diff --git a/config.go b/config.go index 7a4ea41..9a0e405 100644 --- a/config.go +++ b/config.go @@ -6,6 +6,9 @@ const ( DefaultMinLength int64 = 12 // DefaultMaxLength reflects the default maximum length of a generated password DefaultMaxLength int64 = 20 + // DefaultMode sets the default character set mode bitmask to a combination of + // lower- and upper-case characters as well as numbers + DefaultMode ModeMask = ModeLowerCase | ModeNumber | ModeUpperCase // DefaultNumberPass reflects the default amount of passwords returned by the generator DefaultNumberPass int64 = 6 ) @@ -18,8 +21,8 @@ type Config struct { MaxLength int64 // MinLength sets the minimum length for a generated password MinLength int64 - // Modes holds the different character modes for the Random algorithm - Modes ModeMask + // Mode holds the different character modes for the Random algorithm + Mode ModeMask // NumberPass sets the number of passwords that are generated // and returned by the generator NumberPass int64 @@ -34,6 +37,7 @@ func NewConfig(o ...Option) *Config { c := &Config{ MaxLength: DefaultMaxLength, MinLength: DefaultMinLength, + Mode: DefaultMode, NumberPass: DefaultNumberPass, } From 499a82d8847368a2d78f2c709c8f50efd0137b6b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Aug 2023 15:04:34 +0200 Subject: [PATCH 11/53] #53 Add fixed length option for password generation A FixedLength field was added to the Config struct and a corresponding command line flag was added in `apg.go`. The field allows for the generation of passwords of a fixed length, overriding the MinLength and MaxLength values if present. Revised the `random.go` script to accommodate this change. The option for fixed length enhances the flexibility and customization of the password generation tool. --- cmd/apg/apg.go | 23 +++++++++++------------ config.go | 3 +++ random.go | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 9a0666b..05cddd2 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -23,6 +23,7 @@ func main() { flag.BoolVar(&sp, "S", false, "") flag.BoolVar(&co, "C", false, "") flag.BoolVar(&hr, "H", false, "") + flag.Int64Var(&c.FixedLength, "f", 0, "") flag.Int64Var(&c.MinLength, "m", c.MinLength, "") flag.Int64Var(&c.MaxLength, "x", c.MaxLength, "") flag.StringVar(&ms, "M", "", "") @@ -56,19 +57,16 @@ func main() { if ms != "" { c.Mode = apg.ModesFromFlags(ms) } - for _, m := range []apg.Mode{apg.ModeHumanReadable, apg.ModeLowerCase, apg.ModeNumber, apg.ModeSpecial, apg.ModeUpperCase} { - fmt.Printf("%s: %t\n", m, apg.MaskHasMode(c.Mode, m)) - } - /* - g := apg.New(c) - rb, err := g.RandomBytes(c.MinLength) - if err != nil { - fmt.Println("ERROR", err) - os.Exit(1) - } - fmt.Printf("Random: %#v\n", rb) - */ + // Generate the password based on the given flags + g := apg.New(c) + for i := int64(0); i < c.NumberPass; i++ { + pl, err := g.GetPasswordLength() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Error during password generation: %s\n", err) + } + fmt.Printf("PW length: %d\n", pl) + } } // usage is used by the flag package to display the CLI usage message @@ -86,6 +84,7 @@ Options: - 1: random password generation according to password modes/flags -m LENGTH Minimum length of the password to be generated (Default: 12) -x LENGTH Maximum length of the password to be generated (Default: 20) + -f LENGTH Fixed length of the password to be generated (Ignores -m and -x) -n NUMBER Amount of password to be generated (Default: 6) -E CHARS List of characters to be excluded in the generated password -M [LUNSHClunshc] New style password parameters diff --git a/config.go b/config.go index 9a0e405..ef2378c 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,9 @@ const ( type Config struct { // Algo Algorithm Algorithm + // FixedLength sets a fixed length for generated passwords and ignores + // the MinLength and MaxLength values + FixedLength int64 // MaxLength sets the maximum length for a generated password MaxLength int64 // MinLength sets the minimum length for a generated password diff --git a/random.go b/random.go index 12f792c..5b653d7 100644 --- a/random.go +++ b/random.go @@ -106,3 +106,26 @@ func (g *Generator) CoinFlip() int64 { func (g *Generator) CoinFlipBool() bool { return g.CoinFlip() == 1 } + +// GetPasswordLength returns the password length based on the given config +// parameters +func (g *Generator) GetPasswordLength() (int64, error) { + if g.config.FixedLength > 0 { + return g.config.FixedLength, nil + } + mil := g.config.MinLength + mal := g.config.MaxLength + if mil > mal { + mal = mil + } + diff := mal - mil + 1 + ra, err := g.RandNum(diff) + if err != nil { + return 0, fmt.Errorf("failed to calculate password length: %w", err) + } + l := mil + ra + if l <= 0 { + return 1, nil + } + return l, nil +} From d28b0645fd1aad101caa4c45d17a6a9fc6378f99 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Aug 2023 15:14:43 +0200 Subject: [PATCH 12/53] #53 Update CLI usage message in apg.go The usage message has been updated in apg.go to improve its clarity and usefulness. Changes include displaying the version number dynamically, providing the project's GitHub link, changing the term 'options' to 'flags', and more precise terminology in descriptions. Also, formatting has been adjusted to improve readability. --- cmd/apg/apg.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 05cddd2..f5fc1f5 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -72,13 +72,15 @@ func main() { // usage is used by the flag package to display the CLI usage message func usage() { // Usage text - const ut = `apg-go // A "Automated Password Generator"-clone -Copyleft (c) 2021-2023 Winni Neessen + const ut = `apg-go v` + + apg.VERSION + "\n" + + `A OSS "Automated Password Generator"-clone -- https://github.com/wneessen/apg-go/ +Created 2021-2023 by Winni Neessen (MIT licensed) apg [-a ] [-m ] [-x ] [-L] [-U] [-N] [-S] [-H] [-C] [-l] [-M mode] [-E char_string] [-n num_of_pass] [-v] [-h] [-t] -Options: +Flags: -a ALGORITH Choose the password generation algorithm (Default: 1) - 0: pronounceable password generation (koremutake syllables) - 1: random password generation according to password modes/flags @@ -87,7 +89,7 @@ Options: -f LENGTH Fixed length of the password to be generated (Ignores -m and -x) -n NUMBER Amount of password to be generated (Default: 6) -E CHARS List of characters to be excluded in the generated password - -M [LUNSHClunshc] New style password parameters + -M [LUNSHClunshc] New style password flags - Note: new-style flags have higher priority than any of the old-style flags -L Toggle lower case characters in passwords (Default: on) -U Toggle upper case characters in passwords (Default: on) @@ -100,8 +102,7 @@ Options: -p Check the HIBP database if the generated passwords was found in a leak before (Default: off) - Note: this feature requires internet connectivity -h Show this help text - -v Show version string + -v Show version string` -` - _, _ = os.Stderr.WriteString(ut) + _, _ = os.Stderr.WriteString(ut + "\n\n") } From af6d87c1a3a755d36cdd4f3913a24488ddd1f325 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Aug 2023 17:54:41 +0200 Subject: [PATCH 13/53] #53 and #52: Update "Number" references to "Numeric" in password generator Replaced all instances of "Number" with "Numeric" in the password generator. This modification will make the password generation code more intuitive and readable. Moreover, minimum character requirements were added for each character type, giving more flexibility to the password generation settings. --- cmd/apg/apg.go | 20 ++++++++++++++------ config.go | 8 ++++++-- mode.go | 18 +++++++++--------- mode_test.go | 16 ++++++++-------- random_test.go | 12 ++++++------ 5 files changed, 43 insertions(+), 31 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index f5fc1f5..2e5b0ba 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -18,9 +18,13 @@ func main() { var ms string var co, hr, lc, nu, sp, uc bool flag.BoolVar(&lc, "L", false, "") + flag.Int64Var(&c.MinLowerCase, "mL", c.MinLowerCase, "") flag.BoolVar(&uc, "U", false, "") + flag.Int64Var(&c.MinUpperCase, "mU", c.MinUpperCase, "") flag.BoolVar(&nu, "N", false, "") + flag.Int64Var(&c.MinNumeric, "mN", c.MinNumeric, "") flag.BoolVar(&sp, "S", false, "") + flag.Int64Var(&c.MinSpecial, "mS", c.MinSpecial, "") flag.BoolVar(&co, "C", false, "") flag.BoolVar(&hr, "H", false, "") flag.Int64Var(&c.FixedLength, "f", 0, "") @@ -42,13 +46,13 @@ func main() { c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeUpperCase) } if nu { - c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeNumber) + c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeNumeric) } if sp { c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeSpecial) } if co { - c.Mode = apg.MaskSetMode(c.Mode, apg.ModeLowerCase|apg.ModeNumber| + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeLowerCase|apg.ModeNumeric| apg.ModeSpecial|apg.ModeUpperCase) c.Mode = apg.MaskClearMode(c.Mode, apg.ModeHumanReadable) } @@ -91,12 +95,16 @@ Flags: -E CHARS List of characters to be excluded in the generated password -M [LUNSHClunshc] New style password flags - Note: new-style flags have higher priority than any of the old-style flags - -L Toggle lower case characters in passwords (Default: on) - -U Toggle upper case characters in passwords (Default: on) + -mL NUMBER Minimal amount of lower-case characters (implies -L) + -mN NUMBER Minimal amount of numeric characters (imlies -N) + -mS NUMBER Minimal amount of special characters (imlies -S) + -mU NUMBER Minimal amount of upper-case characters (imlies -U) + -C Enable complex password mode (implies -L -U -N -S and disables -H) + -H Avoid ambiguous characters in passwords (i. e.: 1, l, I, O, 0) (Default: off) + -L Toggle lower-case characters in passwords (Default: on) -N Toggle numeric characters in passwords (Default: on) -S Toggle special characters in passwords (Default: off) - -H Avoid ambiguous characters in passwords (i. e.: 1, l, I, O, 0) (Default: off) - -C Enable complex password mode (implies -L -U -N -S and disables -H) + -U Toggle upper-case characters in passwords (Default: on) - Note: this flag has higher priority than the other old-style flags -l Spell generated passwords in phonetic alphabet (Default: off) -p Check the HIBP database if the generated passwords was found in a leak before (Default: off) diff --git a/config.go b/config.go index ef2378c..6358a34 100644 --- a/config.go +++ b/config.go @@ -8,7 +8,7 @@ const ( DefaultMaxLength int64 = 20 // DefaultMode sets the default character set mode bitmask to a combination of // lower- and upper-case characters as well as numbers - DefaultMode ModeMask = ModeLowerCase | ModeNumber | ModeUpperCase + DefaultMode ModeMask = ModeLowerCase | ModeNumeric | ModeUpperCase // DefaultNumberPass reflects the default amount of passwords returned by the generator DefaultNumberPass int64 = 6 ) @@ -23,7 +23,11 @@ type Config struct { // MaxLength sets the maximum length for a generated password MaxLength int64 // MinLength sets the minimum length for a generated password - MinLength int64 + MinLength int64 + MinLowerCase int64 + MinNumeric int64 + MinSpecial int64 + MinUpperCase int64 // Mode holds the different character modes for the Random algorithm Mode ModeMask // NumberPass sets the number of passwords that are generated diff --git a/mode.go b/mode.go index 595906c..3595114 100644 --- a/mode.go +++ b/mode.go @@ -11,8 +11,8 @@ type Mode uint8 type ModeMask uint8 const ( - // ModeNumber sets the bitmask to include numbers in the generated passwords - ModeNumber = 1 << iota + // ModeNumeric sets the bitmask to include numeric in the generated passwords + ModeNumeric = 1 << iota // ModeLowerCase sets the bitmask to include lower case characters in the // generated passwords ModeLowerCase @@ -35,8 +35,8 @@ const ( CharRangeAlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" // CharRangeAlphaUpperHuman represents the human-readable upper-case alphabetical characters CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ" - // CharRangeNumber represents all numerical characters - CharRangeNumber = "1234567890" + // CharRangeNumeric represents all numerical characters + CharRangeNumeric = "1234567890" // CharRangeNumberHuman represents all human-readable numerical characters CharRangeNumberHuman = "23456789" // CharRangeSpecial represents all special characters @@ -63,7 +63,7 @@ func ModesFromFlags(ms string) ModeMask { for _, m := range cl { switch m { case "C": - mm = MaskSetMode(mm, ModeLowerCase|ModeNumber|ModeSpecial|ModeUpperCase) + mm = MaskSetMode(mm, ModeLowerCase|ModeNumeric|ModeSpecial|ModeUpperCase) case "h": mm = MaskClearMode(mm, ModeHumanReadable) case "H": @@ -73,9 +73,9 @@ func ModesFromFlags(ms string) ModeMask { case "L": mm = MaskSetMode(mm, ModeLowerCase) case "n": - mm = MaskClearMode(mm, ModeNumber) + mm = MaskClearMode(mm, ModeNumeric) case "N": - mm = MaskSetMode(mm, ModeNumber) + mm = MaskSetMode(mm, ModeNumeric) case "s": mm = MaskClearMode(mm, ModeSpecial) case "S": @@ -98,8 +98,8 @@ func (m Mode) String() string { return "Human-readable" case ModeLowerCase: return "Lower-case" - case ModeNumber: - return "Number" + case ModeNumeric: + return "Numeric" case ModeSpecial: return "Special" case ModeUpperCase: diff --git a/mode_test.go b/mode_test.go index 7eae4a1..bf2175f 100644 --- a/mode_test.go +++ b/mode_test.go @@ -11,7 +11,7 @@ func TestSetClearHasToggleMode(t *testing.T) { }{ {"ModeHumanReadable", ModeHumanReadable}, {"ModeLowerCase", ModeLowerCase}, - {"ModeNumber", ModeNumber}, + {"ModeNumeric", ModeNumeric}, {"ModeSpecial", ModeSpecial}, {"ModeUpperCase", ModeUpperCase}, } @@ -45,24 +45,24 @@ func TestModesFromFlags(t *testing.T) { ms string mode []Mode }{ - {"ModeComplex", "C", []Mode{ModeLowerCase, ModeNumber, ModeSpecial, + {"ModeComplex", "C", []Mode{ModeLowerCase, ModeNumeric, ModeSpecial, ModeUpperCase}}, {"ModeHumanReadable", "H", []Mode{ModeHumanReadable}}, {"ModeLowerCase", "L", []Mode{ModeLowerCase}}, - {"ModeNumber", "N", []Mode{ModeNumber}}, + {"ModeNumeric", "N", []Mode{ModeNumeric}}, {"ModeUpperCase", "U", []Mode{ModeUpperCase}}, {"ModeSpecial", "S", []Mode{ModeSpecial}}, {"ModeLowerSpecialUpper", "LSUH", []Mode{ModeHumanReadable, ModeLowerCase, ModeSpecial, ModeUpperCase}}, {"ModeComplexNoHumanReadable", "Ch", []Mode{ModeLowerCase, - ModeNumber, ModeSpecial, ModeUpperCase}}, - {"ModeComplexNoLower", "Cl", []Mode{ModeNumber, ModeSpecial, + ModeNumeric, ModeSpecial, ModeUpperCase}}, + {"ModeComplexNoLower", "Cl", []Mode{ModeNumeric, ModeSpecial, ModeUpperCase}}, {"ModeComplexNoNumber", "Cn", []Mode{ModeLowerCase, ModeSpecial, ModeUpperCase}}, - {"ModeComplexNoSpecial", "Cs", []Mode{ModeLowerCase, ModeNumber, + {"ModeComplexNoSpecial", "Cs", []Mode{ModeLowerCase, ModeNumeric, ModeUpperCase}}, - {"ModeComplexNoUpper", "Cu", []Mode{ModeLowerCase, ModeNumber, + {"ModeComplexNoUpper", "Cu", []Mode{ModeLowerCase, ModeNumeric, ModeSpecial}}, } for _, tc := range tt { @@ -87,7 +87,7 @@ func TestMode_String(t *testing.T) { }{ {"ModeHumanReadable", ModeHumanReadable, "Human-readable"}, {"ModeLowerCase", ModeLowerCase, "Lower-case"}, - {"ModeNumber", ModeNumber, "Number"}, + {"ModeNumeric", ModeNumeric, "Number"}, {"ModeSpecial", ModeSpecial, "Special"}, {"ModeUpperCase", ModeUpperCase, "Upper-case"}, {"ModeUnknown", 255, "Unknown"}, diff --git a/random_test.go b/random_test.go index f7d0bca..e352319 100644 --- a/random_test.go +++ b/random_test.go @@ -112,23 +112,23 @@ func TestGenerator_RandomString(t *testing.T) { }{ { "CharRange:AlphaLower", CharRangeAlphaLower, - CharRangeAlphaUpper + CharRangeNumber + CharRangeSpecial, false, + CharRangeAlphaUpper + CharRangeNumeric + CharRangeSpecial, false, }, { "CharRange:AlphaUpper", CharRangeAlphaUpper, - CharRangeAlphaLower + CharRangeNumber + CharRangeSpecial, false, + CharRangeAlphaLower + CharRangeNumeric + CharRangeSpecial, false, }, { - "CharRange:Number", CharRangeNumber, + "CharRange:Number", CharRangeNumeric, CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeSpecial, false, }, { "CharRange:Special", CharRangeSpecial, - CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumber, false, + CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumeric, false, }, { "CharRange:Invalid", "", - CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumber + CharRangeSpecial, true, + CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumeric + CharRangeSpecial, true, }, } for _, tc := range tt { @@ -171,7 +171,7 @@ func BenchmarkGenerator_RandomBytes(b *testing.B) { func BenchmarkGenerator_RandomString(b *testing.B) { b.ReportAllocs() g := New(NewConfig()) - cr := CharRangeAlphaUpper + CharRangeAlphaLower + CharRangeNumber + CharRangeSpecial + cr := CharRangeAlphaUpper + CharRangeAlphaLower + CharRangeNumeric + CharRangeSpecial for i := 0; i < b.N; i++ { _, err := g.RandomStringFromCharRange(32, cr) if err != nil { From 8d42651e5830f04db99ead09a679d37f4d5ecf68 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Aug 2023 17:57:06 +0200 Subject: [PATCH 14/53] #53 Update password configuration parameters Added comments for minimum requirements parameters in config.go to provide clarity on their purpose. This was necessary to increase code readability, making it easier for future developer reference. --- config.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/config.go b/config.go index 6358a34..f0732ea 100644 --- a/config.go +++ b/config.go @@ -23,10 +23,18 @@ type Config struct { // MaxLength sets the maximum length for a generated password MaxLength int64 // MinLength sets the minimum length for a generated password - MinLength int64 + MinLength int64 + // MinLowerCase represents the minimum amount of lower-case characters that have + // to be part of the generated password MinLowerCase int64 - MinNumeric int64 - MinSpecial int64 + // MinNumeric represents the minimum amount of numeric characters that have + // to be part of the generated password + MinNumeric int64 + // MinSpecial represents the minimum amount of special characters that have + // to be part of the generated password + MinSpecial int64 + // MinUpperCase represents the minimum amount of upper-case characters that have + // to be part of the generated password MinUpperCase int64 // Mode holds the different character modes for the Random algorithm Mode ModeMask From 2822f73f5603e7030af01facee95d7aa5ab2f33e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Aug 2023 18:10:11 +0200 Subject: [PATCH 15/53] #53 Add coinflip algorithm and improve error messages Introduced a new password generation algorithm, called 'coinflip', which simply returns "Heads" or "Tails". Associated CLI flag has been added as well. Also, improved error messages during password generation. This addition provides a simpler algorithm option and clearer user feedback during errors. --- algo.go | 5 +++++ cmd/apg/apg.go | 17 ++++++++++++++--- random.go | 24 +++++++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/algo.go b/algo.go index 5cfc5f4..63fb0db 100644 --- a/algo.go +++ b/algo.go @@ -11,6 +11,9 @@ const ( // AlgoRandom represents the algorithm for purely random passwords according // to the provided password modes/flags AlgoRandom + // AlgoCoinFlip represents a very simple coinflip algorithm returning "heads" + // or "tails" + AlgoCoinFlip // AlgoUnsupported represents an unsupported algorithm AlgoUnsupported ) @@ -23,6 +26,8 @@ func IntToAlgo(a int) Algorithm { return AlgoPronouncable case 1: return AlgoRandom + case 2: + return AlgoCoinFlip default: return AlgoUnsupported } diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 2e5b0ba..641f3a3 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -15,8 +15,10 @@ func main() { // Configure and parse the CLI flags // See usage() for flag details + var al int var ms string var co, hr, lc, nu, sp, uc bool + flag.IntVar(&al, "a", 1, "") flag.BoolVar(&lc, "L", false, "") flag.Int64Var(&c.MinLowerCase, "mL", c.MinLowerCase, "") flag.BoolVar(&uc, "U", false, "") @@ -62,14 +64,22 @@ func main() { c.Mode = apg.ModesFromFlags(ms) } + // Check if algorithm is supported + c.Algorithm = apg.IntToAlgo(al) + if c.Algorithm == apg.AlgoUnsupported { + _, _ = fmt.Fprintf(os.Stderr, "unsupported algorithm value: %d\n", al) + os.Exit(1) + } + // Generate the password based on the given flags g := apg.New(c) for i := int64(0); i < c.NumberPass; i++ { - pl, err := g.GetPasswordLength() + p, err := g.Generate() if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "Error during password generation: %s\n", err) + _, _ = fmt.Fprintf(os.Stderr, "failed to generate password: %s\n", err) + os.Exit(1) } - fmt.Printf("PW length: %d\n", pl) + fmt.Println(p) } } @@ -88,6 +98,7 @@ Flags: -a ALGORITH Choose the password generation algorithm (Default: 1) - 0: pronounceable password generation (koremutake syllables) - 1: random password generation according to password modes/flags + - 2: coinflip (returns heads or tails) -m LENGTH Minimum length of the password to be generated (Default: 12) -x LENGTH Maximum length of the password to be generated (Default: 20) -f LENGTH Fixed length of the password to be generated (Ignores -m and -x) diff --git a/random.go b/random.go index 5b653d7..83c995b 100644 --- a/random.go +++ b/random.go @@ -27,6 +27,28 @@ var ( ErrInvalidCharRange = errors.New("provided character range is not valid or empty") ) +// Generate generates a password based on all the different config flags and returns +// it as string type. If the generation fails, an error will be thrown +func (g *Generator) Generate() (string, error) { + // Coinflip mode + if g.config.Algorithm == AlgoCoinFlip { + switch g.CoinFlipBool() { + case true: + return "Heads", nil + case false: + return "Tails", nil + } + } + + l, err := g.GetPasswordLength() + if err != nil { + return "", fmt.Errorf("failed to calculate password length: %w", err) + } + _ = l + + return "", nil +} + // RandomBytes returns a byte slice of random bytes with length n that got generated by // the crypto/rand generator func (g *Generator) RandomBytes(n int64) ([]byte, error) { @@ -121,7 +143,7 @@ func (g *Generator) GetPasswordLength() (int64, error) { diff := mal - mil + 1 ra, err := g.RandNum(diff) if err != nil { - return 0, fmt.Errorf("failed to calculate password length: %w", err) + return 0, err } l := mil + ra if l <= 0 { From 203da17634694ae6166412c748124be74571436a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Aug 2023 18:55:47 +0200 Subject: [PATCH 16/53] #52 and #53: Increase password generation flexibility and error handling Added code to check if set minimum password requirements are met after generating a random password. If not, the password generation process is repeated. This not only adds an extra layer of security but also ensures that all set criteria for the password are met. Also enhanced error handling in functions that could return a negative integer when asked for a random number. Changed 'minimum amount of' parameter descriptions in apg.go to avoid confusion. --- cmd/apg/apg.go | 44 ++++++++- mode.go | 4 +- random.go | 258 ++++++++++++++++++++++++++++++++++++++----------- random_test.go | 4 +- 4 files changed, 244 insertions(+), 66 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 641f3a3..1d72e2b 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -10,6 +10,13 @@ import ( "github.com/wneessen/apg-go" ) +// MinimumAmountTooHigh is an error message displayed when a minimum amount of +// parameter has been set to a too high value +const MinimumAmountTooHigh = "WARNING: You have selected a minimum amount of characters that is bigger\n" + + "than 50% of the minimum password length to be generated. This can lead\n" + + "to extraordinary calculation times resulting in apg-go never finishing\n" + + "the job. Please consider lowering the value.\n\n" + func main() { c := apg.NewConfig() @@ -64,6 +71,33 @@ func main() { c.Mode = apg.ModesFromFlags(ms) } + // For the "minimum amount of" modes we need to imply at the type + // of character mode is set + if c.MinLowerCase > 0 { + if float64(c.MinLength)/2 < float64(c.MinNumeric) { + _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) + } + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeLowerCase) + } + if c.MinNumeric > 0 { + if float64(c.MinLength)/2 < float64(c.MinLowerCase) { + _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) + } + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeNumeric) + } + if c.MinSpecial > 0 { + if float64(c.MinLength)/2 < float64(c.MinSpecial) { + _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) + } + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeSpecial) + } + if c.MinUpperCase > 0 { + if float64(c.MinLength)/2 < float64(c.MinUpperCase) { + _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) + } + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeUpperCase) + } + // Check if algorithm is supported c.Algorithm = apg.IntToAlgo(al) if c.Algorithm == apg.AlgoUnsupported { @@ -106,10 +140,12 @@ Flags: -E CHARS List of characters to be excluded in the generated password -M [LUNSHClunshc] New style password flags - Note: new-style flags have higher priority than any of the old-style flags - -mL NUMBER Minimal amount of lower-case characters (implies -L) - -mN NUMBER Minimal amount of numeric characters (imlies -N) - -mS NUMBER Minimal amount of special characters (imlies -S) - -mU NUMBER Minimal amount of upper-case characters (imlies -U) + -mL NUMBER Minimum amount of lower-case characters (implies -L) + -mN NUMBER Minimum amount of numeric characters (imlies -N) + -mS NUMBER Minimum amount of special characters (imlies -S) + -mU NUMBER Minimum amount of upper-case characters (imlies -U) + - Note: any of the "Minimum amount of" modes may result in + extraordinarily long calculation times -C Enable complex password mode (implies -L -U -N -S and disables -H) -H Avoid ambiguous characters in passwords (i. e.: 1, l, I, O, 0) (Default: off) -L Toggle lower-case characters in passwords (Default: on) diff --git a/mode.go b/mode.go index 3595114..05ea088 100644 --- a/mode.go +++ b/mode.go @@ -37,8 +37,8 @@ const ( CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ" // CharRangeNumeric represents all numerical characters CharRangeNumeric = "1234567890" - // CharRangeNumberHuman represents all human-readable numerical characters - CharRangeNumberHuman = "23456789" + // CharRangeNumericHuman represents all human-readable numerical characters + CharRangeNumericHuman = "23456789" // CharRangeSpecial represents all special characters CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~` // CharRangeSpecialHuman represents all human-readable special characters diff --git a/random.go b/random.go index 83c995b..0ef88f2 100644 --- a/random.go +++ b/random.go @@ -27,28 +27,54 @@ var ( ErrInvalidCharRange = errors.New("provided character range is not valid or empty") ) +// CoinFlip performs a simple coinflip based on the rand library and returns 1 or 0 +func (g *Generator) CoinFlip() int64 { + cf, _ := g.RandNum(2) + return cf +} + +// CoinFlipBool performs a simple coinflip based on the rand library and returns true or false +func (g *Generator) CoinFlipBool() bool { + return g.CoinFlip() == 1 +} + // Generate generates a password based on all the different config flags and returns // it as string type. If the generation fails, an error will be thrown func (g *Generator) Generate() (string, error) { - // Coinflip mode - if g.config.Algorithm == AlgoCoinFlip { - switch g.CoinFlipBool() { - case true: - return "Heads", nil - case false: - return "Tails", nil - } + switch g.config.Algorithm { + case AlgoCoinFlip: + return g.generateCoinFlip() + case AlgoRandom: + return g.generateRandom() + case AlgoUnsupported: + return "", fmt.Errorf("unsupported algorithm") } - - l, err := g.GetPasswordLength() - if err != nil { - return "", fmt.Errorf("failed to calculate password length: %w", err) - } - _ = l - return "", nil } +// GetPasswordLength returns the password length based on the given config +// parameters +func (g *Generator) GetPasswordLength() (int64, error) { + if g.config.FixedLength > 0 { + return g.config.FixedLength, nil + } + mil := g.config.MinLength + mal := g.config.MaxLength + if mil > mal { + mal = mil + } + diff := mal - mil + 1 + ra, err := g.RandNum(diff) + if err != nil { + return 0, err + } + l := mil + ra + if l <= 0 { + return 1, nil + } + return l, nil +} + // RandomBytes returns a byte slice of random bytes with length n that got generated by // the crypto/rand generator func (g *Generator) RandomBytes(n int64) ([]byte, error) { @@ -67,10 +93,23 @@ func (g *Generator) RandomBytes(n int64) ([]byte, error) { return b, nil } +// RandNum generates a random, non-negative number with given maximum value +func (g *Generator) RandNum(m int64) (int64, error) { + if m < 1 { + return 0, ErrInvalidLength + } + mbi := big.NewInt(m) + rn, err := rand.Int(rand.Reader, mbi) + if err != nil { + return 0, fmt.Errorf("random number generation failed: %w", err) + } + return rn.Int64(), nil +} + // RandomStringFromCharRange returns a random string of length l based of the range of characters given. // The method makes use of the crypto/random package and therfore is // cryptographically secure -func (g *Generator) RandomStringFromCharRange(l int, cr string) (string, error) { +func (g *Generator) RandomStringFromCharRange(l int64, cr string) (string, error) { if l < 1 { return "", ErrInvalidLength } @@ -78,7 +117,13 @@ func (g *Generator) RandomStringFromCharRange(l int, cr string) (string, error) return "", ErrInvalidCharRange } rs := strings.Builder{} - rs.Grow(l) + + // As long as the length is smaller than the max. int32 value let's grow + // the string builder to the actual size, so we need less allocations + if l <= 2147483647 { + rs.Grow(int(l)) + } + crl := len(cr) rp := make([]byte, 8) @@ -105,49 +150,146 @@ func (g *Generator) RandomStringFromCharRange(l int, cr string) (string, error) return rs.String(), nil } -// RandNum generates a random, non-negative number with given maximum value -func (g *Generator) RandNum(m int64) (int64, error) { - if m < 1 { - return 0, ErrInvalidLength +// GetCharRangeFromConfig checks the Mode from the Config and returns a +// list of all possible characters that are supported by these Mode +func (g *Generator) GetCharRangeFromConfig() string { + cr := strings.Builder{} + if MaskHasMode(g.config.Mode, ModeLowerCase) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr.WriteString(CharRangeAlphaLowerHuman) + default: + cr.WriteString(CharRangeAlphaLower) + } } - mbi := big.NewInt(m) - rn, err := rand.Int(rand.Reader, mbi) + if MaskHasMode(g.config.Mode, ModeNumeric) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr.WriteString(CharRangeNumericHuman) + default: + cr.WriteString(CharRangeNumeric) + } + } + if MaskHasMode(g.config.Mode, ModeSpecial) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr.WriteString(CharRangeSpecialHuman) + default: + cr.WriteString(CharRangeSpecial) + } + } + if MaskHasMode(g.config.Mode, ModeUpperCase) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr.WriteString(CharRangeAlphaUpperHuman) + default: + cr.WriteString(CharRangeAlphaUpper) + } + } + return cr.String() +} + +func (g *Generator) checkMinimumRequirements(pw string) bool { + ok := true + if g.config.MinLowerCase > 0 { + var cr string + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr = CharRangeAlphaLowerHuman + default: + cr = CharRangeAlphaLower + } + + m := 0 + for _, c := range cr { + m += strings.Count(pw, string(c)) + } + if int64(m) < g.config.MinLowerCase { + ok = false + } + } + if g.config.MinNumeric > 0 { + var cr string + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr = CharRangeNumericHuman + default: + cr = CharRangeNumeric + } + + m := 0 + for _, c := range cr { + m += strings.Count(pw, string(c)) + } + if int64(m) < g.config.MinNumeric { + ok = false + } + } + if g.config.MinSpecial > 0 { + var cr string + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr = CharRangeSpecialHuman + default: + cr = CharRangeSpecial + } + + m := 0 + for _, c := range cr { + m += strings.Count(pw, string(c)) + } + if int64(m) < g.config.MinSpecial { + ok = false + } + } + if g.config.MinUpperCase > 0 { + var cr string + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr = CharRangeAlphaUpperHuman + default: + cr = CharRangeAlphaUpper + } + + m := 0 + for _, c := range cr { + m += strings.Count(pw, string(c)) + } + if int64(m) < g.config.MinUpperCase { + ok = false + } + } + return ok +} + +// generateCoinFlip is executed when Generate() is called with Algorithm set +// to AlgoCoinFlip +func (g *Generator) generateCoinFlip() (string, error) { + switch g.CoinFlipBool() { + case true: + return "Heads", nil + default: + return "Tails", nil + } +} + +// generateRandom is executed when Generate() is called with Algorithm set +// to AlgoRandmom +func (g *Generator) generateRandom() (string, error) { + l, err := g.GetPasswordLength() if err != nil { - return 0, fmt.Errorf("random number generation failed: %w", err) + return "", fmt.Errorf("failed to calculate password length: %w", err) + } + cr := g.GetCharRangeFromConfig() + var pw string + var ok bool + for !ok { + pw, err = g.RandomStringFromCharRange(l, cr) + if err != nil { + return "", err + } + ok = g.checkMinimumRequirements(pw) } - return rn.Int64(), nil -} -// CoinFlip performs a simple coinflip based on the rand library and returns 1 or 0 -func (g *Generator) CoinFlip() int64 { - cf, _ := g.RandNum(2) - return cf -} - -// CoinFlipBool performs a simple coinflip based on the rand library and returns true or false -func (g *Generator) CoinFlipBool() bool { - return g.CoinFlip() == 1 -} - -// GetPasswordLength returns the password length based on the given config -// parameters -func (g *Generator) GetPasswordLength() (int64, error) { - if g.config.FixedLength > 0 { - return g.config.FixedLength, nil - } - mil := g.config.MinLength - mal := g.config.MaxLength - if mil > mal { - mal = mil - } - diff := mal - mil + 1 - ra, err := g.RandNum(diff) - if err != nil { - return 0, err - } - l := mil + ra - if l <= 0 { - return 1, nil - } - return l, nil + return pw, nil } diff --git a/random_test.go b/random_test.go index e352319..a7fc479 100644 --- a/random_test.go +++ b/random_test.go @@ -103,7 +103,7 @@ func TestGenerator_RandomBytes(t *testing.T) { func TestGenerator_RandomString(t *testing.T) { g := New(NewConfig()) - l := 32 + var l int64 = 32 * 1024 tt := []struct { name string cr string @@ -137,7 +137,7 @@ func TestGenerator_RandomString(t *testing.T) { if err != nil && !tc.sf { t.Errorf("RandomStringFromCharRange failed: %s", err) } - if len(rs) != l && !tc.sf { + if int64(len(rs)) != l && !tc.sf { t.Errorf("RandomStringFromCharRange failed. Expected length: %d, got: %d", l, len(rs)) } if strings.ContainsAny(rs, tc.nr) { From 86b0ca972ee65a51c6c322e3b8e613be57756986 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Mar 2024 21:15:36 +0100 Subject: [PATCH 17/53] Update go.mod module path and Go version The go.mod file was updated to reflect a new module path and an upgrade in Go version. The module path is now set to src.neessen.cloud/wneessen/apg-go, and the Go version has been updated to 1.22. --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 10fb223..955e55e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/wneessen/apg-go +module src.neessen.cloud/wneessen/apg-go -go 1.20 +go 1.22 From 8dce4d2eb974fe5ad5e7328b63ed4f70ca53ae17 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Mar 2024 21:15:49 +0100 Subject: [PATCH 18/53] Update import path and change copyright year in apg.go The import path of the module "apg-go" has been changed to "src.neessen.cloud/wneessen/apg-go". Furthermore the copyright year mentioned in the usage text has been updated to 2024. --- cmd/apg/apg.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 1d72e2b..7a4c758 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -7,7 +7,7 @@ import ( "fmt" "os" - "github.com/wneessen/apg-go" + "src.neessen.cloud/wneessen/apg-go" ) // MinimumAmountTooHigh is an error message displayed when a minimum amount of @@ -122,8 +122,8 @@ func usage() { // Usage text const ut = `apg-go v` + apg.VERSION + "\n" + - `A OSS "Automated Password Generator"-clone -- https://github.com/wneessen/apg-go/ -Created 2021-2023 by Winni Neessen (MIT licensed) + `A OSS "Automated Password Generator"-clone -- https://src.neessen.cloud/wneessen/apg-go/ +Created 2021-2024 by Winni Neessen (MIT licensed) apg [-a ] [-m ] [-x ] [-L] [-U] [-N] [-S] [-H] [-C] [-l] [-M mode] [-E char_string] [-n num_of_pass] [-v] [-h] [-t] From 86ca627fed0e10d422942596725e0b8e7baa6ffd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Mar 2024 21:16:04 +0100 Subject: [PATCH 19/53] Add password generation algorithm option and corresponding tests The password generation algorithm is now customizable via the `WithAlgorithm()` function, giving users more control over password generation. Tests have been added to 'config_test.go' to ensure correct functionality of the new option. Additionally, the variable naming in 'config.go' has been improved for better readability. --- config.go | 39 +++++++++++++++++++++++---------------- config_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/config.go b/config.go index f0732ea..e1106bc 100644 --- a/config.go +++ b/config.go @@ -48,8 +48,8 @@ type Option func(*Config) // NewConfig creates a new Config instance and pre-fills it with sane // default settings. The Config is returned as pointer value -func NewConfig(o ...Option) *Config { - c := &Config{ +func NewConfig(opts ...Option) *Config { + config := &Config{ MaxLength: DefaultMaxLength, MinLength: DefaultMinLength, Mode: DefaultMode, @@ -57,32 +57,39 @@ func NewConfig(o ...Option) *Config { } // Override defaults with optionally provided config.Option functions - for _, co := range o { - if co == nil { + for _, opt := range opts { + if opt == nil { continue } - co(c) + opt(config) + } + return config +} + +// WithAlgorithm overrides the algorithm mode for the password generation +func WithAlgorithm(algo Algorithm) Option { + return func(config *Config) { + config.Algorithm = algo } - return c } // WithMinLength overrides the minimum password length -func WithMinLength(l int64) Option { - return func(c *Config) { - c.MinLength = l +func WithMinLength(length int64) Option { + return func(config *Config) { + config.MinLength = length } } // WithMaxLength overrides the maximum password length -func WithMaxLength(l int64) Option { - return func(c *Config) { - c.MaxLength = l +func WithMaxLength(length int64) Option { + return func(config *Config) { + config.MaxLength = length } } -// WithNumberPass overrides the number of generated passwords setting -func WithNumberPass(n int64) Option { - return func(c *Config) { - c.NumberPass = n +// WithNumberPass overrides the amount of generated passwords setting +func WithNumberPass(amount int64) Option { + return func(config *Config) { + config.NumberPass = amount } } diff --git a/config_test.go b/config_test.go index 71706ee..b79b5ed 100644 --- a/config_test.go +++ b/config_test.go @@ -29,6 +29,36 @@ func TestNewConfig(t *testing.T) { } } +func TestWithAlgorithm(t *testing.T) { + tests := []struct { + name string + algo Algorithm + want int + }{ + {"Pronouncble passwords", AlgoPronouncable, 0}, + {"Random passwords", AlgoRandom, 1}, + {"Coinflip", AlgoCoinFlip, 2}, + {"Unsupported", AlgoUnsupported, 3}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewConfig(WithAlgorithm(tt.algo)) + if c == nil { + t.Errorf("NewConfig(WithAlgorithm()) failed, expected config pointer but got nil") + return + } + if c.Algorithm != tt.algo { + t.Errorf("NewConfig(WithAlgorithm()) failed, expected algo: %d, got: %d", + tt.algo, c.Algorithm) + } + if IntToAlgo(tt.want) != c.Algorithm { + t.Errorf("IntToAlgo() failed, expected algo: %d, got: %d", + tt.want, c.Algorithm) + } + }) + } +} + func TestWithMaxLength(t *testing.T) { var e int64 = 123 c := NewConfig(WithMaxLength(e)) From 03867adac04e183b35ed4a4f0f11e80614cea201 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Mar 2024 21:22:14 +0100 Subject: [PATCH 20/53] Replace AlgoUnsupported with AlgoCoinflip in algo_test.go The previously named "AlgoUnsupported" has been replaced with "AlgoCoinflip" in the algorithm tests in algo_test.go. Additionally, the "AlgoUnsupported" test has been moved down to keep the test sequence logical and comprehensive. --- algo_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/algo_test.go b/algo_test.go index b0e2e05..bab7c4a 100644 --- a/algo_test.go +++ b/algo_test.go @@ -10,7 +10,8 @@ func TestIntToAlgo(t *testing.T) { }{ {"AlgoPronouncable", 0, AlgoPronouncable}, {"AlgoRandom", 1, AlgoRandom}, - {"AlgoUnsupported", 2, AlgoUnsupported}, + {"AlgoCoinflip", 2, AlgoCoinFlip}, + {"AlgoUnsupported", 3, AlgoUnsupported}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { From c3eb80a18388ee998b3ee822ce747b8c567d2af7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Mar 2024 21:22:24 +0100 Subject: [PATCH 21/53] Correct "Number" to "Numeric" in mode_test.go The string representation for the "ModeNumeric" enumeration member was incorrectly labeled as "Number". This has been corrected to "Numeric" in the mode tests file mode_test.go to maintain correct and consistent naming conventions. --- mode_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mode_test.go b/mode_test.go index bf2175f..70f4f37 100644 --- a/mode_test.go +++ b/mode_test.go @@ -87,7 +87,7 @@ func TestMode_String(t *testing.T) { }{ {"ModeHumanReadable", ModeHumanReadable, "Human-readable"}, {"ModeLowerCase", ModeLowerCase, "Lower-case"}, - {"ModeNumeric", ModeNumeric, "Number"}, + {"ModeNumeric", ModeNumeric, "Numeric"}, {"ModeSpecial", ModeSpecial, "Special"}, {"ModeUpperCase", ModeUpperCase, "Upper-case"}, {"ModeUnknown", 255, "Unknown"}, From b2d6a3418e95a1f45fbd5c11407abc9868234c71 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Mar 2024 21:36:57 +0100 Subject: [PATCH 22/53] Add Codecov, SonarQube, REUSE Compliance, and golangci-lint workflows The commit includes the addition of four new workflows to the .forgejo workflows directory. These are Codecov for code coverage handling, SonarQube for continuous inspection of code quality, REUSE Compliance Check for license compliance, and golangci-lint for running linters on Go code. The workflows all target the 'main' branch and are triggered by 'push' and 'pull_request' events. --- .forgejo/workflows/codecov.yml | 41 +++++++++++++++++++++++ .forgejo/workflows/golangci-lint.yml | 49 ++++++++++++++++++++++++++++ .forgejo/workflows/reuse.yml | 15 +++++++++ .forgejo/workflows/sonarqube.yml | 45 +++++++++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 .forgejo/workflows/codecov.yml create mode 100644 .forgejo/workflows/golangci-lint.yml create mode 100644 .forgejo/workflows/reuse.yml create mode 100644 .forgejo/workflows/sonarqube.yml diff --git a/.forgejo/workflows/codecov.yml b/.forgejo/workflows/codecov.yml new file mode 100644 index 0000000..bfbb3cb --- /dev/null +++ b/.forgejo/workflows/codecov.yml @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2023 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + +name: Codecov workflow +on: + push: + branches: + - main + paths: + - '**.go' + - 'go.*' + - '.forgejo/**' + - 'codecov.yml' + pull_request: + branches: + - main + paths: + - '**.go' + - 'go.*' + - '.forgejo/**' + - 'codecov.yml' +env: + API_KEY: ${{ secrets.API_KEY }} +jobs: + run: + runs-on: docker + steps: + - name: Checkout Code + uses: actions/checkout@master + - name: Setup go + uses: actions/setup-go@v4 + with: + go-version: '1.22' + - name: Run Tests + run: | + go test -v -shuffle=on -race --coverprofile=coverage.coverprofile --covermode=atomic ./... + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos diff --git a/.forgejo/workflows/golangci-lint.yml b/.forgejo/workflows/golangci-lint.yml new file mode 100644 index 0000000..a2bc22a --- /dev/null +++ b/.forgejo/workflows/golangci-lint.yml @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2022 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + +name: golangci-lint +on: + push: + tags: + - v* + branches: + - main + pull_request: +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + # pull-requests: read +jobs: + golangci: + name: lint + runs-on: docker + steps: + - uses: actions/setup-go@v4 + with: + go-version: '1.22' + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true then the all caching functionality will be complete disabled, + # takes precedence over all other caching options. + # skip-cache: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true diff --git a/.forgejo/workflows/reuse.yml b/.forgejo/workflows/reuse.yml new file mode 100644 index 0000000..accbbf5 --- /dev/null +++ b/.forgejo/workflows/reuse.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + +name: REUSE Compliance Check + +on: [push, pull_request] + +jobs: + test: + runs-on: docker + steps: + - uses: actions/checkout@v2 + - name: REUSE Compliance Check + uses: fsfe/reuse-action@v1 diff --git a/.forgejo/workflows/sonarqube.yml b/.forgejo/workflows/sonarqube.yml new file mode 100644 index 0000000..b658a85 --- /dev/null +++ b/.forgejo/workflows/sonarqube.yml @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2023 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + +name: SonarQube +on: + push: + branches: + - main # or the name of your main branch + pull_request: + branches: + - main # or the name of your main branch +env: + API_KEY: ${{ secrets.API_KEY }} +jobs: + build: + name: Build + runs-on: docker + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: '1.22' + + - name: Run unit Tests + run: | + go test -v -shuffle=on -race --coverprofile=./cov.out ./... + + - name: Install jq + run: | + apt-get update; apt-get -y install jq; which jq + + - uses: sonarsource/sonarqube-scan-action@master + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + + - uses: sonarsource/sonarqube-quality-gate-action@master + timeout-minutes: 5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 4b874e499ee4858ac3e6d3f7dec49fe2ba19fc28 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Mar 2024 21:51:52 +0100 Subject: [PATCH 23/53] Fix typos in 'implies' within command-line flags description The previous version misspelled 'implies' as 'imlies' for several command-line flags (-mN, -mS, -mU) in the apg.go file. This commit corrects these typos, ensuring clarity and better understanding of the program's functions and usage. --- cmd/apg/apg.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 7a4c758..64b9373 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -141,9 +141,9 @@ Flags: -M [LUNSHClunshc] New style password flags - Note: new-style flags have higher priority than any of the old-style flags -mL NUMBER Minimum amount of lower-case characters (implies -L) - -mN NUMBER Minimum amount of numeric characters (imlies -N) - -mS NUMBER Minimum amount of special characters (imlies -S) - -mU NUMBER Minimum amount of upper-case characters (imlies -U) + -mN NUMBER Minimum amount of numeric characters (implies -N) + -mS NUMBER Minimum amount of special characters (implies -S) + -mU NUMBER Minimum amount of upper-case characters (implies -U) - Note: any of the "Minimum amount of" modes may result in extraordinarily long calculation times -C Enable complex password mode (implies -L -U -N -S and disables -H) From 8f8e439f560e073b3718ec54f3a4b0daee79688b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Mar 2024 22:50:47 +0100 Subject: [PATCH 24/53] Refactor variable names and correct typos in `RandomBytes` and `RandNum` methods This commit includes refactoring variable names to enhance readability and understanding within the `RandomBytes`, `RandNum` and `RandomStringFromCharRange` methods. A constant, `maxInt32`, was also added to replace a hard-coded value for better code practice. Typos in the comments and documentation were corrected as well. --- random.go | 63 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/random.go b/random.go index 0ef88f2..5ac5896 100644 --- a/random.go +++ b/random.go @@ -18,6 +18,9 @@ const ( letterIdxMax = 63 / letterIdxBits ) +// maxInt32 is the maximum positive value for a int32 number type +const maxInt32 = 2147483647 + var ( // ErrInvalidLength is returned if the provided maximum number is equal or less than zero ErrInvalidLength = errors.New("provided length value cannot be zero or less") @@ -58,80 +61,80 @@ func (g *Generator) GetPasswordLength() (int64, error) { if g.config.FixedLength > 0 { return g.config.FixedLength, nil } - mil := g.config.MinLength - mal := g.config.MaxLength - if mil > mal { - mal = mil + minLength := g.config.MinLength + maxLength := g.config.MaxLength + if minLength > maxLength { + maxLength = minLength } - diff := mal - mil + 1 - ra, err := g.RandNum(diff) + diff := maxLength - minLength + 1 + randNum, err := g.RandNum(diff) if err != nil { return 0, err } - l := mil + ra - if l <= 0 { + length := minLength + randNum + if length <= 0 { return 1, nil } - return l, nil + return length, nil } -// RandomBytes returns a byte slice of random bytes with length n that got generated by +// RandomBytes returns a byte slice of random bytes with given length that got generated by // the crypto/rand generator -func (g *Generator) RandomBytes(n int64) ([]byte, error) { - if n < 1 { +func (g *Generator) RandomBytes(length int64) ([]byte, error) { + if length < 1 { return nil, ErrInvalidLength } - b := make([]byte, n) - l, err := rand.Read(b) - if int64(l) != n { + bytes := make([]byte, length) + numBytes, err := rand.Read(bytes) + if int64(numBytes) != length { return nil, ErrLengthMismatch } if err != nil { return nil, err } - return b, nil + return bytes, nil } // RandNum generates a random, non-negative number with given maximum value -func (g *Generator) RandNum(m int64) (int64, error) { - if m < 1 { +func (g *Generator) RandNum(max int64) (int64, error) { + if max < 1 { return 0, ErrInvalidLength } - mbi := big.NewInt(m) - rn, err := rand.Int(rand.Reader, mbi) + max64 := big.NewInt(max) + randNum, err := rand.Int(rand.Reader, max64) if err != nil { return 0, fmt.Errorf("random number generation failed: %w", err) } - return rn.Int64(), nil + return randNum.Int64(), nil } // RandomStringFromCharRange returns a random string of length l based of the range of characters given. // The method makes use of the crypto/random package and therfore is // cryptographically secure -func (g *Generator) RandomStringFromCharRange(l int64, cr string) (string, error) { - if l < 1 { +func (g *Generator) RandomStringFromCharRange(length int64, charRange string) (string, error) { + if length < 1 { return "", ErrInvalidLength } - if len(cr) < 1 { + if len(charRange) < 1 { return "", ErrInvalidCharRange } rs := strings.Builder{} // As long as the length is smaller than the max. int32 value let's grow // the string builder to the actual size, so we need less allocations - if l <= 2147483647 { - rs.Grow(int(l)) + if length <= maxInt32 { + rs.Grow(int(length)) } - crl := len(cr) + charRangeLength := len(charRange) rp := make([]byte, 8) _, err := rand.Read(rp) if err != nil { return rs.String(), err } - for i, c, r := l-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; { + for i, c, r := length-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; { if r == 0 { _, err = rand.Read(rp) if err != nil { @@ -139,8 +142,8 @@ func (g *Generator) RandomStringFromCharRange(l int64, cr string) (string, error } c, r = binary.BigEndian.Uint64(rp), letterIdxMax } - if idx := int(c & letterIdxMask); idx < crl { - rs.WriteByte(cr[idx]) + if idx := int(c & letterIdxMask); idx < charRangeLength { + rs.WriteByte(charRange[idx]) i-- } c >>= letterIdxBits From e17da1a2c90c850367b5bddaff4514a46cedbf81 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Mar 2024 23:22:28 +0100 Subject: [PATCH 25/53] Improve code readability by refactoring variable names Several variable names have been changed in 'random.go' to improve overall readability and code comprehension. Additionally, a typo was fixed and a new constant was introduced for the maximum value of Int32, to replace previously hard-coded values. The adjustments not only make the code more digestible, but also adhere to good coding practices. --- mode.go | 1 - mode_test.go | 45 +++++++++++------- random.go | 128 +++++++++++++++++++++++++-------------------------- 3 files changed, 92 insertions(+), 82 deletions(-) diff --git a/mode.go b/mode.go index 05ea088..17ee59b 100644 --- a/mode.go +++ b/mode.go @@ -88,7 +88,6 @@ func ModesFromFlags(ms string) ModeMask { } return mm - } // String satisfies the fmt.Stringer interface for the Mode type diff --git a/mode_test.go b/mode_test.go index 70f4f37..0c5d618 100644 --- a/mode_test.go +++ b/mode_test.go @@ -45,30 +45,43 @@ func TestModesFromFlags(t *testing.T) { ms string mode []Mode }{ - {"ModeComplex", "C", []Mode{ModeLowerCase, ModeNumeric, ModeSpecial, - ModeUpperCase}}, + {"ModeComplex", "C", []Mode{ + ModeLowerCase, ModeNumeric, ModeSpecial, + ModeUpperCase, + }}, {"ModeHumanReadable", "H", []Mode{ModeHumanReadable}}, {"ModeLowerCase", "L", []Mode{ModeLowerCase}}, {"ModeNumeric", "N", []Mode{ModeNumeric}}, {"ModeUpperCase", "U", []Mode{ModeUpperCase}}, {"ModeSpecial", "S", []Mode{ModeSpecial}}, - {"ModeLowerSpecialUpper", "LSUH", []Mode{ModeHumanReadable, - ModeLowerCase, ModeSpecial, ModeUpperCase}}, - {"ModeComplexNoHumanReadable", "Ch", []Mode{ModeLowerCase, - ModeNumeric, ModeSpecial, ModeUpperCase}}, - {"ModeComplexNoLower", "Cl", []Mode{ModeNumeric, ModeSpecial, - ModeUpperCase}}, - {"ModeComplexNoNumber", "Cn", []Mode{ModeLowerCase, ModeSpecial, - ModeUpperCase}}, - {"ModeComplexNoSpecial", "Cs", []Mode{ModeLowerCase, ModeNumeric, - ModeUpperCase}}, - {"ModeComplexNoUpper", "Cu", []Mode{ModeLowerCase, ModeNumeric, - ModeSpecial}}, + {"ModeLowerSpecialUpper", "LSUH", []Mode{ + ModeHumanReadable, + ModeLowerCase, ModeSpecial, ModeUpperCase, + }}, + {"ModeComplexNoHumanReadable", "Ch", []Mode{ + ModeLowerCase, + ModeNumeric, ModeSpecial, ModeUpperCase, + }}, + {"ModeComplexNoLower", "Cl", []Mode{ + ModeNumeric, ModeSpecial, + ModeUpperCase, + }}, + {"ModeComplexNoNumber", "Cn", []Mode{ + ModeLowerCase, ModeSpecial, + ModeUpperCase, + }}, + {"ModeComplexNoSpecial", "Cs", []Mode{ + ModeLowerCase, ModeNumeric, + ModeUpperCase, + }}, + {"ModeComplexNoUpper", "Cu", []Mode{ + ModeLowerCase, ModeNumeric, + ModeSpecial, + }}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - var mm ModeMask - mm = ModesFromFlags(tc.ms) + mm := ModesFromFlags(tc.ms) for _, tm := range tc.mode { if !MaskHasMode(mm, tm) { t.Errorf("ModesFromFlags() failed, expected mode %q not found", diff --git a/random.go b/random.go index 5ac5896..27936e7 100644 --- a/random.go +++ b/random.go @@ -119,146 +119,146 @@ func (g *Generator) RandomStringFromCharRange(length int64, charRange string) (s if len(charRange) < 1 { return "", ErrInvalidCharRange } - rs := strings.Builder{} + randString := strings.Builder{} // As long as the length is smaller than the max. int32 value let's grow // the string builder to the actual size, so we need less allocations if length <= maxInt32 { - rs.Grow(int(length)) + randString.Grow(int(length)) } charRangeLength := len(charRange) - rp := make([]byte, 8) - _, err := rand.Read(rp) + randPool := make([]byte, 8) + _, err := rand.Read(randPool) if err != nil { - return rs.String(), err + return randString.String(), err } - for i, c, r := length-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; { - if r == 0 { - _, err = rand.Read(rp) + for idx, char, rest := length-1, binary.BigEndian.Uint64(randPool), letterIdxMax; idx >= 0; { + if rest == 0 { + _, err = rand.Read(randPool) if err != nil { - return rs.String(), err + return randString.String(), err } - c, r = binary.BigEndian.Uint64(rp), letterIdxMax + char, rest = binary.BigEndian.Uint64(randPool), letterIdxMax } - if idx := int(c & letterIdxMask); idx < charRangeLength { - rs.WriteByte(charRange[idx]) - i-- + if i := int(char & letterIdxMask); i < charRangeLength { + randString.WriteByte(charRange[i]) + idx-- } - c >>= letterIdxBits - r-- + char >>= letterIdxBits + rest-- } - return rs.String(), nil + return randString.String(), nil } // GetCharRangeFromConfig checks the Mode from the Config and returns a // list of all possible characters that are supported by these Mode func (g *Generator) GetCharRangeFromConfig() string { - cr := strings.Builder{} + charRange := strings.Builder{} if MaskHasMode(g.config.Mode, ModeLowerCase) { switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: - cr.WriteString(CharRangeAlphaLowerHuman) + charRange.WriteString(CharRangeAlphaLowerHuman) default: - cr.WriteString(CharRangeAlphaLower) + charRange.WriteString(CharRangeAlphaLower) } } if MaskHasMode(g.config.Mode, ModeNumeric) { switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: - cr.WriteString(CharRangeNumericHuman) + charRange.WriteString(CharRangeNumericHuman) default: - cr.WriteString(CharRangeNumeric) + charRange.WriteString(CharRangeNumeric) } } if MaskHasMode(g.config.Mode, ModeSpecial) { switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: - cr.WriteString(CharRangeSpecialHuman) + charRange.WriteString(CharRangeSpecialHuman) default: - cr.WriteString(CharRangeSpecial) + charRange.WriteString(CharRangeSpecial) } } if MaskHasMode(g.config.Mode, ModeUpperCase) { switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: - cr.WriteString(CharRangeAlphaUpperHuman) + charRange.WriteString(CharRangeAlphaUpperHuman) default: - cr.WriteString(CharRangeAlphaUpper) + charRange.WriteString(CharRangeAlphaUpper) } } - return cr.String() + return charRange.String() } -func (g *Generator) checkMinimumRequirements(pw string) bool { +func (g *Generator) checkMinimumRequirements(password string) bool { ok := true if g.config.MinLowerCase > 0 { - var cr string + var charRange string switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: - cr = CharRangeAlphaLowerHuman + charRange = CharRangeAlphaLowerHuman default: - cr = CharRangeAlphaLower + charRange = CharRangeAlphaLower } - m := 0 - for _, c := range cr { - m += strings.Count(pw, string(c)) + count := 0 + for _, char := range charRange { + count += strings.Count(password, string(char)) } - if int64(m) < g.config.MinLowerCase { + if int64(count) < g.config.MinLowerCase { ok = false } } if g.config.MinNumeric > 0 { - var cr string + var charRange string switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: - cr = CharRangeNumericHuman + charRange = CharRangeNumericHuman default: - cr = CharRangeNumeric + charRange = CharRangeNumeric } - m := 0 - for _, c := range cr { - m += strings.Count(pw, string(c)) + count := 0 + for _, char := range charRange { + count += strings.Count(password, string(char)) } - if int64(m) < g.config.MinNumeric { + if int64(count) < g.config.MinNumeric { ok = false } } if g.config.MinSpecial > 0 { - var cr string + var charRange string switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: - cr = CharRangeSpecialHuman + charRange = CharRangeSpecialHuman default: - cr = CharRangeSpecial + charRange = CharRangeSpecial } - m := 0 - for _, c := range cr { - m += strings.Count(pw, string(c)) + count := 0 + for _, char := range charRange { + count += strings.Count(password, string(char)) } - if int64(m) < g.config.MinSpecial { + if int64(count) < g.config.MinSpecial { ok = false } } if g.config.MinUpperCase > 0 { - var cr string + var charRange string switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: - cr = CharRangeAlphaUpperHuman + charRange = CharRangeAlphaUpperHuman default: - cr = CharRangeAlphaUpper + charRange = CharRangeAlphaUpper } - m := 0 - for _, c := range cr { - m += strings.Count(pw, string(c)) + count := 0 + for _, char := range charRange { + count += strings.Count(password, string(char)) } - if int64(m) < g.config.MinUpperCase { + if int64(count) < g.config.MinUpperCase { ok = false } } @@ -268,31 +268,29 @@ func (g *Generator) checkMinimumRequirements(pw string) bool { // generateCoinFlip is executed when Generate() is called with Algorithm set // to AlgoCoinFlip func (g *Generator) generateCoinFlip() (string, error) { - switch g.CoinFlipBool() { - case true: + if g.CoinFlipBool() { return "Heads", nil - default: - return "Tails", nil } + return "Tails", nil } // generateRandom is executed when Generate() is called with Algorithm set // to AlgoRandmom func (g *Generator) generateRandom() (string, error) { - l, err := g.GetPasswordLength() + length, err := g.GetPasswordLength() if err != nil { return "", fmt.Errorf("failed to calculate password length: %w", err) } - cr := g.GetCharRangeFromConfig() - var pw string + charRange := g.GetCharRangeFromConfig() + var password string var ok bool for !ok { - pw, err = g.RandomStringFromCharRange(l, cr) + password, err = g.RandomStringFromCharRange(length, charRange) if err != nil { return "", err } - ok = g.checkMinimumRequirements(pw) + ok = g.checkMinimumRequirements(password) } - return pw, nil + return password, nil } From 61ca9af22ac533bf2506a50fea253278de73f05b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Mar 2024 10:21:57 +0100 Subject: [PATCH 26/53] Refactor Generator struct and New function in apg.go Removed the unused 'charRange' field from the Generator struct. Also, renamed the parameter in the New function from 'c' to 'config' for better clarity and understanding of its purpose. --- apg.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apg.go b/apg.go index 9aba57a..a3d9982 100644 --- a/apg.go +++ b/apg.go @@ -5,15 +5,13 @@ const VERSION = "2.0.0" // Generator is the password generator type of the APG package type Generator struct { - // charRange is the range of character used for the - charRange string // config is a pointer to the apg config instance config *Config } // New returns a new password Generator type -func New(c *Config) *Generator { +func New(config *Config) *Generator { return &Generator{ - config: c, + config: config, } } From b6f91459c5eef90b5c8cbb2ad5e330485ee3f6bf Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Mar 2024 10:22:20 +0100 Subject: [PATCH 27/53] Refactor variable names in mode.go Renamed variables in the MaskSetMode, MaskClearMode, MaskToggleMode, and MaskHasMode functions as well as within ModesFromFlags function to improve clarity and readability of their functionality. These changes ensure that the purpose of each variable is immediately identifiable, enhancing maintainability of the codebase. --- mode.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/mode.go b/mode.go index 17ee59b..d268464 100644 --- a/mode.go +++ b/mode.go @@ -46,48 +46,48 @@ const ( ) // MaskSetMode sets a specific Mode to a given Mode bitmask -func MaskSetMode(ma ModeMask, mo Mode) ModeMask { return ModeMask(uint8(ma) | uint8(mo)) } +func MaskSetMode(mask ModeMask, mode Mode) ModeMask { return ModeMask(uint8(mask) | uint8(mode)) } // MaskClearMode clears a specific Mode from a given Mode bitmask -func MaskClearMode(ma ModeMask, mo Mode) ModeMask { return ModeMask(uint8(ma) &^ uint8(mo)) } +func MaskClearMode(mask ModeMask, mode Mode) ModeMask { return ModeMask(uint8(mask) &^ uint8(mode)) } // MaskToggleMode toggles a specific Mode in a given Mode bitmask -func MaskToggleMode(ma ModeMask, mo Mode) ModeMask { return ModeMask(uint8(ma) ^ uint8(mo)) } +func MaskToggleMode(mask ModeMask, mode Mode) ModeMask { return ModeMask(uint8(mask) ^ uint8(mode)) } // MaskHasMode returns true if a given Mode bitmask holds a specific Mode -func MaskHasMode(ma ModeMask, mo Mode) bool { return uint8(ma)&uint8(mo) != 0 } +func MaskHasMode(mask ModeMask, mode Mode) bool { return uint8(mask)&uint8(mode) != 0 } -func ModesFromFlags(ms string) ModeMask { - cl := strings.Split(ms, "") - var mm ModeMask +func ModesFromFlags(maskString string) ModeMask { + cl := strings.Split(maskString, "") + var modeMask ModeMask for _, m := range cl { switch m { case "C": - mm = MaskSetMode(mm, ModeLowerCase|ModeNumeric|ModeSpecial|ModeUpperCase) + modeMask = MaskSetMode(modeMask, ModeLowerCase|ModeNumeric|ModeSpecial|ModeUpperCase) case "h": - mm = MaskClearMode(mm, ModeHumanReadable) + modeMask = MaskClearMode(modeMask, ModeHumanReadable) case "H": - mm = MaskSetMode(mm, ModeHumanReadable) + modeMask = MaskSetMode(modeMask, ModeHumanReadable) case "l": - mm = MaskClearMode(mm, ModeLowerCase) + modeMask = MaskClearMode(modeMask, ModeLowerCase) case "L": - mm = MaskSetMode(mm, ModeLowerCase) + modeMask = MaskSetMode(modeMask, ModeLowerCase) case "n": - mm = MaskClearMode(mm, ModeNumeric) + modeMask = MaskClearMode(modeMask, ModeNumeric) case "N": - mm = MaskSetMode(mm, ModeNumeric) + modeMask = MaskSetMode(modeMask, ModeNumeric) case "s": - mm = MaskClearMode(mm, ModeSpecial) + modeMask = MaskClearMode(modeMask, ModeSpecial) case "S": - mm = MaskSetMode(mm, ModeSpecial) + modeMask = MaskSetMode(modeMask, ModeSpecial) case "u": - mm = MaskClearMode(mm, ModeUpperCase) + modeMask = MaskClearMode(modeMask, ModeUpperCase) case "U": - mm = MaskSetMode(mm, ModeUpperCase) + modeMask = MaskSetMode(modeMask, ModeUpperCase) } } - return mm + return modeMask } // String satisfies the fmt.Stringer interface for the Mode type From 0ad5f4a74d87a7298fb6887136a196c82c5b677b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Mar 2024 10:22:34 +0100 Subject: [PATCH 28/53] Refactor variable names in random.go Updated variable naming within the CoinFlip function to enhance clarity. Also added an additional case statement within the Generate function for the 'AlgoPronouncable' algorithm. These changes improve readability and extend the functionality of the random number generation code. --- random.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/random.go b/random.go index 27936e7..893339b 100644 --- a/random.go +++ b/random.go @@ -32,8 +32,8 @@ var ( // CoinFlip performs a simple coinflip based on the rand library and returns 1 or 0 func (g *Generator) CoinFlip() int64 { - cf, _ := g.RandNum(2) - return cf + coinFlip, _ := g.RandNum(2) + return coinFlip } // CoinFlipBool performs a simple coinflip based on the rand library and returns true or false @@ -45,6 +45,7 @@ func (g *Generator) CoinFlipBool() bool { // it as string type. If the generation fails, an error will be thrown func (g *Generator) Generate() (string, error) { switch g.config.Algorithm { + case AlgoPronouncable: case AlgoCoinFlip: return g.generateCoinFlip() case AlgoRandom: From 4219a2700719606129aab05dda026ae61d8ea2ad Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Mar 2024 16:02:32 +0100 Subject: [PATCH 29/53] Refactor variable names and improve code readability in apg.go Updated several variable names such as 'c' to 'config' and 'al' to 'algorithm' in order to improve code readability and understanding. An additional 'SpellPassword' option was added to provide users the ability to hear their passwords spelled out, enhancing functionality. This improves readability and user experience. --- cmd/apg/apg.go | 117 ++++++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 54 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 64b9373..16e6996 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -18,102 +18,111 @@ const MinimumAmountTooHigh = "WARNING: You have selected a minimum amount of cha "the job. Please consider lowering the value.\n\n" func main() { - c := apg.NewConfig() + config := apg.NewConfig() // Configure and parse the CLI flags // See usage() for flag details - var al int - var ms string - var co, hr, lc, nu, sp, uc bool - flag.IntVar(&al, "a", 1, "") - flag.BoolVar(&lc, "L", false, "") - flag.Int64Var(&c.MinLowerCase, "mL", c.MinLowerCase, "") - flag.BoolVar(&uc, "U", false, "") - flag.Int64Var(&c.MinUpperCase, "mU", c.MinUpperCase, "") - flag.BoolVar(&nu, "N", false, "") - flag.Int64Var(&c.MinNumeric, "mN", c.MinNumeric, "") - flag.BoolVar(&sp, "S", false, "") - flag.Int64Var(&c.MinSpecial, "mS", c.MinSpecial, "") - flag.BoolVar(&co, "C", false, "") - flag.BoolVar(&hr, "H", false, "") - flag.Int64Var(&c.FixedLength, "f", 0, "") - flag.Int64Var(&c.MinLength, "m", c.MinLength, "") - flag.Int64Var(&c.MaxLength, "x", c.MaxLength, "") - flag.StringVar(&ms, "M", "", "") - flag.Int64Var(&c.NumberPass, "n", c.NumberPass, "") + var algorithm int + var modeString string + var complexPass, humanReadable, lowerCase, numeric, special, upperCase bool + flag.IntVar(&algorithm, "a", 1, "") + flag.BoolVar(&lowerCase, "L", false, "") + flag.Int64Var(&config.MinLowerCase, "mL", config.MinLowerCase, "") + flag.BoolVar(&upperCase, "U", false, "") + flag.Int64Var(&config.MinUpperCase, "mU", config.MinUpperCase, "") + flag.BoolVar(&numeric, "N", false, "") + flag.Int64Var(&config.MinNumeric, "mN", config.MinNumeric, "") + flag.BoolVar(&special, "S", false, "") + flag.Int64Var(&config.MinSpecial, "mS", config.MinSpecial, "") + flag.BoolVar(&complexPass, "C", false, "") + flag.BoolVar(&humanReadable, "H", false, "") + flag.Int64Var(&config.FixedLength, "f", 0, "") + flag.Int64Var(&config.MinLength, "m", config.MinLength, "") + flag.Int64Var(&config.MaxLength, "x", config.MaxLength, "") + flag.StringVar(&modeString, "M", "", "") + flag.Int64Var(&config.NumberPass, "n", config.NumberPass, "") + flag.BoolVar(&config.SpellPassword, "l", false, "") flag.Usage = usage flag.Parse() // Old style character modes - if hr { - c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeHumanReadable) + if humanReadable { + config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeHumanReadable) } - if lc { - c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeLowerCase) + if lowerCase { + config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeLowerCase) } - if uc { - c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeUpperCase) + if upperCase { + config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeUpperCase) } - if nu { - c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeNumeric) + if numeric { + config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeNumeric) } - if sp { - c.Mode = apg.MaskToggleMode(c.Mode, apg.ModeSpecial) + if special { + config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeSpecial) } - if co { - c.Mode = apg.MaskSetMode(c.Mode, apg.ModeLowerCase|apg.ModeNumeric| + if complexPass { + config.Mode = apg.MaskSetMode(config.Mode, apg.ModeLowerCase|apg.ModeNumeric| apg.ModeSpecial|apg.ModeUpperCase) - c.Mode = apg.MaskClearMode(c.Mode, apg.ModeHumanReadable) + config.Mode = apg.MaskClearMode(config.Mode, apg.ModeHumanReadable) } // New style character modes (has higher priority than the old style modes) - if ms != "" { - c.Mode = apg.ModesFromFlags(ms) + if modeString != "" { + config.Mode = apg.ModesFromFlags(modeString) } // For the "minimum amount of" modes we need to imply at the type // of character mode is set - if c.MinLowerCase > 0 { - if float64(c.MinLength)/2 < float64(c.MinNumeric) { + if config.MinLowerCase > 0 { + if float64(config.MinLength)/2 < float64(config.MinNumeric) { _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) } - c.Mode = apg.MaskSetMode(c.Mode, apg.ModeLowerCase) + config.Mode = apg.MaskSetMode(config.Mode, apg.ModeLowerCase) } - if c.MinNumeric > 0 { - if float64(c.MinLength)/2 < float64(c.MinLowerCase) { + if config.MinNumeric > 0 { + if float64(config.MinLength)/2 < float64(config.MinLowerCase) { _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) } - c.Mode = apg.MaskSetMode(c.Mode, apg.ModeNumeric) + config.Mode = apg.MaskSetMode(config.Mode, apg.ModeNumeric) } - if c.MinSpecial > 0 { - if float64(c.MinLength)/2 < float64(c.MinSpecial) { + if config.MinSpecial > 0 { + if float64(config.MinLength)/2 < float64(config.MinSpecial) { _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) } - c.Mode = apg.MaskSetMode(c.Mode, apg.ModeSpecial) + config.Mode = apg.MaskSetMode(config.Mode, apg.ModeSpecial) } - if c.MinUpperCase > 0 { - if float64(c.MinLength)/2 < float64(c.MinUpperCase) { + if config.MinUpperCase > 0 { + if float64(config.MinLength)/2 < float64(config.MinUpperCase) { _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) } - c.Mode = apg.MaskSetMode(c.Mode, apg.ModeUpperCase) + config.Mode = apg.MaskSetMode(config.Mode, apg.ModeUpperCase) } // Check if algorithm is supported - c.Algorithm = apg.IntToAlgo(al) - if c.Algorithm == apg.AlgoUnsupported { - _, _ = fmt.Fprintf(os.Stderr, "unsupported algorithm value: %d\n", al) + config.Algorithm = apg.IntToAlgo(algorithm) + if config.Algorithm == apg.AlgoUnsupported { + _, _ = fmt.Fprintf(os.Stderr, "unsupported algorithm value: %d\n", algorithm) os.Exit(1) } // Generate the password based on the given flags - g := apg.New(c) - for i := int64(0); i < c.NumberPass; i++ { - p, err := g.Generate() + generator := apg.New(config) + for i := int64(0); i < config.NumberPass; i++ { + password, err := generator.Generate() if err != nil { _, _ = fmt.Fprintf(os.Stderr, "failed to generate password: %s\n", err) os.Exit(1) } - fmt.Println(p) + if config.SpellPassword { + spellPass, err := apg.Spell(password) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to spell password: %s\n", err) + } + fmt.Printf("%s (%s)\n", password, spellPass) + continue + } + fmt.Println(password) } } From 93f092e6902edef8028c0d355207c08b5171f07e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Mar 2024 16:03:02 +0100 Subject: [PATCH 30/53] Add phonetic spelling functionality to password generator The update introduces a new 'SpellPassword' setting in the configuration that, when enabled, spells out the generated passwords in the phonetic alphabet. The accompanied changes include the addition of 'spelling.go' and 'spelling_test.go' files containing the spelling logic and corresponding tests. The domain-specific error handling is also enhanced for unsupported characters. --- config.go | 2 + spelling.go | 113 +++++++++++++++++++++++++++++++++++++++++++++++ spelling_test.go | 53 ++++++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 spelling.go create mode 100644 spelling_test.go diff --git a/config.go b/config.go index e1106bc..b5253c9 100644 --- a/config.go +++ b/config.go @@ -41,6 +41,8 @@ type Config struct { // NumberPass sets the number of passwords that are generated // and returned by the generator NumberPass int64 + // SpellPassword if set will spell the generated passwords in the phonetic alphabet + SpellPassword bool } // Option is a function that can override default Config settings diff --git a/spelling.go b/spelling.go new file mode 100644 index 0000000..67b0028 --- /dev/null +++ b/spelling.go @@ -0,0 +1,113 @@ +package apg + +import ( + "fmt" + "strings" +) + +var ( + symbNumNames = map[byte]string{ + '1': "ONE", + '2': "TWO", + '3': "THREE", + '4': "FOUR", + '5': "FIVE", + '6': "SIX", + '7': "SEVEN", + '8': "EIGHT", + '9': "NINE", + '0': "ZERO", + 33: "EXCLAMATION_POINT", + 34: "QUOTATION_MARK", + 35: "CROSSHATCH", + 36: "DOLLAR_SIGN", + 37: "PERCENT_SIGN", + 38: "AMPERSAND", + 39: "APOSTROPHE", + 40: "LEFT_PARENTHESIS", + 41: "RIGHT_PARENTHESIS", + 42: "ASTERISK", + 43: "PLUS_SIGN", + 44: "COMMA", + 45: "HYPHEN", + 46: "PERIOD", + 47: "SLASH", + 58: "COLON", + 59: "SEMICOLON", + 60: "LESS_THAN", + 61: "EQUAL_SIGN", + 62: "GREATER_THAN", + 63: "QUESTION_MARK", + 64: "AT_SIGN", + 91: "LEFT_BRACKET", + 92: "BACKSLASH", + 93: "RIGHT_BRACKET", + 94: "CIRCUMFLEX", + 95: "UNDERSCORE", + 96: "GRAVE", + 123: "LEFT_BRACE", + 124: "VERTICAL_BAR", + 125: "RIGHT_BRACE", + 126: "TILDE", + } + alphabetNames = map[byte]string{ + 'A': "Alfa", + 'B': "Bravo", + 'C': "Charlie", + 'D': "Delta", + 'E': "Echo", + 'F': "Foxtrot", + 'G': "Golf", + 'H': "Hotel", + 'I': "India", + 'J': "Juliett", + 'K': "Kilo", + 'L': "Lima", + 'M': "Mike", + 'N': "November", + 'O': "Oscar", + 'P': "Papa", + 'Q': "Quebec", + 'R': "Romeo", + 'S': "Sierra", + 'T': "Tango", + 'U': "Uniform", + 'V': "Victor", + 'W': "Whiskey", + 'X': "X_ray", + 'Y': "Yankee", + 'Z': "Zulu", + } +) + +// Spell returns a given string as spelled english phonetic alphabet string +func Spell(input string) (string, error) { + var returnString []string + for _, curChar := range input { + curSpellString, err := ConvertByteToWord(byte(curChar)) + if err != nil { + return "", err + } + returnString = append(returnString, curSpellString) + } + return strings.Join(returnString, "/"), nil +} + +// ConvertByteToWord converts a given ASCII byte into the corresponding spelled version +// of the english phonetic alphabet +func ConvertByteToWord(charByte byte) (string, error) { + var returnString string + switch { + case charByte > 64 && charByte < 91: + returnString = alphabetNames[charByte] + case charByte > 96 && charByte < 123: + returnString = strings.ToLower(alphabetNames[charByte-32]) + default: + returnString = symbNumNames[charByte] + } + + if returnString == "" { + return "", fmt.Errorf("failed to convert given byte to word: %s is unsupported", string(charByte)) + } + return returnString, nil +} diff --git a/spelling_test.go b/spelling_test.go new file mode 100644 index 0000000..039ef08 --- /dev/null +++ b/spelling_test.go @@ -0,0 +1,53 @@ +package apg + +import ( + "strings" + "testing" +) + +func TestConvertByteToWord(t *testing.T) { + tests := []struct { + name string + char byte + want string + wantErr bool + }{ + { + name: "UpperCaseChar", + char: 'A', + want: alphabetNames['A'], + wantErr: false, + }, + { + name: "LowerCaseChar", + char: 'a', + want: strings.ToLower(alphabetNames['A']), + wantErr: false, + }, + { + name: "NonAlphaChar", + char: '(', + want: symbNumNames['('], + wantErr: false, + }, + { + name: "UnsupportedChar", + char: 'ü', + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertByteToWord(tt.char) + if (err != nil) != tt.wantErr { + t.Errorf("ConvertByteToWord() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ConvertByteToWord() got = %v, want %v", got, tt.want) + } + }) + } +} From bd654d40b8ff1d94fd4f18f67b6edb3b46c211e2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Mar 2024 16:04:16 +0100 Subject: [PATCH 31/53] Refactor error messages in spelling tests Modified the error messages in spelling tests within spelling_test.go to improve clarity and readability. These adjustments involve changing the format specifiers in the error messages of 'ConvertByteToWord' function tests to correspond with the expected data types for better error reporting. --- spelling_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spelling_test.go b/spelling_test.go index 039ef08..bfc3d6c 100644 --- a/spelling_test.go +++ b/spelling_test.go @@ -42,11 +42,11 @@ func TestConvertByteToWord(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := ConvertByteToWord(tt.char) if (err != nil) != tt.wantErr { - t.Errorf("ConvertByteToWord() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConvertByteToWord() error = %s, wantErr %t", err, tt.wantErr) return } if got != tt.want { - t.Errorf("ConvertByteToWord() got = %v, want %v", got, tt.want) + t.Errorf("ConvertByteToWord() got = %s, want %s", got, tt.want) } }) } From 72576961e64b37663ffcd13cca109c5a6a4b132d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Mar 2024 17:07:55 +0100 Subject: [PATCH 32/53] Implement pronouncable password generation and refactor code The new function "generatePronouncable" generates pronounceable passwords using the Koremutake syllabic representation. It is executed when 'Generate()' method is called with Algorithm set to 'AlgoPronouncable'. Additionally, significant changes were made to enhance the readability and performance of the 'GetCharRangeFromConfig' and 'checkMinimumRequirements' methods. In 'mode_test.go', the error message format has been updated for clear and concise display. --- koremutake.go | 22 +++++++++ random.go | 125 ++++++++++++++++++++++++++++++++++---------------- 2 files changed, 108 insertions(+), 39 deletions(-) create mode 100644 koremutake.go diff --git a/koremutake.go b/koremutake.go new file mode 100644 index 0000000..6af5ed8 --- /dev/null +++ b/koremutake.go @@ -0,0 +1,22 @@ +package apg + +// KoremutakeSyllables is a slightly modified Koremutake syllables list based on +// the mechanism described on https://shorl.com/koremutake.php +var KoremutakeSyllables = []string{"ba", "be", "bi", "bo", "bu", "by", "da", "de", "di", + "do", "du", "dy", "fe", "fi", "fo", "fu", "fy", "ga", "ge", "gi", "go", "gu", + "gy", "ha", "he", "hi", "ho", "hu", "hy", "ja", "je", "ji", "jo", "ju", "jy", + "ka", "ke", "ko", "ku", "ky", "la", "le", "li", "lo", "lu", "ly", "ma", + "me", "mi", "mo", "mu", "my", "na", "ne", "ni", "no", "nu", "ny", "pa", "pe", + "pi", "po", "pu", "py", "ra", "re", "ri", "ro", "ru", "ry", "sa", "se", + "si", "so", "su", "sy", "ta", "te", "ti", "to", "tu", "ty", "va", "ve", "vi", + "vo", "vu", "vy", "bra", "bre", "bri", "bro", "bru", "bry", "dra", "dre", + "dri", "dro", "dru", "dry", "fra", "fre", "fri", "fro", "fru", "fry", "gra", + "gre", "gri", "gro", "gru", "gry", "pra", "pre", "pri", "pro", "pru", + "pry", "sta", "ste", "sti", "sto", "stu", "sty", "tra", "tre", "er", "ed", + "in", "ex", "al", "en", "an", "ad", "or", "at", "ca", "ap", "el", "ci", "an", + "et", "it", "ob", "of", "af", "au", "cy", "im", "op", "co", "up", "ing", + "con", "ter", "com", "per", "ble", "der", "cal", "man", "est", "for", "mer", + "col", "ful", "get", "low", "son", "tle", "day", "pen", "pre", "ten", + "tor", "ver", "ber", "can", "ple", "fer", "gen", "den", "mag", "sub", "sur", + "men", "min", "out", "tal", "but", "cit", "cle", "cov", "dif", "ern", + "eve", "hap", "ket", "nal", "sup", "ted", "tem", "tin", "tro", "tro"} diff --git a/random.go b/random.go index 893339b..8e0b665 100644 --- a/random.go +++ b/random.go @@ -46,6 +46,7 @@ func (g *Generator) CoinFlipBool() bool { func (g *Generator) Generate() (string, error) { switch g.config.Algorithm { case AlgoPronouncable: + return g.generatePronouncable() case AlgoCoinFlip: return g.generateCoinFlip() case AlgoRandom: @@ -56,6 +57,45 @@ func (g *Generator) Generate() (string, error) { return "", nil } +// GetCharRangeFromConfig checks the Mode from the Config and returns a +// list of all possible characters that are supported by these Mode +func (g *Generator) GetCharRangeFromConfig() string { + charRange := strings.Builder{} + if MaskHasMode(g.config.Mode, ModeLowerCase) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + charRange.WriteString(CharRangeAlphaLowerHuman) + default: + charRange.WriteString(CharRangeAlphaLower) + } + } + if MaskHasMode(g.config.Mode, ModeNumeric) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + charRange.WriteString(CharRangeNumericHuman) + default: + charRange.WriteString(CharRangeNumeric) + } + } + if MaskHasMode(g.config.Mode, ModeSpecial) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + charRange.WriteString(CharRangeSpecialHuman) + default: + charRange.WriteString(CharRangeSpecial) + } + } + if MaskHasMode(g.config.Mode, ModeUpperCase) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + charRange.WriteString(CharRangeAlphaUpperHuman) + default: + charRange.WriteString(CharRangeAlphaUpper) + } + } + return charRange.String() +} + // GetPasswordLength returns the password length based on the given config // parameters func (g *Generator) GetPasswordLength() (int64, error) { @@ -154,45 +194,14 @@ func (g *Generator) RandomStringFromCharRange(length int64, charRange string) (s return randString.String(), nil } -// GetCharRangeFromConfig checks the Mode from the Config and returns a -// list of all possible characters that are supported by these Mode -func (g *Generator) GetCharRangeFromConfig() string { - charRange := strings.Builder{} - if MaskHasMode(g.config.Mode, ModeLowerCase) { - switch MaskHasMode(g.config.Mode, ModeHumanReadable) { - case true: - charRange.WriteString(CharRangeAlphaLowerHuman) - default: - charRange.WriteString(CharRangeAlphaLower) - } - } - if MaskHasMode(g.config.Mode, ModeNumeric) { - switch MaskHasMode(g.config.Mode, ModeHumanReadable) { - case true: - charRange.WriteString(CharRangeNumericHuman) - default: - charRange.WriteString(CharRangeNumeric) - } - } - if MaskHasMode(g.config.Mode, ModeSpecial) { - switch MaskHasMode(g.config.Mode, ModeHumanReadable) { - case true: - charRange.WriteString(CharRangeSpecialHuman) - default: - charRange.WriteString(CharRangeSpecial) - } - } - if MaskHasMode(g.config.Mode, ModeUpperCase) { - switch MaskHasMode(g.config.Mode, ModeHumanReadable) { - case true: - charRange.WriteString(CharRangeAlphaUpperHuman) - default: - charRange.WriteString(CharRangeAlphaUpper) - } - } - return charRange.String() -} - +// checkMinimumRequirements checks if a password meets the minimum requirements specified in the +// generator's configuration. It returns true if the password meets the requirements, otherwise it +// returns false. +// +// The minimum requirements for each character type (lowercase, numeric, special, uppercase) are +// checked independently. For each character type, the corresponding character range is determined +// based on the generator's configuration. The password is then checked for the presence of each +// character in the character range, and a count is maintained. func (g *Generator) checkMinimumRequirements(password string) bool { ok := true if g.config.MinLowerCase > 0 { @@ -275,6 +284,44 @@ func (g *Generator) generateCoinFlip() (string, error) { return "Tails", nil } +// generatePronouncable is executed when Generate() is called with Algorithm set +// to AlgoPronouncable +func (g *Generator) generatePronouncable() (string, error) { + var password string + syllables := make([]string, 0) + + length, err := g.GetPasswordLength() + if err != nil { + return "", fmt.Errorf("failed to calculate password length: %w", err) + } + + characterSet := append(KoremutakeSyllables, strings.Split(CharRangeNumericHuman, "")...) + characterSet = append(characterSet, strings.Split(CharRangeSpecialHuman, "")...) + characterSetLength := len(characterSet) + for int64(len(password)) < length { + randNum, err := g.RandNum(int64(characterSetLength)) + if err != nil { + return "", fmt.Errorf("failed to generate a random number for Koremutake syllable generation: %s", + err) + } + nextSyllable := characterSet[randNum] + if g.CoinFlipBool() { + syllableLength := len(nextSyllable) + characterPosition, err := g.RandNum(int64(syllableLength)) + if err != nil { + return "", fmt.Errorf("failed to generate a random number for Koremutake syllable generation: %s", + err) + } + randomChar := string(nextSyllable[characterPosition]) + nextSyllable = strings.ReplaceAll(nextSyllable, randomChar, strings.ToUpper(randomChar)) + } + password += nextSyllable + syllables = append(syllables, nextSyllable) + } + + return password, nil +} + // generateRandom is executed when Generate() is called with Algorithm set // to AlgoRandmom func (g *Generator) generateRandom() (string, error) { From 1c22c9b8f0e249e6516b4a3552471f0182644a06 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Mar 2024 17:08:05 +0100 Subject: [PATCH 33/53] Refactor error message in `mode_test.go` The error message format in `ModesFromFlags()` method within `mode_test.go` has been refactored. The refactoring improved the clarity and conciseness of error reporting when the expected mode is not found during test execution. --- mode_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mode_test.go b/mode_test.go index 0c5d618..9ec2867 100644 --- a/mode_test.go +++ b/mode_test.go @@ -84,8 +84,7 @@ func TestModesFromFlags(t *testing.T) { mm := ModesFromFlags(tc.ms) for _, tm := range tc.mode { if !MaskHasMode(mm, tm) { - t.Errorf("ModesFromFlags() failed, expected mode %q not found", - tm) + t.Errorf("ModesFromFlags() failed, expected mode %q not found", tm) } } }) From 45b45919c1ca215948beee8d17e7ac24093d082c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 12:14:36 +0100 Subject: [PATCH 34/53] Add TestGetCharRangeFromConfig function in random_test.go A comprehensive unit test, TestGetCharRangeFromConfig, has been added to the random_test.go file. This test validates the GetCharRangeFromConfig function across various scenarios and configurations. It also improves code reliability and makes the application more robust against potential issues. --- random_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/random_test.go b/random_test.go index a7fc479..d49776d 100644 --- a/random_test.go +++ b/random_test.go @@ -147,6 +147,76 @@ func TestGenerator_RandomString(t *testing.T) { } } +func TestGetCharRangeFromConfig(t *testing.T) { + // Arrange + config := NewConfig() + generator := New(config) + + // Test cases + testCases := []struct { + Name string + ConfigMode ModeMask + ExpectedRange string + }{ + { + Name: "LowerCaseHumanReadable", + ConfigMode: ModeLowerCase | ModeHumanReadable, + ExpectedRange: CharRangeAlphaLowerHuman, + }, + { + Name: "LowerCaseNonHumanReadable", + ConfigMode: ModeLowerCase, + ExpectedRange: CharRangeAlphaLower, + }, + { + Name: "NumericHumanReadable", + ConfigMode: ModeNumeric | ModeHumanReadable, + ExpectedRange: CharRangeNumericHuman, + }, + { + Name: "NumericNonHumanReadable", + ConfigMode: ModeNumeric, + ExpectedRange: CharRangeNumeric, + }, + { + Name: "SpecialHumanReadable", + ConfigMode: ModeSpecial | ModeHumanReadable, + ExpectedRange: CharRangeSpecialHuman, + }, + { + Name: "SpecialNonHumanReadable", + ConfigMode: ModeSpecial, + ExpectedRange: CharRangeSpecial, + }, + { + Name: "UpperCaseHumanReadable", + ConfigMode: ModeUpperCase | ModeHumanReadable, + ExpectedRange: CharRangeAlphaUpperHuman, + }, + { + Name: "UpperCaseNonHumanReadable", + ConfigMode: ModeUpperCase, + ExpectedRange: CharRangeAlphaUpper, + }, + { + Name: "MultipleModes", + ConfigMode: ModeLowerCase | ModeNumeric | ModeUpperCase | ModeHumanReadable, + ExpectedRange: CharRangeAlphaLowerHuman + CharRangeNumericHuman + CharRangeAlphaUpperHuman, + }, + } + + // Act and assert for each test case + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + generator.config.Mode = tc.ConfigMode + actualRange := generator.GetCharRangeFromConfig() + if actualRange != tc.ExpectedRange { + t.Errorf("Expected range %s, got %s", tc.ExpectedRange, actualRange) + } + }) + } +} + func BenchmarkGenerator_CoinFlip(b *testing.B) { b.ReportAllocs() g := New(NewConfig()) From a03f1707380bf9b811fd7490aeb521d0ecbac4be Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 12:14:46 +0100 Subject: [PATCH 35/53] Add TestSpell function in spelling_test.go Added a new unit test, TestSpell, to the spelling_test.go file. This new test safely triggers several different spell checking scenarios, from empty strings to non-alphabetical characters. As a result, the function's reliability and robustness is significantly improved. --- spelling_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/spelling_test.go b/spelling_test.go index bfc3d6c..82fe73f 100644 --- a/spelling_test.go +++ b/spelling_test.go @@ -51,3 +51,62 @@ func TestConvertByteToWord(t *testing.T) { }) } } + +func TestSpell(t *testing.T) { + tests := []struct { + name string + input string + want string + wantErr bool + }{ + { + name: "empty string", + input: "", + want: "", + wantErr: false, + }, + { + name: "single character", + input: "a", + want: "alfa", + wantErr: false, + }, + { + name: "multiple characters", + input: "abc", + want: "alfa/bravo/charlie", + wantErr: false, + }, + { + name: "non-alphabetic character", + input: "1", + want: "ONE", + wantErr: false, + }, + { + name: "mixed alphabetic and non-alphabetic characters", + input: "a1", + want: "alfa/ONE", + wantErr: false, + }, + { + name: "not supported characters", + input: "üäö߀", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Spell(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("Spell() error = %s, wantErr %t", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Spell() = %s, want %s", got, tt.want) + } + }) + } +} From 2973ff4c39a361e0c9265370387e06249f169404 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 16:43:20 +0100 Subject: [PATCH 36/53] Remove unnecessary comments in random_test.go Removed redundant comments in random_test.go, specifically in TestGetCharRangeFromConfig function. This change simplifies and tidies the code, while maintaining its comprehensibility and the clarity of the test cases structure. --- random_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/random_test.go b/random_test.go index d49776d..15de2a3 100644 --- a/random_test.go +++ b/random_test.go @@ -148,11 +148,8 @@ func TestGenerator_RandomString(t *testing.T) { } func TestGetCharRangeFromConfig(t *testing.T) { - // Arrange config := NewConfig() generator := New(config) - - // Test cases testCases := []struct { Name string ConfigMode ModeMask @@ -205,7 +202,6 @@ func TestGetCharRangeFromConfig(t *testing.T) { }, } - // Act and assert for each test case for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { generator.config.Mode = tc.ConfigMode From 8cb702c932d8cae6f2ccbbbd13d37c94bf8ddd68 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 16:57:48 +0100 Subject: [PATCH 37/53] Add password length and coin flip tests in random_test.go Added detailed test cases for handling password length and coin flip functionality in random_test.go. These additional unit tests cover various conditions for password lengths, and ensure that 'generateCoinFlip' behaves as expected across multiple invocations. --- random_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/random_test.go b/random_test.go index 15de2a3..9aced01 100644 --- a/random_test.go +++ b/random_test.go @@ -2,6 +2,7 @@ package apg import ( "bytes" + "errors" "strings" "testing" ) @@ -213,6 +214,90 @@ func TestGetCharRangeFromConfig(t *testing.T) { } } +func TestGetPasswordLength(t *testing.T) { + config := NewConfig() + generator := New(config) + testCases := []struct { + Name string + ConfigFixedLength int64 + ConfigMinLength int64 + ConfigMaxLength int64 + ExpectedLength int64 + ExpectedError error + }{ + { + Name: "FixedLength", + ConfigFixedLength: 10, + ConfigMinLength: 5, + ConfigMaxLength: 15, + ExpectedLength: 10, + ExpectedError: nil, + }, + { + Name: "MinLengthEqualToMaxLength", + ConfigFixedLength: 0, + ConfigMinLength: 8, + ConfigMaxLength: 8, + ExpectedLength: 8, + ExpectedError: nil, + }, + { + Name: "MinLengthGreaterThanMaxLength", + ConfigFixedLength: 0, + ConfigMinLength: 12, + ConfigMaxLength: 5, + ExpectedLength: 12, + ExpectedError: nil, + }, + } + + // Act and assert for each test case + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + generator.config.FixedLength = tc.ConfigFixedLength + generator.config.MinLength = tc.ConfigMinLength + generator.config.MaxLength = tc.ConfigMaxLength + length, err := generator.GetPasswordLength() + + if err != nil && !errors.Is(err, tc.ExpectedError) { + t.Errorf("Unexpected error: %v", err) + } else if err == nil && tc.ExpectedError != nil { + t.Errorf("Expected error %v, got nil", tc.ExpectedError) + } else if err == nil && length != tc.ExpectedLength { + t.Errorf("Expected length %d, got %d", tc.ExpectedLength, length) + } + }) + } +} + +// TestGenerateCoinFlip tries to test the coinflip. Randomness is hard to +// test, since it's supposed to be not prodictable. We think that in 100k +// tries at least one of each two results should be returned, even though +// it is possible that not. Therefore we only throw a warning +func TestGenerateCoinFlip(t *testing.T) { + config := NewConfig() + generator := New(config) + foundTails := false + foundHeads := false + for range 100_000 { + res, err := generator.generateCoinFlip() + if err != nil { + t.Errorf("generateCoinFlip() failed: %s", err) + return + } + switch res { + case "Tails": + foundTails = true + case "Heads": + foundHeads = true + } + } + if !foundTails && !foundHeads { + t.Logf("WARNING: generateCoinFlip() was supposed to find heads and tails "+ + "in 100_000 tries but didn't. Heads: %t, Tails: %t", foundHeads, foundTails) + } +} + func BenchmarkGenerator_CoinFlip(b *testing.B) { b.ReportAllocs() g := New(NewConfig()) From 90ff88de41a37ad3a43e1c680d7924f97977e4f8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 17:49:26 +0100 Subject: [PATCH 38/53] Implement additional tests for password generation functionality In this commit, several new tests have been introduced into random_test.go. These include cases for generating pronounceable passwords, testing minimum requirement conditions, and checking different password algorithms. This provides more comprehensive testing coverage and confirms the expected functionality of password generator methods. --- random_test.go | 182 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 181 insertions(+), 1 deletion(-) diff --git a/random_test.go b/random_test.go index 9aced01..3e99e5a 100644 --- a/random_test.go +++ b/random_test.go @@ -3,6 +3,8 @@ package apg import ( "bytes" "errors" + "fmt" + "strconv" "strings" "testing" ) @@ -251,7 +253,6 @@ func TestGetPasswordLength(t *testing.T) { }, } - // Act and assert for each test case for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { generator.config.FixedLength = tc.ConfigFixedLength @@ -298,6 +299,185 @@ func TestGenerateCoinFlip(t *testing.T) { } } +func TestGeneratePronouncable(t *testing.T) { + config := NewConfig() + generator := New(config) + foundSylables := 0 + for range 100 { + res, err := generator.generatePronouncable() + if err != nil { + t.Errorf("generatePronouncable() failed: %s", err) + return + } + for _, syl := range KoremutakeSyllables { + if strings.Contains(res, syl) { + foundSylables++ + } + } + } + if foundSylables < 100 { + t.Errorf("generatePronouncable() failed, expected at least 1 sylable, got none") + } + +} + +func TestCheckMinimumRequirements(t *testing.T) { + config := NewConfig() + generator := New(config) + testCases := []struct { + Name string + Password string + ConfigMinLowerCase int64 + ConfigMinNumeric int64 + ConfigMinSpecial int64 + ConfigMinUpperCase int64 + ExpectedResult bool + }{ + { + Name: "Meets all requirements", + Password: "Th1sIsA$trongP@ssword", + ConfigMinLowerCase: 2, + ConfigMinNumeric: 1, + ConfigMinSpecial: 1, + ConfigMinUpperCase: 1, + ExpectedResult: true, + }, + { + Name: "Missing lowercase", + Password: "THISIS@STRONGPASSWORD", + ConfigMinLowerCase: 2, + ConfigMinNumeric: 1, + ConfigMinSpecial: 1, + ConfigMinUpperCase: 1, + ExpectedResult: false, + }, + { + Name: "Missing numeric", + Password: "ThisIsA$trongPassword", + ConfigMinLowerCase: 2, + ConfigMinNumeric: 1, + ConfigMinSpecial: 1, + ConfigMinUpperCase: 1, + ExpectedResult: false, + }, + { + Name: "Missing special", + Password: "ThisIsALowercaseNumericPassword", + ConfigMinLowerCase: 2, + ConfigMinNumeric: 1, + ConfigMinSpecial: 1, + ConfigMinUpperCase: 1, + ExpectedResult: false, + }, + { + Name: "Missing uppercase", + Password: "thisisanumericspecialpassword", + ConfigMinLowerCase: 2, + ConfigMinNumeric: 1, + ConfigMinSpecial: 1, + ConfigMinUpperCase: 1, + ExpectedResult: false, + }, + { + Name: "Bare minimum", + Password: "a1!", + ConfigMinLowerCase: 1, + ConfigMinNumeric: 1, + ConfigMinSpecial: 1, + ConfigMinUpperCase: 0, + ExpectedResult: true, + }, + { + Name: "Empty password", + Password: "", + ConfigMinLowerCase: 1, + ConfigMinNumeric: 1, + ConfigMinSpecial: 1, + ConfigMinUpperCase: 1, + ExpectedResult: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + generator.config.MinLowerCase = tc.ConfigMinLowerCase + generator.config.MinNumeric = tc.ConfigMinNumeric + generator.config.MinSpecial = tc.ConfigMinSpecial + generator.config.MinUpperCase = tc.ConfigMinUpperCase + result := generator.checkMinimumRequirements(tc.Password) + if result != tc.ExpectedResult { + t.Errorf("Expected result %v, got %v", tc.ExpectedResult, result) + } + }) + } +} + +func TestGenerateRandom(t *testing.T) { + config := NewConfig(WithAlgorithm(AlgoRandom), WithNumberPass(1), + WithMinLength(1), WithMaxLength(1)) + config.MinNumeric = 1 + generator := New(config) + pw, err := generator.generateRandom() + if err != nil { + t.Errorf("generateRandom() failed: %s", err) + } + if len(pw) > 1 { + t.Errorf("expected password with length 1 but got: %d", len(pw)) + } + n, err := strconv.Atoi(pw) + if err != nil { + t.Errorf("expected password to be a number but got an error: %s", err) + } + if n < 0 || n > 9 { + t.Errorf("expected password to be a number between 0 and 9, got: %d", n) + } +} + +func TestGenerate(t *testing.T) { + tests := []struct { + name string + algorithm Algorithm + expectedErr error + }{ + { + name: "Pronouncable", + algorithm: AlgoPronouncable, + }, + { + name: "CoinFlip", + algorithm: AlgoCoinFlip, + }, + { + name: "Random", + algorithm: AlgoRandom, + }, + { + name: "Unsupported", + algorithm: AlgoUnsupported, + expectedErr: fmt.Errorf("unsupported algorithm"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := NewConfig(WithAlgorithm(tt.algorithm)) + g := New(config) + _, err := g.Generate() + if tt.expectedErr != nil { + if err == nil || err.Error() != tt.expectedErr.Error() { + t.Errorf("Expected error: %s, got: %s", tt.expectedErr, err) + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + }) + } +} + func BenchmarkGenerator_CoinFlip(b *testing.B) { b.ReportAllocs() g := New(NewConfig()) From fefb2557fc889d4b098818337f4617b766982594 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 18:28:01 +0100 Subject: [PATCH 39/53] Refactor spelling of "Pronounceable" and introduce syllable spelling feature The spelling of "Pronounceable" has been adjusted throughout the code. Moreover, a new functionality for producing pronounceable passwords spelled as correlating syllables has been integrated. This includes relevant changes to password character sets used for pronounceable passwords and enhancements to test this new feature. --- algo.go | 6 +++--- algo_test.go | 2 +- apg.go | 3 +++ cmd/apg/apg.go | 17 ++++++++++++++++- config.go | 3 +++ config_test.go | 2 +- random.go | 17 +++++++++-------- random_test.go | 12 ++++++------ spelling.go | 25 +++++++++++++++++++++++++ 9 files changed, 67 insertions(+), 20 deletions(-) diff --git a/algo.go b/algo.go index 63fb0db..972de82 100644 --- a/algo.go +++ b/algo.go @@ -5,9 +5,9 @@ package apg type Algorithm int const ( - // AlgoPronouncable represents the algorithm for pronouncable passwords + // AlgoPronounceable represents the algorithm for pronounceable passwords // (koremutake syllables) - AlgoPronouncable Algorithm = iota + AlgoPronounceable Algorithm = iota // AlgoRandom represents the algorithm for purely random passwords according // to the provided password modes/flags AlgoRandom @@ -23,7 +23,7 @@ const ( func IntToAlgo(a int) Algorithm { switch a { case 0: - return AlgoPronouncable + return AlgoPronounceable case 1: return AlgoRandom case 2: diff --git a/algo_test.go b/algo_test.go index bab7c4a..5980530 100644 --- a/algo_test.go +++ b/algo_test.go @@ -8,7 +8,7 @@ func TestIntToAlgo(t *testing.T) { a int e Algorithm }{ - {"AlgoPronouncable", 0, AlgoPronouncable}, + {"AlgoPronounceable", 0, AlgoPronounceable}, {"AlgoRandom", 1, AlgoRandom}, {"AlgoCoinflip", 2, AlgoCoinFlip}, {"AlgoUnsupported", 3, AlgoUnsupported}, diff --git a/apg.go b/apg.go index a3d9982..7fd7830 100644 --- a/apg.go +++ b/apg.go @@ -7,6 +7,9 @@ const VERSION = "2.0.0" type Generator struct { // config is a pointer to the apg config instance config *Config + // syllables holds the single syllables of the lasst generated + // pronounceable password + syllables []string } // New returns a new password Generator type diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 16e6996..9ec210b 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -42,6 +42,7 @@ func main() { flag.StringVar(&modeString, "M", "", "") flag.Int64Var(&config.NumberPass, "n", config.NumberPass, "") flag.BoolVar(&config.SpellPassword, "l", false, "") + flag.BoolVar(&config.SpellPronounceable, "t", false, "") flag.Usage = usage flag.Parse() @@ -114,7 +115,7 @@ func main() { _, _ = fmt.Fprintf(os.Stderr, "failed to generate password: %s\n", err) os.Exit(1) } - if config.SpellPassword { + if config.Algorithm == apg.AlgoRandom && config.SpellPassword { spellPass, err := apg.Spell(password) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "failed to spell password: %s\n", err) @@ -122,6 +123,14 @@ func main() { fmt.Printf("%s (%s)\n", password, spellPass) continue } + if config.Algorithm == apg.AlgoPronounceable && config.SpellPronounceable { + pronouncePass, err := generator.Pronounce() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to pronounce password: %s\n", err) + } + fmt.Printf("%s (%s)\n", password, pronouncePass) + continue + } fmt.Println(password) } } @@ -145,6 +154,8 @@ Flags: -m LENGTH Minimum length of the password to be generated (Default: 12) -x LENGTH Maximum length of the password to be generated (Default: 20) -f LENGTH Fixed length of the password to be generated (Ignores -m and -x) + - Note: Due to the way the pronounceable password algorithm works, + this setting might not always apply -n NUMBER Amount of password to be generated (Default: 6) -E CHARS List of characters to be excluded in the generated password -M [LUNSHClunshc] New style password flags @@ -155,6 +166,8 @@ Flags: -mU NUMBER Minimum amount of upper-case characters (implies -U) - Note: any of the "Minimum amount of" modes may result in extraordinarily long calculation times + - Note 2: The "minimum amount of" modes do not apply in + pronounceable mode (-a 0) -C Enable complex password mode (implies -L -U -N -S and disables -H) -H Avoid ambiguous characters in passwords (i. e.: 1, l, I, O, 0) (Default: off) -L Toggle lower-case characters in passwords (Default: on) @@ -163,6 +176,8 @@ Flags: -U Toggle upper-case characters in passwords (Default: on) - Note: this flag has higher priority than the other old-style flags -l Spell generated passwords in phonetic alphabet (Default: off) + -t Spell generated pronounceable passwords with the corresponding + syllables (Default: off) -p Check the HIBP database if the generated passwords was found in a leak before (Default: off) - Note: this feature requires internet connectivity -h Show this help text diff --git a/config.go b/config.go index b5253c9..47c09d7 100644 --- a/config.go +++ b/config.go @@ -43,6 +43,9 @@ type Config struct { NumberPass int64 // SpellPassword if set will spell the generated passwords in the phonetic alphabet SpellPassword bool + // SpellPronounceable if set will spell the generated pronounceable passwords in + // as its corresponding syllables + SpellPronounceable bool } // Option is a function that can override default Config settings diff --git a/config_test.go b/config_test.go index b79b5ed..712d27d 100644 --- a/config_test.go +++ b/config_test.go @@ -35,7 +35,7 @@ func TestWithAlgorithm(t *testing.T) { algo Algorithm want int }{ - {"Pronouncble passwords", AlgoPronouncable, 0}, + {"Pronouncble passwords", AlgoPronounceable, 0}, {"Random passwords", AlgoRandom, 1}, {"Coinflip", AlgoCoinFlip, 2}, {"Unsupported", AlgoUnsupported, 3}, diff --git a/random.go b/random.go index 8e0b665..4fa6df4 100644 --- a/random.go +++ b/random.go @@ -45,8 +45,8 @@ func (g *Generator) CoinFlipBool() bool { // it as string type. If the generation fails, an error will be thrown func (g *Generator) Generate() (string, error) { switch g.config.Algorithm { - case AlgoPronouncable: - return g.generatePronouncable() + case AlgoPronounceable: + return g.generatePronounceable() case AlgoCoinFlip: return g.generateCoinFlip() case AlgoRandom: @@ -284,18 +284,19 @@ func (g *Generator) generateCoinFlip() (string, error) { return "Tails", nil } -// generatePronouncable is executed when Generate() is called with Algorithm set -// to AlgoPronouncable -func (g *Generator) generatePronouncable() (string, error) { +// generatePronounceable is executed when Generate() is called with Algorithm set +// to AlgoPronounceable +func (g *Generator) generatePronounceable() (string, error) { var password string - syllables := make([]string, 0) + g.syllables = make([]string, 0) length, err := g.GetPasswordLength() if err != nil { return "", fmt.Errorf("failed to calculate password length: %w", err) } - characterSet := append(KoremutakeSyllables, strings.Split(CharRangeNumericHuman, "")...) + characterSet := KoremutakeSyllables + characterSet = append(characterSet, strings.Split(CharRangeNumericHuman, "")...) characterSet = append(characterSet, strings.Split(CharRangeSpecialHuman, "")...) characterSetLength := len(characterSet) for int64(len(password)) < length { @@ -316,7 +317,7 @@ func (g *Generator) generatePronouncable() (string, error) { nextSyllable = strings.ReplaceAll(nextSyllable, randomChar, strings.ToUpper(randomChar)) } password += nextSyllable - syllables = append(syllables, nextSyllable) + g.syllables = append(g.syllables, nextSyllable) } return password, nil diff --git a/random_test.go b/random_test.go index 3e99e5a..4e42a24 100644 --- a/random_test.go +++ b/random_test.go @@ -299,14 +299,14 @@ func TestGenerateCoinFlip(t *testing.T) { } } -func TestGeneratePronouncable(t *testing.T) { +func TestGeneratePronounceable(t *testing.T) { config := NewConfig() generator := New(config) foundSylables := 0 for range 100 { - res, err := generator.generatePronouncable() + res, err := generator.generatePronounceable() if err != nil { - t.Errorf("generatePronouncable() failed: %s", err) + t.Errorf("generatePronounceable() failed: %s", err) return } for _, syl := range KoremutakeSyllables { @@ -316,7 +316,7 @@ func TestGeneratePronouncable(t *testing.T) { } } if foundSylables < 100 { - t.Errorf("generatePronouncable() failed, expected at least 1 sylable, got none") + t.Errorf("generatePronounceable() failed, expected at least 1 sylable, got none") } } @@ -440,8 +440,8 @@ func TestGenerate(t *testing.T) { expectedErr error }{ { - name: "Pronouncable", - algorithm: AlgoPronouncable, + name: "Pronounceable", + algorithm: AlgoPronounceable, }, { name: "CoinFlip", diff --git a/spelling.go b/spelling.go index 67b0028..67a835c 100644 --- a/spelling.go +++ b/spelling.go @@ -93,6 +93,31 @@ func Spell(input string) (string, error) { return strings.Join(returnString, "/"), nil } +// Pronounce returns last generated pronounceable password as spelled syllables string +func (g *Generator) Pronounce() (string, error) { + var returnString []string + for _, syllable := range g.syllables { + isKoremutake := false + for _, x := range KoremutakeSyllables { + if x == strings.ToLower(syllable) { + isKoremutake = true + } + } + + if isKoremutake { + returnString = append(returnString, syllable) + continue + } + + curSpellString, err := ConvertByteToWord(syllable[0]) + if err != nil { + return "", err + } + returnString = append(returnString, curSpellString) + } + return strings.Join(returnString, "-"), nil +} + // ConvertByteToWord converts a given ASCII byte into the corresponding spelled version // of the english phonetic alphabet func ConvertByteToWord(charByte byte) (string, error) { From 4b0437d3b1691ff536b874db0e59b96cf6de60ce Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 18:43:51 +0100 Subject: [PATCH 40/53] Add pronunciation tests in spelling_test.go Added "TestPronounce" function in spelling_test.go file to ensure pronunciation mechanism works as expected. The function tests various cases including no syllables, single syllable, multiple syllables, and non-Koremutake syllables. --- spelling_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/spelling_test.go b/spelling_test.go index 82fe73f..a417565 100644 --- a/spelling_test.go +++ b/spelling_test.go @@ -110,3 +110,52 @@ func TestSpell(t *testing.T) { }) } } + +func TestPronounce(t *testing.T) { + tests := []struct { + name string + syllables []string + want string + wantErr bool + }{ + { + name: "Pronounce_NoSyllables", + syllables: []string{}, + want: "", + wantErr: false, + }, + { + name: "Pronounce_SingleSyllable", + syllables: []string{"me"}, + want: "me", + wantErr: false, + }, + { + name: "Pronounce_MultipleSyllables", + syllables: []string{"mu", "sa"}, + want: "mu-sa", + wantErr: false, + }, + { + name: "Pronounce_NonKoremutakeSyllable", + syllables: []string{"ä"}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := NewConfig() + g := New(config) + g.syllables = tt.syllables + got, err := g.Pronounce() + if (err != nil) != tt.wantErr { + t.Errorf("Generator.Pronounce() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Generator.Pronounce() = %v, want %v", got, tt.want) + } + }) + } +} From 87f93ddbc6e525b0c2ec3980e7c922ffc7899823 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 19:00:21 +0100 Subject: [PATCH 41/53] Add HIBP password check functionality Implemented HIBP password check feature which will crosscheck generated passwords with the HIBP pwned passwords database. This functionality enhances security by not recommending potentially compromised passwords. A new flag, CheckHIBP has been added to enable or disable this feature. --- cmd/apg/apg.go | 12 ++++++++++++ config.go | 5 ++++- go.mod | 2 ++ go.sum | 2 ++ hibp.go | 16 ++++++++++++++++ 5 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 go.sum create mode 100644 hibp.go diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 9ec210b..4c817ea 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -43,6 +43,7 @@ func main() { flag.Int64Var(&config.NumberPass, "n", config.NumberPass, "") flag.BoolVar(&config.SpellPassword, "l", false, "") flag.BoolVar(&config.SpellPronounceable, "t", false, "") + flag.BoolVar(&config.CheckHIBP, "p", false, "") flag.Usage = usage flag.Parse() @@ -132,6 +133,17 @@ func main() { continue } fmt.Println(password) + + if config.CheckHIBP { + pwned, err := apg.HasBeenPwned(password) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to check HIBP database: %s\n", err) + } + if pwned { + fmt.Print("^-- !!WARNING: The previously generated password was found in " + + "HIBP database. Do not use it!!\n") + } + } } } diff --git a/config.go b/config.go index 47c09d7..1425a4c 100644 --- a/config.go +++ b/config.go @@ -15,8 +15,11 @@ const ( // Config represents the apg.Generator config parameters type Config struct { - // Algo + // Algorithm sets the Algorithm used for the password generation Algorithm Algorithm + // CheckHIBP sets a flag if the generated password has to be checked + // against the HIBP pwned password database + CheckHIBP bool // FixedLength sets a fixed length for generated passwords and ignores // the MinLength and MaxLength values FixedLength int64 diff --git a/go.mod b/go.mod index 955e55e..b64b6fe 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module src.neessen.cloud/wneessen/apg-go go 1.22 + +require github.com/wneessen/go-hibp v1.0.6 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6379982 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/wneessen/go-hibp v1.0.6 h1:RpV540tVJpxefjCCctzq1cQaIFFlkd3nh+HhPVD6Hys= +github.com/wneessen/go-hibp v1.0.6/go.mod h1:Ldg6DQg4fMCveVKgL+RL9Jy+9TsljjAP704Ix8X3jOw= diff --git a/hibp.go b/hibp.go new file mode 100644 index 0000000..db20189 --- /dev/null +++ b/hibp.go @@ -0,0 +1,16 @@ +package apg + +import ( + "time" + + "github.com/wneessen/go-hibp" +) + +// HasBeenPwned checks the given password string against the HIBP pwned +// passwords database and returns true if the password has been leaked +func HasBeenPwned(password string) (bool, error) { + hc := hibp.New(hibp.WithHTTPTimeout(time.Second*2), + hibp.WithPwnedPadding()) + matches, _, err := hc.PwnedPassAPI.CheckPassword(password) + return matches != nil && matches.Count != 0, err +} From c5caab0066e9ffa28d4d25d0064460ce9bcb740d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 19:09:51 +0100 Subject: [PATCH 42/53] Add tests for HasBeenPwned function Introduced unit tests for the HasBeenPwned function in the hibp_test.go file. These tests consist of scenarios with both secured and compromised passwords, improving the function's reliability and error handling capabilities. --- hibp_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 hibp_test.go diff --git a/hibp_test.go b/hibp_test.go new file mode 100644 index 0000000..f890c1d --- /dev/null +++ b/hibp_test.go @@ -0,0 +1,27 @@ +package apg + +import ( + "testing" +) + +func TestHasBeenPwned(t *testing.T) { + tests := []struct { + name string + password string + want bool + }{ + {"Pwned PW", "Test123", true}, + {"Secure PW", "Cta8mWYmW7O*j1V!YMTS", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := HasBeenPwned(tt.password) + if err != nil { + t.Errorf("HasBeenPwned() failed: %s", err) + } + if tt.want != got { + t.Errorf("HasBeenPwned() failed, wanted: %t, got: %t", tt.want, got) + } + }) + } +} From 1eb4cf37e2b64748fac57d9fcbbd8f1c01c1a550 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 20:12:32 +0100 Subject: [PATCH 43/53] Added version display feature in apg.go An option flag has been added to apg.go, which allows users to check the version of the script. When activated, the script displays the version and exits. It also provides OS and architecture information, outputted to stderr for user convenience. --- cmd/apg/apg.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 4c817ea..d3382df 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "os" + "runtime" "src.neessen.cloud/wneessen/apg-go" ) @@ -24,7 +25,7 @@ func main() { // See usage() for flag details var algorithm int var modeString string - var complexPass, humanReadable, lowerCase, numeric, special, upperCase bool + var complexPass, humanReadable, lowerCase, numeric, special, showVer, upperCase bool flag.IntVar(&algorithm, "a", 1, "") flag.BoolVar(&lowerCase, "L", false, "") flag.Int64Var(&config.MinLowerCase, "mL", config.MinLowerCase, "") @@ -44,9 +45,20 @@ func main() { flag.BoolVar(&config.SpellPassword, "l", false, "") flag.BoolVar(&config.SpellPronounceable, "t", false, "") flag.BoolVar(&config.CheckHIBP, "p", false, "") + flag.BoolVar(&showVer, "v", false, "") flag.Usage = usage flag.Parse() + // Show version and exit + if showVer { + _, _ = os.Stderr.WriteString(`apg-go // A "Automated Password Generator"-clone ` + + `v` + apg.VERSION + "\n") + _, _ = os.Stderr.WriteString("OS: " + runtime.GOOS + " // Arch: " + + runtime.GOARCH + " \n") + _, _ = os.Stderr.WriteString("(C) 2021-2024 by Winni Neessen\n") + os.Exit(0) + } + // Old style character modes if humanReadable { config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeHumanReadable) From 2022e0953d291da52257a76e25f608c06e44859a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 20:13:41 +0100 Subject: [PATCH 44/53] Added goreleaser --- .gitignore | 2 ++ .goreleaser.yaml | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .goreleaser.yaml diff --git a/.gitignore b/.gitignore index 5f1a16a..3984ba9 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ examples/* # IDEA specific ignores .idea/ + +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..155ab6b --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,66 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 1 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + - freebsd + - openbsd + - netbsd + main: ./cmd/apg + binary: apg + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +nfpms: + - vendor: Winni Neessen + maintainer: Winni Neessen + formats: + - apk + - deb + - rpm + - termux.deb + - archlinux + +dmg: + - replace: true + +universal_binaries: From 2143ca99f58dbcf7cb40f748211ddf9f0c1b5d44 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 20:31:27 +0100 Subject: [PATCH 45/53] Added option to exclude certain characters in password generation The code now supports the configuration parameter 'ExcludeChars', which gives users the ability to specify particular characters that should not be included in the generated passwords. This addition was fully implemented both on config.go and random.go, while the relevant option flag was also added to apg.go for user interaction. --- cmd/apg/apg.go | 1 + config.go | 3 +++ random.go | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index d3382df..fca3457 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -46,6 +46,7 @@ func main() { flag.BoolVar(&config.SpellPronounceable, "t", false, "") flag.BoolVar(&config.CheckHIBP, "p", false, "") flag.BoolVar(&showVer, "v", false, "") + flag.StringVar(&config.ExcludeChars, "E", "", "") flag.Usage = usage flag.Parse() diff --git a/config.go b/config.go index 1425a4c..3576aa9 100644 --- a/config.go +++ b/config.go @@ -20,6 +20,9 @@ type Config struct { // CheckHIBP sets a flag if the generated password has to be checked // against the HIBP pwned password database CheckHIBP bool + // ExcludeChars is a list of characters that should be excluded from + // generated passwords + ExcludeChars string // FixedLength sets a fixed length for generated passwords and ignores // the MinLength and MaxLength values FixedLength int64 diff --git a/random.go b/random.go index 4fa6df4..07036db 100644 --- a/random.go +++ b/random.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "math/big" + "os" + "regexp" "strings" ) @@ -93,6 +95,16 @@ func (g *Generator) GetCharRangeFromConfig() string { charRange.WriteString(CharRangeAlphaUpper) } } + if g.config.ExcludeChars != "" { + rex, err := regexp.Compile("[" + regexp.QuoteMeta(g.config.ExcludeChars) + "]") + if err == nil { + newRange := rex.ReplaceAllLiteralString(charRange.String(), "") + charRange.Reset() + charRange.WriteString(newRange) + } else { + _, _ = fmt.Fprintf(os.Stderr, "failed to exclude characters: %s\n", err) + } + } return charRange.String() } From d521defa38d1c5f848baac9b135920d7e5041099 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 20:41:24 +0100 Subject: [PATCH 46/53] Rearrange flag assignments for readability in apg.go The flag assignments in the apg.go file have been rearranged and organized in alphabetical order by their first letter for better readability and easy management. The logic remains unaffected, but the changes should improve the overall code aesthetic and maintainability. --- cmd/apg/apg.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index fca3457..8b9b53e 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -27,26 +27,26 @@ func main() { var modeString string var complexPass, humanReadable, lowerCase, numeric, special, showVer, upperCase bool flag.IntVar(&algorithm, "a", 1, "") - flag.BoolVar(&lowerCase, "L", false, "") - flag.Int64Var(&config.MinLowerCase, "mL", config.MinLowerCase, "") - flag.BoolVar(&upperCase, "U", false, "") - flag.Int64Var(&config.MinUpperCase, "mU", config.MinUpperCase, "") - flag.BoolVar(&numeric, "N", false, "") - flag.Int64Var(&config.MinNumeric, "mN", config.MinNumeric, "") - flag.BoolVar(&special, "S", false, "") - flag.Int64Var(&config.MinSpecial, "mS", config.MinSpecial, "") flag.BoolVar(&complexPass, "C", false, "") - flag.BoolVar(&humanReadable, "H", false, "") - flag.Int64Var(&config.FixedLength, "f", 0, "") - flag.Int64Var(&config.MinLength, "m", config.MinLength, "") - flag.Int64Var(&config.MaxLength, "x", config.MaxLength, "") - flag.StringVar(&modeString, "M", "", "") - flag.Int64Var(&config.NumberPass, "n", config.NumberPass, "") - flag.BoolVar(&config.SpellPassword, "l", false, "") - flag.BoolVar(&config.SpellPronounceable, "t", false, "") - flag.BoolVar(&config.CheckHIBP, "p", false, "") - flag.BoolVar(&showVer, "v", false, "") flag.StringVar(&config.ExcludeChars, "E", "", "") + flag.Int64Var(&config.FixedLength, "f", 0, "") + flag.BoolVar(&humanReadable, "H", false, "") + flag.BoolVar(&config.SpellPassword, "l", false, "") + flag.BoolVar(&lowerCase, "L", false, "") + flag.Int64Var(&config.MinLength, "m", config.MinLength, "") + flag.Int64Var(&config.MinLowerCase, "mL", config.MinLowerCase, "") + flag.Int64Var(&config.MinNumeric, "mN", config.MinNumeric, "") + flag.Int64Var(&config.MinSpecial, "mS", config.MinSpecial, "") + flag.Int64Var(&config.MinUpperCase, "mU", config.MinUpperCase, "") + flag.Int64Var(&config.NumberPass, "n", config.NumberPass, "") + flag.StringVar(&modeString, "M", "", "") + flag.BoolVar(&numeric, "N", false, "") + flag.BoolVar(&config.CheckHIBP, "p", false, "") + flag.BoolVar(&special, "S", false, "") + flag.BoolVar(&config.SpellPronounceable, "t", false, "") + flag.BoolVar(&upperCase, "U", false, "") + flag.BoolVar(&showVer, "v", false, "") + flag.Int64Var(&config.MaxLength, "x", config.MaxLength, "") flag.Usage = usage flag.Parse() From f2a57cf46fe8182ff0be5e4f3ee69e9990b36b6b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 20:43:48 +0100 Subject: [PATCH 47/53] Update flag order and add `-mX` and `-p` flags in apg.go The flag assignments in apg.go have been updated to be in alphabetic order and two new flags, `-mX` and `-p`, have been added. This change improves readability and makes maintaining the code easier, while not affecting the algorithm's functionality. --- cmd/apg/apg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 8b9b53e..8d780f9 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -169,7 +169,7 @@ func usage() { Created 2021-2024 by Winni Neessen (MIT licensed) apg [-a ] [-m ] [-x ] [-L] [-U] [-N] [-S] [-H] [-C] - [-l] [-M mode] [-E char_string] [-n num_of_pass] [-v] [-h] [-t] + [-l] [-M mode] [-E char_string] [-n num_of_pass] [-mX number] [-t] [-p] [-v] [-h] Flags: -a ALGORITH Choose the password generation algorithm (Default: 1) From 79f921f9add2f83a1b7828116fc07a64e93df351 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 20:59:07 +0100 Subject: [PATCH 48/53] Add .gitgnore and SPDX headers in several files This commit introduces the .gitignore configuration file and adds SPDX headers to several files including test and documentation files. The headers provide license information in a standardized format which can be easily picked up by automated tools for license compliance checks. Additionally, it deleted a .idea/.gitignore file, which is a project specific IDE configuration file not necessary for the repository. It also introduced a README.md file providing more insightful information about the project. --- .github/FUNDING.yml | 4 + .github/ISSUE_TEMPLATE/bug_report.md | 6 + .github/ISSUE_TEMPLATE/feature_request.md | 6 + .github/dependabot.yml | 4 + .github/workflows/codeql-analysis.yml | 4 + .github/workflows/docker-publish.yml | 4 + .github/workflows/release-bsd.yml | 4 + .github/workflows/release-darwin.yml | 4 + .github/workflows/release-linux.yml | 4 + .github/workflows/release-windows.yml | 4 + .github/workflows/sonarqube.yml | 4 + .gitignore | 3 +- .golangci.toml | 6 +- .goreleaser.yaml | 10 +- .idea/.gitignore | 8 - README.md | 278 ++++++++++++++++++++++ algo.go | 4 + algo_test.go | 4 + apg.go | 4 + cmd/apg/apg.go | 4 + config.go | 4 + config_test.go | 4 + go.mod | 4 + go.sum.license | 3 + hibp.go | 4 + hibp_test.go | 4 + koremutake.go | 4 + mode.go | 4 + mode_test.go | 4 + random.go | 4 + random_test.go | 4 + sonar-project.properties | 4 + spelling.go | 4 + spelling_test.go | 4 + 34 files changed, 405 insertions(+), 19 deletions(-) delete mode 100644 .idea/.gitignore create mode 100644 README.md create mode 100644 go.sum.license diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2db91cf..8604f99 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,6 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + github: wneessen ko_fi: winni diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..e68b1b6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,3 +1,9 @@ + + --- name: Bug report about: Create a report to help us improve diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7..ce546ee 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,3 +1,9 @@ + + --- name: Feature request about: Suggest an idea for this project diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eb4bfe6..c868b81 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d7b775a..6d7a4dc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 9db74d7..5816721 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + name: Docker # This workflow uses actions that are not certified by GitHub. diff --git a/.github/workflows/release-bsd.yml b/.github/workflows/release-bsd.yml index 6124a7b..703e2c9 100644 --- a/.github/workflows/release-bsd.yml +++ b/.github/workflows/release-bsd.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + name: Go on: diff --git a/.github/workflows/release-darwin.yml b/.github/workflows/release-darwin.yml index 22cbf7e..d4c793d 100644 --- a/.github/workflows/release-darwin.yml +++ b/.github/workflows/release-darwin.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + name: Go on: diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 51cc93a..4bfa457 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + name: Go on: diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index d1c39d0..ee41e2b 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + name: Go on: diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index f99f61e..35b2b9d 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + name: SonarQube on: push: diff --git a/.gitignore b/.gitignore index 3984ba9..dae9425 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 Winni Neessen +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen # # SPDX-License-Identifier: CC0-1.0 @@ -27,5 +27,6 @@ examples/* # IDEA specific ignores .idea/ +.idea/.gitignore dist/ diff --git a/.golangci.toml b/.golangci.toml index afd9684..205c9e3 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -1,6 +1,6 @@ -## SPDX-FileCopyrightText: 2022 Winni Neessen -## -## SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 [run] go = "1.20" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 155ab6b..f8c69b0 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,10 +1,6 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com - -# The lines below are called `modelines`. See `:help modeline` -# Feel free to remove those if you don't want/need to use them. -# yaml-language-server: $schema=https://goreleaser.com/static/schema.json -# vim: set ts=2 sw=2 tw=0 fo=cnqoj +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 version: 1 diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1b3c06 --- /dev/null +++ b/README.md @@ -0,0 +1,278 @@ + + +# A "Automated Password Generator"-clone +[![Go Reference](https://pkg.go.dev/badge/github.com/wneessen/apg-go.svg)](https://pkg.go.dev/github.com/wneessen/apg-go) [![Go Report Card](https://goreportcard.com/badge/github.com/wneessen/apg-go)](https://goreportcard.com/report/github.com/wneessen/apg-go) [![Build Status](https://api.cirrus-ci.com/github/wneessen/apg-go.svg)](https://cirrus-ci.com/github/wneessen/apg-go) ![CodeQL workflow](https://github.com/wneessen/apg-go/actions/workflows/codeql-analysis.yml/badge.svg) buy ma a coffee + +_apg-go_ is a simple APG-like password generator written in Go. It tries to replicate the +functionality of the +"[Automated Password Generator](https://web.archive.org/web/20130313042424/http://www.adel.nursat.kz:80/apg)", +which hasn't been maintained since 2003. Since more and more Unix distributions are abondoning the tool, I was +looking for an alternative. FreeBSD for example recommends "security/makepasswd", which is written in Perl +but requires a lot of dependency packages and doesn't offer the feature-set/flexibility of APG. + +Since FIPS-181 (pronouncable passwords) has been withdrawn in 2015, apg-go does not follow this standard. Instead +it implements the [Koremutake Syllables System](https://shorl.com/koremutake.php) in its pronouncable password mode. + +## Installation + +### Docker +There is a ready-to-use Docker image hosted on Github. + +* Download the image: + ```shell + $ docker pull ghcr.io/wneessen/apg-go:main + ``` +* Run the image: + ```shell + $ docker run ghcr.io/wneessen/apg-go:main + ``` + +### Ports/Packages +#### FreeBSD +apg-go can be found as `/security/apg` in the [FreeBSD ports](https://cgit.freebsd.org/ports/tree/security/apg) +tree. +#### Arch Linux +Find apg-go in [Arch Linux AUR](https://aur.archlinux.org/packages/apg-go/). \ +Alternatively use the [PKGBUILD](https://github.com/wneessen/apg-go/tree/main/buildfiles/arch-linux) file +in this git repository +### Binary releases +#### Linux/BSD/MacOS +* Download release + ```sh + $ curl -LO https://github.com/wneessen/apg-go/releases/download/v/apg-v--.tar.gz + $ curl -LO https://github.com/wneessen/apg-go/releases/download/v/apg-v--.tar.gz.sha256 + ``` +* Verify the checksum + ```sh + $ sha256 apg-v--.tar.gz + $ cat apg-v--.tar.gz.sha256 + ``` + **Make sure the checksum of the downloaded file and the checksum in the .sha256 match** +* Extract archive + ```sh + $ tar xzf apg-v--.tar.gz + ``` +* Execute + ```sh + $ ./apg + ``` +#### Windows +* Download release + ```PowerShell + PS> Invoke-RestMethod -Uri https://github.com/wneessen/apg-go/releases/download/v/apg-v-windows-.zip -OutFile apg-v-windows-.zip + PS> Invoke-RestMethod -Uri https://github.com/wneessen/apg-go/releases/download/v/apg-v-windows-.zip.sha256 -OutFile apg-v-windows-.zip.sha256 + ``` +* Verify the checksum + ```PowerShell + PS> Get-FileHash apg-v-windows-.zip | Format-List + PS> type apg-v-windows-.zip.sha256 + ``` + **Make sure the checksum of the downloaded file and the checksum in the .sha256 match** +* Extract archive + ```PowerShell + PS> Expand-Archive -LiteralPath apg-v-windows- + ``` +* Execute + ```PowerShell + PS> cd apg-v-windows- + PS> apg.exe + ``` + +### Sources +* Download sources + ```sh + $ curl -LO https://github.com/wneessen/apg-go/archive/refs/tags/v.tar.gz + ``` +* Extract source + ```sh + $ tar xzf v.tar.gz + ``` +* Build binary + ```sh + $ cd apg-go- + $ go build -o apg ./... + ``` +* Execute the brand new binary + ```sh + $ ./apg + ``` + +### Systemwide installation +It is recommed to install apg in a directory of your ```$PATH``` environment. To do so run: +(In this example we use ```/usr/local/bin``` as system-wide binary path. YMMV) +```sh +$ sudo cp apg /usr/local/bin/apg +``` + +## Programmatic interface +Since v0.4.0 the CLI and the main package functionality have been separated from each other, which makes +it easier to use the `apg-go` package in other Go code as well. This way you can make of the password +generation in your own code without having to rely on the actual apg-go binary. + +Code examples on how to use the package can be found in the [example-code](example-code) directory. + +## Usage examples +### Default behaviour +By default apg-go will generate 6 passwords, with a minimum length of 12 characters and a +maxiumum length of 20 characters. The generated password will use a character set constructed +from lower case, upper case and numeric characters. +```shell +$ ./apg-go +R8rCC8bw5NvJmTUK2g +cHB9qogTbfdzFgnH +hoHfpWAHHSNa4Q +QyjscIsZkQGh +904YqsU5SnoqLo2w +utdFKXdeiXFzM +``` +### Modifying the character sets +#### Old style +Let's assume you want to generate a single password, constructed out of upper case, numeric +and special characters. Since lower case is part of the default set, you would need to disable them +by setting the `-L` parameter. In addition you would set the `-S` parameter to enable special +characters. Finally the parameter `-n 1` is needed to keep apg-go from generating more than one +password: +```shell +$ ./apg-go -n 1 -L -S +XY7>}H@5U40&_A1*9I$ +``` + +#### New/modern style +Since the old style switches can be kind of confusing, it is recommended to use the "new style" +parameters instead. The new style is all combined in the `-M` parameter. Using the upper case +version of a parameter argument enables a feature, while the lower case version disabled it. The +previous example could be represented like this in new style: +```shell +$ ./apg-go -n 1 -M lUSN +$B|~sudhtyDBu +``` + +### Password spelling +If you need to read out a password, it can be helpful to know the corresponding word for that character in +the phonetic alphabet. By setting the `-l` parameter, agp-go will provide you with the phonetic spelling +(english language) of your newly created password: +```shell +$ ./apg-go -n 1 -M LUSN -H -E : -l +fUTDKeFsU+zn3r= (foxtrot/Uniform/Tango/Delta/Kilo/echo/Foxtrot/sierra/Uniform/PLUS_SIGN/zulu/november/THREE/romeo/EQUAL_SIGN) +``` + +### Pronouncable passwords +Since v0.4.0 apg-go supports pronouncable passwords, anologous to the original c-apg using the `-a 0` +flag. The original c-apg implemented FIPS-181, which was withdrawn in 2015 for generating pronouncable +passwords. Since the standard is not recommended anymore, `apg-go` instead make use of the +[Koremutake Syllables System](https://shorl.com/koremutake.php). Similar to the original apg, `agp-go` +will automatically randomly add special characters and number (from the human-readable pool) to each +generated pronouncable password. Additionally it will perform a "coinflip" for each Koremutake syllable +and decided if it should switch the case of one of the characters to an upper-case character. + +Using the `-t` parameter, `apg-go` will display a spelled out version of the pronouncable password, where +each syllable or number/special character is seperated with a "-" (dash) and if the syllable is not a +Koremutake syllable the character will be spelled out the same was as with activated `-l` in the +non-pronouncable password mode (`-a 1`). + +**Note on password length**: The `-m` and `-x` parameters will work in prouncable password mode, but +please keep in mind, that due to the nature how syllables work, your generated password might exceed +the desired length by one complete syllable (which can be up to 3 characters long). + +**Security consideration:** Please keep in mind, that pronouncable passwords are less secure then truly +randomly created passwords, due to the nature how syllables work. As a rule of thumb, it is recommended +to multiply the length of your generated pronouncable passwords by at least 1.5 times, compared to truly +randomly generated passwords. It might also be helpful to run the pronoucable password mode with enabled +"[HIBP](#have-i-been-pwned)" flag, so that each generated password is automatically checked against "Have I Been Pwned" +database. +```shell +$ ./apg-go -a 0 -n 1 +KebrutinernMy + +$ ./apg-go -a 0 -n 1 -m 15 -x 15 -t +pEnbocydrageT*En (pEn-bo-cy-dra-geT-ASTERISK-En) +``` + +### Have I Been Pwned +Even though, the passwords that apg-go generated for you, are secure, there is a minimal chance, that +someone on the planet used exactly the same password before and that this person was part of an +internet leak or hack, which exposed the password to the public. Such passwords are not considered +secure anymore as they usually land on public available password lists, that are used by crackers. + +To be on the safe side, you can use the `-p` parameter, to enable a HIBP check. When the feature is +enabled, apg-go will check the HIBP database at https://haveibeenpwned.com if that password has been +leaked before and provide you with a warning if that is the case. + +Please be aware, that this is a live check against the HIBP API, which not only requires internet +connectivity, but also might take between 500ms to 1s to complete. When you generating a bigger list +of password `-n 100`, the process could take much longer than without the `-p` feature enabled. + +## CLI parameters +_apg-go_ replicates most of the parameters of the original c-apg. Some parameters are different though: + +- `-a `: Choose password generation algorithm (Default: 1) + - `0`: Pronouncable password generation (Koremutake syllables) + - `1`: Random password generation according to password modes/flags +- `-m `: The minimum length of the password to be generated (Default: 12) +- `-x `: The maximum length of the password to be generated (Default: 20) +- `-n `: The amount of passwords to be generated (Default: 6) +- `-E `: Do not use the specified characters in generated passwords +- `-M <[LUNSHClunshc]>`: New style password parameters (upper-case enables, lower-case disables) +- `-L`: Use lower-case characters in passwords (Default: on) +- `-U`: Use upper-case characters in passwords (Default: on) +- `-N`: Use numeric characters in passwords (Default: on) +- `-S`: Use special characters in passwords (Default: off) +- `-H`: Avoid ambiguous characters in passwords (i. e.: 1, l, I, o, O, 0) (Default: off) +- `-C`: Generate complex passwords (implies -L -U -N -S and disables -H) (Default: off) +- `-l`: Spell generated passwords in random password mode (Default: off) +- `-t`: Spell generated passwords in pronouncable password mode (Default: off) +- `-p`: Check the HIBP database if the generated passwords was found in a leak before (Default: off) // *this feature requires internet connectivity* +- `-h`: Show a CLI help text +- `-v`: Show the version number + +## Contributors +Thanks to the following people for contributing to the apg-go codebase: +* [Romain Tartière](https://github.com/smortex) +* [Abraham Ingersoll](https://github.com/aberoham) +* [Vinícius Zavam](https://github.com/egypcio) (Maintaining the FreeBSD port) diff --git a/algo.go b/algo.go index 972de82..74d2937 100644 --- a/algo.go +++ b/algo.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg // Algorithm is a type wrapper for an int type to represent different diff --git a/algo_test.go b/algo_test.go index 5980530..3aa8ca5 100644 --- a/algo_test.go +++ b/algo_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import "testing" diff --git a/apg.go b/apg.go index 7fd7830..1436e0f 100644 --- a/apg.go +++ b/apg.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg // VERSION represents the version string diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 8d780f9..827ffcf 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + // Package main is the APG command line client that makes use of the apg-go library package main diff --git a/config.go b/config.go index 3576aa9..06c6975 100644 --- a/config.go +++ b/config.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg // List of default values for Config instances diff --git a/config_test.go b/config_test.go index 712d27d..0e3dcee 100644 --- a/config_test.go +++ b/config_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import ( diff --git a/go.mod b/go.mod index b64b6fe..8a23530 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + module src.neessen.cloud/wneessen/apg-go go 1.22 diff --git a/go.sum.license b/go.sum.license new file mode 100644 index 0000000..7f6c151 --- /dev/null +++ b/go.sum.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Winni Neessen + +SPDX-License-Identifier: MIT diff --git a/hibp.go b/hibp.go index db20189..39e8d5d 100644 --- a/hibp.go +++ b/hibp.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import ( diff --git a/hibp_test.go b/hibp_test.go index f890c1d..16c6e3a 100644 --- a/hibp_test.go +++ b/hibp_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import ( diff --git a/koremutake.go b/koremutake.go index 6af5ed8..0eea139 100644 --- a/koremutake.go +++ b/koremutake.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg // KoremutakeSyllables is a slightly modified Koremutake syllables list based on diff --git a/mode.go b/mode.go index d268464..f64b693 100644 --- a/mode.go +++ b/mode.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import ( diff --git a/mode_test.go b/mode_test.go index 9ec2867..befe8be 100644 --- a/mode_test.go +++ b/mode_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import ( diff --git a/random.go b/random.go index 07036db..9949fdd 100644 --- a/random.go +++ b/random.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import ( diff --git a/random_test.go b/random_test.go index 4e42a24..e013497 100644 --- a/random_test.go +++ b/random_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import ( diff --git a/sonar-project.properties b/sonar-project.properties index d5ec76f..c698fd7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1 +1,5 @@ +# SPDX-FileCopyrightText: 2021-2024 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + sonar.projectKey=apg-go \ No newline at end of file diff --git a/spelling.go b/spelling.go index 67a835c..c66ca39 100644 --- a/spelling.go +++ b/spelling.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import ( diff --git a/spelling_test.go b/spelling_test.go index a417565..e42b3ce 100644 --- a/spelling_test.go +++ b/spelling_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021-2024 Winni Neessen +// +// SPDX-License-Identifier: MIT + package apg import ( From f26ca059af2ad173c857a841ff4fe0ac8b7061f4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 20:59:37 +0100 Subject: [PATCH 49/53] Add MIT and CC0-1.0 license files This commit adds two new license files, MIT.txt and CC0-1.0.txt under the LICENSES directory. These licenses provide a clear understanding of the permissions and restrictions applied to the software which can be important for both developers and users. --- LICENSES/CC0-1.0.txt | 121 +++++++++++++++++++++++++++++++++++++++++++ LICENSES/MIT.txt | 9 ++++ 2 files changed, 130 insertions(+) create mode 100644 LICENSES/CC0-1.0.txt create mode 100644 LICENSES/MIT.txt diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..2071b23 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +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. From 8fef6356649da69574eb15d298b0a1df22db678f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 21:02:25 +0100 Subject: [PATCH 50/53] Update badges links in README.md This commit updates the existing badge references on the README.md file. The previous badge links were pointing to GitHub locations, these are being changed to point to 'src.neessen.cloud', potentially reflecting a move away from GitHub. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a1b3c06..da4234c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ SPDX-License-Identifier: CC0-1.0 --> # A "Automated Password Generator"-clone -[![Go Reference](https://pkg.go.dev/badge/github.com/wneessen/apg-go.svg)](https://pkg.go.dev/github.com/wneessen/apg-go) [![Go Report Card](https://goreportcard.com/badge/github.com/wneessen/apg-go)](https://goreportcard.com/report/github.com/wneessen/apg-go) [![Build Status](https://api.cirrus-ci.com/github/wneessen/apg-go.svg)](https://cirrus-ci.com/github/wneessen/apg-go) ![CodeQL workflow](https://github.com/wneessen/apg-go/actions/workflows/codeql-analysis.yml/badge.svg) buy ma a coffee +[![Go Reference](https://pkg.go.dev/badge/src.neessen.cloud/wneessen/apg-go.svg)](https://pkg.go.dev/src.neessen.cloud/wneessen/apg-go) +[![Go Report Card](https://goreportcard.com/badge/src.neessen.cloud/wneessen/apg-go)](https://goreportcard.com/report/src.neessen.cloud/wneessen/apg-go) +buy ma a coffee _apg-go_ is a simple APG-like password generator written in Go. It tries to replicate the functionality of the From 239501e83b00a7463c573577303456fd6ac8f4cd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 21:23:15 +0100 Subject: [PATCH 51/53] Refactor error formatting and update Go version in linter config Reformatted error messages to use %w for improved error handling in "random.go", and updated the used Go version in the ".golangci.toml" file. Also, removed some unnecessary lines in "random_test.go" and improved the layout of an array in "koremutake.go". --- .golangci.toml | 5 ++--- koremutake.go | 6 ++++-- random.go | 4 ++-- random_test.go | 1 - 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.golangci.toml b/.golangci.toml index 205c9e3..548f0b2 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -3,14 +3,13 @@ # SPDX-License-Identifier: CC0-1.0 [run] -go = "1.20" +go = "1.22" tests = true -skip-dirs = ["ui/"] [linters] enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder", "errname", "errorlint", "gofmt", "gofumpt", "goimports"] [linters-settings.goimports] -local-prefixes = "git.cgn.cleverbridge.com/infosec/vulnmon" +local-prefixes = "src.neessen.cloud/wneessen/apg-go" diff --git a/koremutake.go b/koremutake.go index 0eea139..cefcf78 100644 --- a/koremutake.go +++ b/koremutake.go @@ -6,7 +6,8 @@ package apg // KoremutakeSyllables is a slightly modified Koremutake syllables list based on // the mechanism described on https://shorl.com/koremutake.php -var KoremutakeSyllables = []string{"ba", "be", "bi", "bo", "bu", "by", "da", "de", "di", +var KoremutakeSyllables = []string{ + "ba", "be", "bi", "bo", "bu", "by", "da", "de", "di", "do", "du", "dy", "fe", "fi", "fo", "fu", "fy", "ga", "ge", "gi", "go", "gu", "gy", "ha", "he", "hi", "ho", "hu", "hy", "ja", "je", "ji", "jo", "ju", "jy", "ka", "ke", "ko", "ku", "ky", "la", "le", "li", "lo", "lu", "ly", "ma", @@ -23,4 +24,5 @@ var KoremutakeSyllables = []string{"ba", "be", "bi", "bo", "bu", "by", "da", "de "col", "ful", "get", "low", "son", "tle", "day", "pen", "pre", "ten", "tor", "ver", "ber", "can", "ple", "fer", "gen", "den", "mag", "sub", "sur", "men", "min", "out", "tal", "but", "cit", "cle", "cov", "dif", "ern", - "eve", "hap", "ket", "nal", "sup", "ted", "tem", "tin", "tro", "tro"} + "eve", "hap", "ket", "nal", "sup", "ted", "tem", "tin", "tro", "tro", +} diff --git a/random.go b/random.go index 9949fdd..776fc5d 100644 --- a/random.go +++ b/random.go @@ -318,7 +318,7 @@ func (g *Generator) generatePronounceable() (string, error) { for int64(len(password)) < length { randNum, err := g.RandNum(int64(characterSetLength)) if err != nil { - return "", fmt.Errorf("failed to generate a random number for Koremutake syllable generation: %s", + return "", fmt.Errorf("failed to generate a random number for Koremutake syllable generation: %w", err) } nextSyllable := characterSet[randNum] @@ -326,7 +326,7 @@ func (g *Generator) generatePronounceable() (string, error) { syllableLength := len(nextSyllable) characterPosition, err := g.RandNum(int64(syllableLength)) if err != nil { - return "", fmt.Errorf("failed to generate a random number for Koremutake syllable generation: %s", + return "", fmt.Errorf("failed to generate a random number for Koremutake syllable generation: %w", err) } randomChar := string(nextSyllable[characterPosition]) diff --git a/random_test.go b/random_test.go index e013497..e537123 100644 --- a/random_test.go +++ b/random_test.go @@ -322,7 +322,6 @@ func TestGeneratePronounceable(t *testing.T) { if foundSylables < 100 { t.Errorf("generatePronounceable() failed, expected at least 1 sylable, got none") } - } func TestCheckMinimumRequirements(t *testing.T) { From 96fa3c53fb470b02d53cf2a8ec98f0360b115f5f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 21:31:26 +0100 Subject: [PATCH 52/53] Add coverage report path to sonar properties Updated the sonar-project.properties file to include a path for the Go coverage report. This path will allow Sonar to access the coverage report created by Go tests, improving overall project testing and coverage monitoring. --- sonar-project.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index c698fd7..d480a5d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,4 +2,5 @@ # # SPDX-License-Identifier: CC0-1.0 -sonar.projectKey=apg-go \ No newline at end of file +sonar.projectKey=apg-go +sonar.go.coverage.reportPaths=cov.out \ No newline at end of file From ce3f8effa271f13a473a2fefd1bad4d8cddc360d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 12 Mar 2024 21:35:58 +0100 Subject: [PATCH 53/53] Remove codecov workflow for Forgejo --- .forgejo/workflows/codecov.yml | 41 ---------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 .forgejo/workflows/codecov.yml diff --git a/.forgejo/workflows/codecov.yml b/.forgejo/workflows/codecov.yml deleted file mode 100644 index bfbb3cb..0000000 --- a/.forgejo/workflows/codecov.yml +++ /dev/null @@ -1,41 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Winni Neessen -# -# SPDX-License-Identifier: CC0-1.0 - -name: Codecov workflow -on: - push: - branches: - - main - paths: - - '**.go' - - 'go.*' - - '.forgejo/**' - - 'codecov.yml' - pull_request: - branches: - - main - paths: - - '**.go' - - 'go.*' - - '.forgejo/**' - - 'codecov.yml' -env: - API_KEY: ${{ secrets.API_KEY }} -jobs: - run: - runs-on: docker - steps: - - name: Checkout Code - uses: actions/checkout@master - - name: Setup go - uses: actions/setup-go@v4 - with: - go-version: '1.22' - - name: Run Tests - run: | - go test -v -shuffle=on -race --coverprofile=coverage.coverprofile --covermode=atomic ./... - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos