mirror of
https://github.com/wneessen/apg-go.git
synced 2024-11-25 07:10:50 +01:00
Merge pull request 'v2 refactor' (#54) from v2 into main
Reviewed-on: #54
This commit is contained in:
commit
b9b93905b2
59 changed files with 2258 additions and 1117 deletions
23
.cirrus.yml
23
.cirrus.yml
|
@ -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
|
49
.forgejo/workflows/golangci-lint.yml
Normal file
49
.forgejo/workflows/golangci-lint.yml
Normal file
|
@ -0,0 +1,49 @@
|
|||
# SPDX-FileCopyrightText: 2022 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# 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
|
15
.forgejo/workflows/reuse.yml
Normal file
15
.forgejo/workflows/reuse.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# 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
|
45
.forgejo/workflows/sonarqube.yml
Normal file
45
.forgejo/workflows/sonarqube.yml
Normal file
|
@ -0,0 +1,45 @@
|
|||
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# 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 }}
|
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
|
@ -1,2 +1,6 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
github: wneessen
|
||||
ko_fi: winni
|
||||
|
|
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,3 +1,9 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
-->
|
||||
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
|
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,3 +1,9 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
-->
|
||||
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
|
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
|
@ -1,3 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# 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:
|
||||
|
|
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
|
@ -1,3 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
|
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
|
@ -1,3 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Docker
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
|
|
4
.github/workflows/release-bsd.yml
vendored
4
.github/workflows/release-bsd.yml
vendored
|
@ -1,3 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
|
|
4
.github/workflows/release-darwin.yml
vendored
4
.github/workflows/release-darwin.yml
vendored
|
@ -1,3 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
|
|
4
.github/workflows/release-linux.yml
vendored
4
.github/workflows/release-linux.yml
vendored
|
@ -1,3 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
|
|
4
.github/workflows/release-windows.yml
vendored
4
.github/workflows/release-windows.yml
vendored
|
@ -1,3 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
|
|
4
.github/workflows/sonarqube.yml
vendored
4
.github/workflows/sonarqube.yml
vendored
|
@ -1,3 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
name: SonarQube
|
||||
on:
|
||||
push:
|
||||
|
|
21
.gitignore
vendored
21
.gitignore
vendored
|
@ -1,13 +1,13 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
apg
|
||||
bin
|
||||
.go
|
||||
build
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
@ -17,3 +17,16 @@ build
|
|||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Local testfiles and auth data
|
||||
.auth
|
||||
examples/*
|
||||
|
||||
# SonarQube
|
||||
.scannerwork/
|
||||
|
||||
# IDEA specific ignores
|
||||
.idea/
|
||||
.idea/.gitignore
|
||||
|
||||
dist/
|
||||
|
|
15
.golangci.toml
Normal file
15
.golangci.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
[run]
|
||||
go = "1.22"
|
||||
tests = true
|
||||
|
||||
[linters]
|
||||
enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder",
|
||||
"errname", "errorlint", "gofmt", "gofumpt", "goimports"]
|
||||
|
||||
[linters-settings.goimports]
|
||||
local-prefixes = "src.neessen.cloud/wneessen/apg-go"
|
||||
|
62
.goreleaser.yaml
Normal file
62
.goreleaser.yaml
Normal file
|
@ -0,0 +1,62 @@
|
|||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
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 <wn@neessen.dev>
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
- termux.deb
|
||||
- archlinux
|
||||
|
||||
dmg:
|
||||
- replace: true
|
||||
|
||||
universal_binaries:
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
|
@ -1,12 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="GrazieInspection" enabled="false" level="TYPO" enabled_by_default="false" />
|
||||
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/apg-go.iml" filepath="$PROJECT_DIR$/.idea/apg-go.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -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.
|
20
Dockerfile
20
Dockerfile
|
@ -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"]
|
21
LICENSE
21
LICENSE
|
@ -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.
|
121
LICENSES/CC0-1.0.txt
Normal file
121
LICENSES/CC0-1.0.txt
Normal file
|
@ -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.
|
9
LICENSES/MIT.txt
Normal file
9
LICENSES/MIT.txt
Normal file
|
@ -0,0 +1,9 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
|
||||
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.
|
10
README.md
10
README.md
|
@ -1,5 +1,13 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
|
||||
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) <a href="https://ko-fi.com/D1D24V9IX"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a>
|
||||
[![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)
|
||||
<a href="https://ko-fi.com/D1D24V9IX"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a>
|
||||
|
||||
_apg-go_ is a simple APG-like password generator written in Go. It tries to replicate the
|
||||
functionality of the
|
||||
|
|
|
@ -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.
|
38
algo.go
Normal file
38
algo.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apg
|
||||
|
||||
// Algorithm is a type wrapper for an int type to represent different
|
||||
// password generation algorithm
|
||||
type Algorithm int
|
||||
|
||||
const (
|
||||
// AlgoPronounceable represents the algorithm for pronounceable passwords
|
||||
// (koremutake syllables)
|
||||
AlgoPronounceable Algorithm = iota
|
||||
// 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
|
||||
)
|
||||
|
||||
// IntToAlgo takes an int value as input and returns the corresponding
|
||||
// Algorithm
|
||||
func IntToAlgo(a int) Algorithm {
|
||||
switch a {
|
||||
case 0:
|
||||
return AlgoPronounceable
|
||||
case 1:
|
||||
return AlgoRandom
|
||||
case 2:
|
||||
return AlgoCoinFlip
|
||||
default:
|
||||
return AlgoUnsupported
|
||||
}
|
||||
}
|
28
algo_test.go
Normal file
28
algo_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apg
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIntToAlgo(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
a int
|
||||
e Algorithm
|
||||
}{
|
||||
{"AlgoPronounceable", 0, AlgoPronounceable},
|
||||
{"AlgoRandom", 1, AlgoRandom},
|
||||
{"AlgoCoinflip", 2, AlgoCoinFlip},
|
||||
{"AlgoUnsupported", 3, 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
24
apg.go
Normal file
24
apg.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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 {
|
||||
// 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
|
||||
func New(config *Config) *Generator {
|
||||
return &Generator{
|
||||
config: config,
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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 <wn@neessen.net>
|
||||
|
||||
# MIT
|
||||
PERMIT_PACKAGE = Yes
|
||||
|
||||
MODULES = lang/go
|
||||
MODGO_TYPE = bin
|
||||
|
||||
ALL_TARGET = wneessen/apg-go/cmd/apg
|
||||
|
||||
.include <bsd.port.mk>
|
|
@ -1,2 +0,0 @@
|
|||
SHA256 (apg-go-0.3.2.tar.gz) = QvCC0vVNHLIOHW1jwdkjJVtxEVHJNwQfZBZBgHWM4OQ=
|
||||
SIZE (apg-go-0.3.2.tar.gz) = 20114
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
@comment $OpenBSD: PLIST,v$
|
||||
@bin bin/apg-go
|
|
@ -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
|
||||
}
|
312
cmd/apg/apg.go
312
cmd/apg/apg.go
|
@ -1,148 +1,216 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package main is the APG command line client that makes use of the apg-go library
|
||||
|
||||
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"
|
||||
|
||||
"src.neessen.cloud/wneessen/apg-go"
|
||||
)
|
||||
|
||||
// VersionString represents the current version of the apg-go CLI
|
||||
const VersionString string = "0.4.1"
|
||||
// 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"
|
||||
|
||||
// Help text
|
||||
const usage = `apg-go // A "Automated Password Generator"-clone
|
||||
Copyright (c) 2021 Winni Neessen
|
||||
|
||||
apg [-a <algo>] [-m <length>] [-x <length>] [-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)
|
||||
config := apg.NewConfig()
|
||||
|
||||
// Read and parse flags
|
||||
flag.Usage = func() { _, _ = fmt.Fprintf(os.Stderr, "%s\n", usage) }
|
||||
var cfgObj = config.New()
|
||||
// Configure and parse the CLI flags
|
||||
// See usage() for flag details
|
||||
var algorithm int
|
||||
var modeString string
|
||||
var complexPass, humanReadable, lowerCase, numeric, special, showVer, upperCase bool
|
||||
flag.IntVar(&algorithm, "a", 1, "")
|
||||
flag.BoolVar(&complexPass, "C", 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()
|
||||
|
||||
// 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")
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// Old style character modes
|
||||
if humanReadable {
|
||||
config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeHumanReadable)
|
||||
}
|
||||
if lowerCase {
|
||||
config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeLowerCase)
|
||||
}
|
||||
if upperCase {
|
||||
config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeUpperCase)
|
||||
}
|
||||
if numeric {
|
||||
config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeNumeric)
|
||||
}
|
||||
if special {
|
||||
config.Mode = apg.MaskToggleMode(config.Mode, apg.ModeSpecial)
|
||||
}
|
||||
if complexPass {
|
||||
config.Mode = apg.MaskSetMode(config.Mode, apg.ModeLowerCase|apg.ModeNumeric|
|
||||
apg.ModeSpecial|apg.ModeUpperCase)
|
||||
config.Mode = apg.MaskClearMode(config.Mode, apg.ModeHumanReadable)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// New style character modes (has higher priority than the old style modes)
|
||||
if modeString != "" {
|
||||
config.Mode = apg.ModesFromFlags(modeString)
|
||||
}
|
||||
|
||||
if cfgObj.CheckHibp {
|
||||
hc := hibp.New(hibp.WithHTTPTimeout(time.Second*2), hibp.WithPwnedPadding())
|
||||
pwnObj, _, err := hc.PwnedPassAPI.CheckPassword(p)
|
||||
// For the "minimum amount of" modes we need to imply at the type
|
||||
// of character mode is set
|
||||
if config.MinLowerCase > 0 {
|
||||
if float64(config.MinLength)/2 < float64(config.MinNumeric) {
|
||||
_, _ = os.Stderr.WriteString(MinimumAmountTooHigh)
|
||||
}
|
||||
config.Mode = apg.MaskSetMode(config.Mode, apg.ModeLowerCase)
|
||||
}
|
||||
if config.MinNumeric > 0 {
|
||||
if float64(config.MinLength)/2 < float64(config.MinLowerCase) {
|
||||
_, _ = os.Stderr.WriteString(MinimumAmountTooHigh)
|
||||
}
|
||||
config.Mode = apg.MaskSetMode(config.Mode, apg.ModeNumeric)
|
||||
}
|
||||
if config.MinSpecial > 0 {
|
||||
if float64(config.MinLength)/2 < float64(config.MinSpecial) {
|
||||
_, _ = os.Stderr.WriteString(MinimumAmountTooHigh)
|
||||
}
|
||||
config.Mode = apg.MaskSetMode(config.Mode, apg.ModeSpecial)
|
||||
}
|
||||
if config.MinUpperCase > 0 {
|
||||
if float64(config.MinLength)/2 < float64(config.MinUpperCase) {
|
||||
_, _ = os.Stderr.WriteString(MinimumAmountTooHigh)
|
||||
}
|
||||
config.Mode = apg.MaskSetMode(config.Mode, apg.ModeUpperCase)
|
||||
}
|
||||
|
||||
// Check if algorithm is supported
|
||||
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
|
||||
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)
|
||||
}
|
||||
if config.Algorithm == apg.AlgoRandom && config.SpellPassword {
|
||||
spellPass, err := apg.Spell(password)
|
||||
if err != nil {
|
||||
log.Printf("unable to check HIBP database: %v", err)
|
||||
_, _ = fmt.Fprintf(os.Stderr, "failed to spell password: %s\n", err)
|
||||
}
|
||||
if pwnObj != nil && pwnObj.Count != 0 {
|
||||
fmt.Print("^-- !!WARNING: The previously generated password was found in HIBP database. Do not use it!!\n")
|
||||
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)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// usage is used by the flag package to display the CLI usage message
|
||||
func usage() {
|
||||
// Usage text
|
||||
const ut = `apg-go v` +
|
||||
apg.VERSION + "\n" +
|
||||
`A OSS "Automated Password Generator"-clone -- https://src.neessen.cloud/wneessen/apg-go/
|
||||
Created 2021-2024 by Winni Neessen (MIT licensed)
|
||||
|
||||
apg [-a <algo>] [-m <length>] [-x <length>] [-L] [-U] [-N] [-S] [-H] [-C]
|
||||
[-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)
|
||||
- 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)
|
||||
- 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
|
||||
- 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 (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
|
||||
- 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)
|
||||
-N Toggle numeric characters in passwords (Default: on)
|
||||
-S Toggle special characters in passwords (Default: off)
|
||||
-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
|
||||
-v Show version string`
|
||||
|
||||
_, _ = os.Stderr.WriteString(ut + "\n\n")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
110
config.go
Normal file
110
config.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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
|
||||
// DefaultMode sets the default character set mode bitmask to a combination of
|
||||
// lower- and upper-case characters as well as numbers
|
||||
DefaultMode ModeMask = ModeLowerCase | ModeNumeric | ModeUpperCase
|
||||
// DefaultNumberPass reflects the default amount of passwords returned by the generator
|
||||
DefaultNumberPass int64 = 6
|
||||
)
|
||||
|
||||
// Config represents the apg.Generator config parameters
|
||||
type Config struct {
|
||||
// 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
|
||||
// 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
|
||||
// MaxLength sets the maximum length for a generated password
|
||||
MaxLength int64
|
||||
// MinLength sets the minimum length for a generated password
|
||||
MinLength int64
|
||||
// MinLowerCase represents the minimum amount of lower-case characters that have
|
||||
// to be part of the generated password
|
||||
MinLowerCase 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
|
||||
// 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
|
||||
// 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
|
||||
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(opts ...Option) *Config {
|
||||
config := &Config{
|
||||
MaxLength: DefaultMaxLength,
|
||||
MinLength: DefaultMinLength,
|
||||
Mode: DefaultMode,
|
||||
NumberPass: DefaultNumberPass,
|
||||
}
|
||||
|
||||
// Override defaults with optionally provided config.Option functions
|
||||
for _, opt := range opts {
|
||||
if opt == nil {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// WithMinLength overrides the minimum password length
|
||||
func WithMinLength(length int64) Option {
|
||||
return func(config *Config) {
|
||||
config.MinLength = length
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxLength overrides the maximum password length
|
||||
func WithMaxLength(length int64) Option {
|
||||
return func(config *Config) {
|
||||
config.MaxLength = length
|
||||
}
|
||||
}
|
||||
|
||||
// WithNumberPass overrides the amount of generated passwords setting
|
||||
func WithNumberPass(amount int64) Option {
|
||||
return func(config *Config) {
|
||||
config.NumberPass = amount
|
||||
}
|
||||
}
|
194
config/config.go
194
config/config.go
|
@ -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
|
||||
}
|
103
config_test.go
Normal file
103
config_test.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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 TestWithAlgorithm(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
algo Algorithm
|
||||
want int
|
||||
}{
|
||||
{"Pronouncble passwords", AlgoPronounceable, 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))
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
apg-go:*:1000:apg-go
|
|
@ -1 +0,0 @@
|
|||
apg-go:*:1000:1000:Automated Password Generator User:/apg-go:/usr/bin/false
|
|
@ -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)
|
||||
}
|
8
go.mod
8
go.mod
|
@ -1,5 +1,9 @@
|
|||
module github.com/wneessen/apg-go
|
||||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
go 1.16
|
||||
module src.neessen.cloud/wneessen/apg-go
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/wneessen/go-hibp v1.0.6
|
||||
|
|
3
go.sum.license
Normal file
3
go.sum.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
|
||||
SPDX-License-Identifier: MIT
|
20
hibp.go
Normal file
20
hibp.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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
|
||||
}
|
31
hibp_test.go
Normal file
31
hibp_test.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
package chars
|
||||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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",
|
||||
|
@ -19,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",
|
||||
}
|
113
mode.go
Normal file
113
mode.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Mode represents a mode of characters
|
||||
type Mode uint8
|
||||
|
||||
// ModeMask represents a bitmask of character modes
|
||||
type ModeMask uint8
|
||||
|
||||
const (
|
||||
// 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
|
||||
// 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"
|
||||
// CharRangeNumeric represents all numerical characters
|
||||
CharRangeNumeric = "1234567890"
|
||||
// CharRangeNumericHuman represents all human-readable numerical characters
|
||||
CharRangeNumericHuman = "23456789"
|
||||
// CharRangeSpecial represents all special characters
|
||||
CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~`
|
||||
// CharRangeSpecialHuman represents all human-readable special characters
|
||||
CharRangeSpecialHuman = `#%*+-:;=`
|
||||
)
|
||||
|
||||
// MaskSetMode sets a specific Mode to a given Mode bitmask
|
||||
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(mask ModeMask, mode Mode) ModeMask { return ModeMask(uint8(mask) &^ uint8(mode)) }
|
||||
|
||||
// MaskToggleMode toggles a specific Mode in a given Mode bitmask
|
||||
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(mask ModeMask, mode Mode) bool { return uint8(mask)&uint8(mode) != 0 }
|
||||
|
||||
func ModesFromFlags(maskString string) ModeMask {
|
||||
cl := strings.Split(maskString, "")
|
||||
var modeMask ModeMask
|
||||
for _, m := range cl {
|
||||
switch m {
|
||||
case "C":
|
||||
modeMask = MaskSetMode(modeMask, ModeLowerCase|ModeNumeric|ModeSpecial|ModeUpperCase)
|
||||
case "h":
|
||||
modeMask = MaskClearMode(modeMask, ModeHumanReadable)
|
||||
case "H":
|
||||
modeMask = MaskSetMode(modeMask, ModeHumanReadable)
|
||||
case "l":
|
||||
modeMask = MaskClearMode(modeMask, ModeLowerCase)
|
||||
case "L":
|
||||
modeMask = MaskSetMode(modeMask, ModeLowerCase)
|
||||
case "n":
|
||||
modeMask = MaskClearMode(modeMask, ModeNumeric)
|
||||
case "N":
|
||||
modeMask = MaskSetMode(modeMask, ModeNumeric)
|
||||
case "s":
|
||||
modeMask = MaskClearMode(modeMask, ModeSpecial)
|
||||
case "S":
|
||||
modeMask = MaskSetMode(modeMask, ModeSpecial)
|
||||
case "u":
|
||||
modeMask = MaskClearMode(modeMask, ModeUpperCase)
|
||||
case "U":
|
||||
modeMask = MaskSetMode(modeMask, ModeUpperCase)
|
||||
}
|
||||
}
|
||||
|
||||
return modeMask
|
||||
}
|
||||
|
||||
// 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 ModeNumeric:
|
||||
return "Numeric"
|
||||
case ModeSpecial:
|
||||
return "Special"
|
||||
case ModeUpperCase:
|
||||
return "Upper-case"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
119
mode_test.go
Normal file
119
mode_test.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetClearHasToggleMode(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
}{
|
||||
{"ModeHumanReadable", ModeHumanReadable},
|
||||
{"ModeLowerCase", ModeLowerCase},
|
||||
{"ModeNumeric", ModeNumeric},
|
||||
{"ModeSpecial", ModeSpecial},
|
||||
{"ModeUpperCase", ModeUpperCase},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var m ModeMask
|
||||
m = MaskSetMode(m, tc.mode)
|
||||
if !MaskHasMode(m, tc.mode) {
|
||||
t.Errorf("MaskSetMode() failed, mode not found in bitmask")
|
||||
}
|
||||
m = MaskToggleMode(m, tc.mode)
|
||||
if MaskHasMode(m, tc.mode) {
|
||||
t.Errorf("MaskToggleMode() failed, mode found in bitmask")
|
||||
}
|
||||
m = MaskToggleMode(m, tc.mode)
|
||||
if !MaskHasMode(m, tc.mode) {
|
||||
t.Errorf("MaskToggleMode() failed, mode not found in bitmask")
|
||||
}
|
||||
m = MaskClearMode(m, tc.mode)
|
||||
if MaskHasMode(m, tc.mode) {
|
||||
t.Errorf("MaskClearMode() failed, mode found in bitmask")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModesFromFlags(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
ms string
|
||||
mode []Mode
|
||||
}{
|
||||
{"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,
|
||||
}},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMode_String(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
m Mode
|
||||
e string
|
||||
}{
|
||||
{"ModeHumanReadable", ModeHumanReadable, "Human-readable"},
|
||||
{"ModeLowerCase", ModeLowerCase, "Lower-case"},
|
||||
{"ModeNumeric", ModeNumeric, "Numeric"},
|
||||
{"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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
361
random.go
Normal file
361
random.go
Normal file
|
@ -0,0 +1,361 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apg
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// 7 bits to represent a letter index
|
||||
letterIdxBits = 7
|
||||
// All 1-bits, as many as letterIdxBits
|
||||
letterIdxMask = 1<<letterIdxBits - 1
|
||||
// # of letter indices fitting in 63 bits)
|
||||
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")
|
||||
// ErrLengthMismatch is returned if the number of generated bytes does not match the expected length
|
||||
ErrLengthMismatch = errors.New("number of generated random bytes does not match the expected length")
|
||||
// ErrInvalidCharRange is returned if the given range of characters is not valid
|
||||
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 {
|
||||
coinFlip, _ := g.RandNum(2)
|
||||
return coinFlip
|
||||
}
|
||||
|
||||
// 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) {
|
||||
switch g.config.Algorithm {
|
||||
case AlgoPronounceable:
|
||||
return g.generatePronounceable()
|
||||
case AlgoCoinFlip:
|
||||
return g.generateCoinFlip()
|
||||
case AlgoRandom:
|
||||
return g.generateRandom()
|
||||
case AlgoUnsupported:
|
||||
return "", fmt.Errorf("unsupported algorithm")
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
minLength := g.config.MinLength
|
||||
maxLength := g.config.MaxLength
|
||||
if minLength > maxLength {
|
||||
maxLength = minLength
|
||||
}
|
||||
diff := maxLength - minLength + 1
|
||||
randNum, err := g.RandNum(diff)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
length := minLength + randNum
|
||||
if length <= 0 {
|
||||
return 1, nil
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
|
||||
// RandomBytes returns a byte slice of random bytes with given length that got generated by
|
||||
// the crypto/rand generator
|
||||
func (g *Generator) RandomBytes(length int64) ([]byte, error) {
|
||||
if length < 1 {
|
||||
return nil, ErrInvalidLength
|
||||
}
|
||||
bytes := make([]byte, length)
|
||||
numBytes, err := rand.Read(bytes)
|
||||
if int64(numBytes) != length {
|
||||
return nil, ErrLengthMismatch
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
// RandNum generates a random, non-negative number with given maximum value
|
||||
func (g *Generator) RandNum(max int64) (int64, error) {
|
||||
if max < 1 {
|
||||
return 0, ErrInvalidLength
|
||||
}
|
||||
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 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(length int64, charRange string) (string, error) {
|
||||
if length < 1 {
|
||||
return "", ErrInvalidLength
|
||||
}
|
||||
if len(charRange) < 1 {
|
||||
return "", ErrInvalidCharRange
|
||||
}
|
||||
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 {
|
||||
randString.Grow(int(length))
|
||||
}
|
||||
|
||||
charRangeLength := len(charRange)
|
||||
|
||||
randPool := make([]byte, 8)
|
||||
_, err := rand.Read(randPool)
|
||||
if err != nil {
|
||||
return randString.String(), err
|
||||
}
|
||||
for idx, char, rest := length-1, binary.BigEndian.Uint64(randPool), letterIdxMax; idx >= 0; {
|
||||
if rest == 0 {
|
||||
_, err = rand.Read(randPool)
|
||||
if err != nil {
|
||||
return randString.String(), err
|
||||
}
|
||||
char, rest = binary.BigEndian.Uint64(randPool), letterIdxMax
|
||||
}
|
||||
if i := int(char & letterIdxMask); i < charRangeLength {
|
||||
randString.WriteByte(charRange[i])
|
||||
idx--
|
||||
}
|
||||
char >>= letterIdxBits
|
||||
rest--
|
||||
}
|
||||
|
||||
return randString.String(), nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var charRange string
|
||||
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
||||
case true:
|
||||
charRange = CharRangeAlphaLowerHuman
|
||||
default:
|
||||
charRange = CharRangeAlphaLower
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, char := range charRange {
|
||||
count += strings.Count(password, string(char))
|
||||
}
|
||||
if int64(count) < g.config.MinLowerCase {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if g.config.MinNumeric > 0 {
|
||||
var charRange string
|
||||
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
||||
case true:
|
||||
charRange = CharRangeNumericHuman
|
||||
default:
|
||||
charRange = CharRangeNumeric
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, char := range charRange {
|
||||
count += strings.Count(password, string(char))
|
||||
}
|
||||
if int64(count) < g.config.MinNumeric {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if g.config.MinSpecial > 0 {
|
||||
var charRange string
|
||||
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
||||
case true:
|
||||
charRange = CharRangeSpecialHuman
|
||||
default:
|
||||
charRange = CharRangeSpecial
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, char := range charRange {
|
||||
count += strings.Count(password, string(char))
|
||||
}
|
||||
if int64(count) < g.config.MinSpecial {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if g.config.MinUpperCase > 0 {
|
||||
var charRange string
|
||||
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
||||
case true:
|
||||
charRange = CharRangeAlphaUpperHuman
|
||||
default:
|
||||
charRange = CharRangeAlphaUpper
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, char := range charRange {
|
||||
count += strings.Count(password, string(char))
|
||||
}
|
||||
if int64(count) < 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) {
|
||||
if g.CoinFlipBool() {
|
||||
return "Heads", nil
|
||||
}
|
||||
return "Tails", nil
|
||||
}
|
||||
|
||||
// generatePronounceable is executed when Generate() is called with Algorithm set
|
||||
// to AlgoPronounceable
|
||||
func (g *Generator) generatePronounceable() (string, error) {
|
||||
var password string
|
||||
g.syllables = make([]string, 0)
|
||||
|
||||
length, err := g.GetPasswordLength()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to calculate password length: %w", err)
|
||||
}
|
||||
|
||||
characterSet := KoremutakeSyllables
|
||||
characterSet = append(characterSet, 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: %w",
|
||||
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: %w",
|
||||
err)
|
||||
}
|
||||
randomChar := string(nextSyllable[characterPosition])
|
||||
nextSyllable = strings.ReplaceAll(nextSyllable, randomChar, strings.ToUpper(randomChar))
|
||||
}
|
||||
password += nextSyllable
|
||||
g.syllables = append(g.syllables, nextSyllable)
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// generateRandom is executed when Generate() is called with Algorithm set
|
||||
// to AlgoRandmom
|
||||
func (g *Generator) generateRandom() (string, error) {
|
||||
length, err := g.GetPasswordLength()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to calculate password length: %w", err)
|
||||
}
|
||||
charRange := g.GetCharRangeFromConfig()
|
||||
var password string
|
||||
var ok bool
|
||||
for !ok {
|
||||
password, err = g.RandomStringFromCharRange(length, charRange)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ok = g.checkMinimumRequirements(password)
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
|
@ -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<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
// GetChar generates random characters based on given character range
|
||||
// and password length
|
||||
func GetChar(cr string, l int) (string, error) {
|
||||
if l < 1 {
|
||||
return "", fmt.Errorf("length is negative")
|
||||
}
|
||||
rs := strings.Builder{}
|
||||
rs.Grow(l)
|
||||
crl := len(cr)
|
||||
|
||||
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; {
|
||||
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
|
||||
}
|
515
random_test.go
Normal file
515
random_test.go
Normal file
|
@ -0,0 +1,515 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerator_CoinFlip(t *testing.T) {
|
||||
g := New(NewConfig())
|
||||
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(NewConfig())
|
||||
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(NewConfig())
|
||||
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(NewConfig())
|
||||
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(NewConfig())
|
||||
var l int64 = 32 * 1024
|
||||
tt := []struct {
|
||||
name string
|
||||
cr string
|
||||
nr string
|
||||
sf bool
|
||||
}{
|
||||
{
|
||||
"CharRange:AlphaLower", CharRangeAlphaLower,
|
||||
CharRangeAlphaUpper + CharRangeNumeric + CharRangeSpecial, false,
|
||||
},
|
||||
{
|
||||
"CharRange:AlphaUpper", CharRangeAlphaUpper,
|
||||
CharRangeAlphaLower + CharRangeNumeric + CharRangeSpecial, false,
|
||||
},
|
||||
{
|
||||
"CharRange:Number", CharRangeNumeric,
|
||||
CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeSpecial, false,
|
||||
},
|
||||
{
|
||||
"CharRange:Special", CharRangeSpecial,
|
||||
CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumeric, false,
|
||||
},
|
||||
{
|
||||
"CharRange:Invalid", "",
|
||||
CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumeric + CharRangeSpecial, true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rs, err := g.RandomStringFromCharRange(l, tc.cr)
|
||||
if err != nil && !tc.sf {
|
||||
t.Errorf("RandomStringFromCharRange failed: %s", err)
|
||||
}
|
||||
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) {
|
||||
t.Errorf("RandomStringFromCharRange failed. Unexpected character found in returned string: %s", rs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCharRangeFromConfig(t *testing.T) {
|
||||
config := NewConfig()
|
||||
generator := New(config)
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
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 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,
|
||||
},
|
||||
}
|
||||
|
||||
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 TestGeneratePronounceable(t *testing.T) {
|
||||
config := NewConfig()
|
||||
generator := New(config)
|
||||
foundSylables := 0
|
||||
for range 100 {
|
||||
res, err := generator.generatePronounceable()
|
||||
if err != nil {
|
||||
t.Errorf("generatePronounceable() failed: %s", err)
|
||||
return
|
||||
}
|
||||
for _, syl := range KoremutakeSyllables {
|
||||
if strings.Contains(res, syl) {
|
||||
foundSylables++
|
||||
}
|
||||
}
|
||||
}
|
||||
if foundSylables < 100 {
|
||||
t.Errorf("generatePronounceable() 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: "Pronounceable",
|
||||
algorithm: AlgoPronounceable,
|
||||
},
|
||||
{
|
||||
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())
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = g.CoinFlip()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenerator_RandomBytes(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
g := New(NewConfig())
|
||||
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(NewConfig())
|
||||
cr := CharRangeAlphaUpper + CharRangeAlphaLower + CharRangeNumeric + CharRangeSpecial
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := g.RandomStringFromCharRange(32, cr)
|
||||
if err != nil {
|
||||
b.Errorf("RandomStringFromCharRange() failed: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +1,6 @@
|
|||
sonar.projectKey=apg-go
|
||||
# SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
sonar.projectKey=apg-go
|
||||
sonar.go.coverage.reportPaths=cov.out
|
|
@ -1,8 +1,11 @@
|
|||
package spelling
|
||||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wneessen/apg-go/chars"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -81,11 +84,11 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// String returns an english spelled version of the given string
|
||||
func String(pwString string) (string, error) {
|
||||
// Spell returns a given string as spelled english phonetic alphabet string
|
||||
func Spell(input string) (string, error) {
|
||||
var returnString []string
|
||||
for _, curChar := range pwString {
|
||||
curSpellString, err := ConvertCharToName(byte(curChar))
|
||||
for _, curChar := range input {
|
||||
curSpellString, err := ConvertByteToWord(byte(curChar))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -94,24 +97,23 @@ func String(pwString string) (string, error) {
|
|||
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) {
|
||||
// Pronounce returns last generated pronounceable password as spelled syllables string
|
||||
func (g *Generator) Pronounce() (string, error) {
|
||||
var returnString []string
|
||||
for _, curSyl := range sylList {
|
||||
isKore := false
|
||||
for _, x := range chars.KoremutakeSyllables {
|
||||
if x == strings.ToLower(curSyl) {
|
||||
isKore = true
|
||||
for _, syllable := range g.syllables {
|
||||
isKoremutake := false
|
||||
for _, x := range KoremutakeSyllables {
|
||||
if x == strings.ToLower(syllable) {
|
||||
isKoremutake = true
|
||||
}
|
||||
}
|
||||
|
||||
if isKore {
|
||||
returnString = append(returnString, curSyl)
|
||||
if isKoremutake {
|
||||
returnString = append(returnString, syllable)
|
||||
continue
|
||||
}
|
||||
|
||||
curSpellString, err := ConvertCharToName(curSyl[0])
|
||||
curSpellString, err := ConvertByteToWord(syllable[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -120,21 +122,21 @@ func Koremutake(sylList []string) (string, error) {
|
|||
return strings.Join(returnString, "-"), nil
|
||||
}
|
||||
|
||||
// ConvertCharToName converts a given ascii byte into the corresponding english spelled
|
||||
// name
|
||||
func ConvertCharToName(charByte byte) (string, error) {
|
||||
// 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
|
||||
if charByte > 64 && charByte < 91 {
|
||||
switch {
|
||||
case charByte > 64 && charByte < 91:
|
||||
returnString = alphabetNames[charByte]
|
||||
} else if charByte > 96 && charByte < 123 {
|
||||
case charByte > 96 && charByte < 123:
|
||||
returnString = strings.ToLower(alphabetNames[charByte-32])
|
||||
} else {
|
||||
default:
|
||||
returnString = symbNumNames[charByte]
|
||||
}
|
||||
|
||||
if returnString == "" {
|
||||
err := fmt.Errorf("cannot convert to character to name: %q is an unknown character", charByte)
|
||||
return "", err
|
||||
return "", fmt.Errorf("failed to convert given byte to word: %s is unsupported", string(charByte))
|
||||
}
|
||||
return returnString, nil
|
||||
}
|
165
spelling_test.go
Normal file
165
spelling_test.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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 = %s, wantErr %t", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("ConvertByteToWord() got = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue