Compare commits

...

53 Commits

Author SHA1 Message Date
Florian Walther
13c791b1ae added password counter
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 58s
2026-02-07 23:45:57 +01:00
Florian Walther
8869db0f6b screenshot auf v0.6.0 altualisiert 2026-02-07 16:49:46 +01:00
Florian Walther
c9296beb5f footer und AppVersion eingebaut
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 1m2s
2026-02-07 16:45:02 +01:00
Florian Walther
dd8eab7975 fixed URL 2026-02-07 00:34:05 +01:00
Florian Walther
3e2f4cf104 fixed spelling error 2026-02-07 00:32:20 +01:00
Florian Walther
9d4c816642 fixed spelling error 2026-02-07 00:31:40 +01:00
Florian Walther
2a8d343ff4 fixed spelling error 2026-02-07 00:29:41 +01:00
Florian Walther
c990d8a335 mention Dark- and Light-Mode in README 2026-02-06 13:08:16 +01:00
Florian Walther
e99bf45be9 favicon geändert
All checks were successful
/ push_to_registry (push) Successful in 53s
2026-02-06 13:04:52 +01:00
Florian Walther
3c324d355d favicon added 2026-02-06 12:43:48 +01:00
Florian Walther
149cdf2e63 new screenshot added 2026-02-06 12:40:50 +01:00
Florian Walther
3ab261d777 added templates, more solid DarkMode
All checks were successful
/ push_to_registry (push) Successful in 1m13s
2026-02-06 12:34:10 +01:00
Florian Walther
623cfd3a50 Merge branch 'main' of gitea.scu.si:Florian.Walther/Web-Password
All checks were successful
/ push_to_registry (push) Successful in 58s
2026-02-04 00:00:04 +01:00
Florian Walther
2f9ee42071 added darkmode 2026-02-03 23:55:35 +01:00
33fe4b2b80 added log statement to APIHandler
All checks were successful
/ push_to_registry (push) Successful in 51s
2026-02-02 22:06:42 +01:00
21f512c2d7 fixed log statement
All checks were successful
/ push_to_registry (push) Successful in 51s
2026-02-02 22:03:08 +01:00
dfda16f8e1 fixed update howto 2026-02-02 22:01:54 +01:00
44bb35abac added basic logging
Some checks failed
/ push_to_registry (push) Failing after 46s
2026-02-02 22:00:01 +01:00
1ae5f9c679 corrected pull url 2026-02-02 21:59:43 +01:00
9a052b3ef7 added more usage tips 2026-02-02 21:53:40 +01:00
9a906ec55c http to https changed in API page
All checks were successful
/ push_to_registry (push) Successful in 51s
2026-02-02 21:32:30 +01:00
8b93585422 workflow angepasst
All checks were successful
/ push_to_registry (push) Successful in 1m14s
2026-01-24 17:52:15 +01:00
59dd16d4ac fixed code URL
Some checks failed
/ push_to_registry (push) Failing after 1m38s
2026-01-24 17:42:32 +01:00
d7678274be added pull info
Some checks failed
/ push_to_registry (push) Failing after 1m38s
2026-01-17 22:57:04 +01:00
4a8f7525e7 updated README 2026-01-17 22:54:57 +01:00
fb55f47e5c fixed spelling 2026-01-17 22:32:35 +01:00
0251b9dc69 undo centered image, did not work 2026-01-17 22:30:58 +01:00
2b505b0d9d centered image 2026-01-17 22:30:00 +01:00
cb3545f261 updated claim in README.md 2026-01-17 22:29:08 +01:00
d673b97b4b deleted obsolete lines in misc/docker-compose.traefik.yml 2026-01-17 22:26:58 +01:00
1be4aeb6b8 updated MoreUsage.md 2026-01-17 22:24:16 +01:00
0a3f4ab5ef added dynamic hostname to helpHandler
Some checks failed
/ push_to_registry (push) Failing after 1m34s
2026-01-17 21:59:54 +01:00
36ce60aa28 container sized down to 500px width
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-17 21:44:01 +01:00
Florian Walther
d21b959104 58er Zeichensatz hinzugefügt 2026-01-17 13:13:09 +01:00
Florian Walther
2b822af907 deleted deprecated workflow 2026-01-17 12:57:04 +01:00
Florian Walther
2aa636409a Screenshot aktualisiert 2026-01-17 12:47:28 +01:00
Florian Walther
819af38886 added link to code repository
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-17 12:09:26 +01:00
Florian Walther
65fe245e9f updated README 2026-01-17 11:58:02 +01:00
Florian Walther
8ebde5ffbc reorganising, added docker-compose.traefik.yml, split README 2026-01-17 11:51:02 +01:00
Florian Walther
eef425d7b7 3rd try with registry_url non hard coded
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-17 10:23:37 +01:00
Florian Walther
490ec5b151 2nd try with registry_url non hard coded
Some checks failed
/ push_to_registry (push) Failing after 12s
2026-01-17 10:22:09 +01:00
Florian Walther
1e38f4b842 add title to README 2026-01-17 10:20:30 +01:00
Florian Walther
827d6fecec add pic 2026-01-16 23:01:34 +01:00
Florian Walther
e3a76baec7 README erweitert 2026-01-16 22:54:38 +01:00
Florian Walther
f41eb6de51 removed latex code for better readability 2026-01-16 22:51:10 +01:00
Florian Walther
90829b054c added SECURITY.md 2026-01-16 22:46:46 +01:00
Florian Walther
8db6c5af9f hard coded registry url
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-16 22:35:14 +01:00
Florian Walther
e8d34d1bc2 fixed typo 2
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-16 22:33:23 +01:00
Florian Walther
520420f863 fixed typo
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-16 22:31:04 +01:00
Florian Walther
656c3ab073 workflow docker-build-push set to workflow_dispatch 2026-01-16 22:29:41 +01:00
Florian Walther
162d69cb93 Variablen angepasst
Some checks failed
/ push_to_registry (push) Failing after 12s
2026-01-16 22:26:22 +01:00
Florian Walther
c6506dde33 docker-compose.yml added 2026-01-16 22:23:59 +01:00
Florian Walther
11dc42574b workflow docker-build-push deaktiviert
workflow docker-release aktiviert
2026-01-16 22:23:37 +01:00
16 changed files with 803 additions and 275 deletions

View File

@@ -1,27 +0,0 @@
name: Docker Build and Push
on: [push]
jobs:
build-and-push:
runs-on: docker
if: branch == 'main'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Login to Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_URL }} -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
- name: Build Docker Image
run: |
docker build -t ${{ secrets.REGISTRY_URL }}/FlorianWalther/password-generator:latest .
- name: Push Docker Image
run: |
docker push ${{ secrets.REGISTRY_URL }}/FlorianWalther/password-generator:latest
- name: Cleanup
run: |
docker system prune -f

View File

@@ -0,0 +1,32 @@
name: Docker Release Build
on:
push:
tags:
- 'v*' # Reagiert auf Tags die mit 'v' beginnen, z.B. v1.0.2
jobs:
push_to_registry:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Gitea
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_URL }} # gitea.scu.si
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
# Hier wird die Git-Referenz automatisch als Docker-Tag genutzt
#tags: gitea.scu.si/florianwalther/password-generator:${{ gitea.ref_name }}
build-args: |
APP_VERSION=${{ gitea.ref_name }}
tags: |
gitea.scu.si/florian.walther/password-generator:${{ gitea.ref_name }}
gitea.scu.si/florian.walther/password-generator:latest

View File

@@ -8,7 +8,8 @@ WORKDIR /app
COPY . . COPY . .
# Baue die Anwendung # Baue die Anwendung
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/password-generator ARG APP_VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X 'main.AppVersion=${APP_VERSION}'" -o /app/password-generator
# Verwende ein minimales Image für die finale Stage # Verwende ein minimales Image für die finale Stage
FROM scratch FROM scratch
@@ -16,9 +17,14 @@ FROM scratch
# Kopiere die gebaute Binärdatei # Kopiere die gebaute Binärdatei
COPY --from=builder /app/password-generator /password-generator COPY --from=builder /app/password-generator /password-generator
# Kopiere die templates
COPY templates/ /templates/
# Kopiere die static files
COPY static/ /static/
# Setze die Umgebungsvariable für die Ports # Setze die Umgebungsvariable für die Ports
ENV PORT=8080 ENV PORT=8080
# Starte die Anwendung # Starte die Anwendung
CMD ["/password-generator"] CMD ["/password-generator"]

View File

@@ -1,26 +1,41 @@
# Web-Password
# Funktionsweise _a web based password generator, with an API endpoint_
* Passwortgenerierung: Die Anwendung generiert ein 32-stelliges Passwort mit Großbuchstaben, Kleinbuchstaben und Ziffern (entspricht dem Befehl `apg -a 1 -m 32 -n 1 -M NCL`). ![App Screenshot](img/screenshot.png)
* Zwischenablage: Mit dem Button "In Zwischenablage kopieren" wird das Passwort in die Zwischenablage kopiert.
* Docker: Der Container enthält nur die Go-Anwendung und keine zusätzliche Linux-Distribution.
## Features
# Baue die Go-Anwendung * generates long and random, secure passwords (read about the [security considerations](SECURITY.md))
* copy to clipboard
* very small docker container, that only contains the application and has minimum attack surface
* supports DarkMode and LightMode, you can toggle
## Demo
There is a demo at [https://passwd.scu.si](https://passwd.scu.si)
## Usage
The following example shows how to get up your own instance with `docker compose`.
``` ```
go build -o password-generator ./ git clone https://gitea.scu.si/FlorianWalther/Web-Password.git
cd Web-Password
cp misc/docker-compose.yml ./
docker compose pull
docker compose up -d
``` ```
# Baue das Docker-Image ## Docker image
The latest official docker image is at [https://gitea.scu.si/Florian.Walther/-/packages/container/password-generator/latest](https://gitea.scu.si/Florian.Walther/-/packages/container/password-generator/latest)
You can pull it like this:
``` ```
docker build -t password-generator . docker pull gitea.scu.si/florian.walther/password-generator:latest
``` ```
# Starte den Docker Container ## more usage examples
```
docker run -p 8080:8080 password-generator
```
There are some more usage example in [misc/MoreUsage.md](misc/MoreUsage.md)

110
SECURITY.md Normal file
View File

@@ -0,0 +1,110 @@
# Security Considerations
---
## 1. Overview
This document analyzes the security of passwords generated by the application, which uses the following parameters:
- Length: 32 characters
- Character set: Uppercase letters (A-Z), lowercase letters (a-z), digits (0-9)
- No special characters (equivalent to `apg -a 1 -m 32 -n 1 -M NCL`)
---
## 2. Keyspace Analysis
### 2.1. Character Set and Length
- Character set size: 26 (uppercase) + 26 (lowercase) + 10 (digits) = **62 possible characters per position**.
- Password length: 32 characters.
### 2.2. Total Keyspace
The total number of possible passwords is calculated as:
62^32 ≈ 1.46 × 10^57
This means there are **1.46 decillion** possible combinations.
---
## 3. Brute-Force Resistance
### 3.1. Average Number of Guesses
On average, an attacker would need to try half of the keyspace to guess the correct password:
(62^32) / 2 ≈ 7.3 × 10^56 attempts
### 3.2. Time to Crack on Modern Hardware
| Hardware | Hashes per Second | Time to Exhaust Keyspace |
|-------------------|-------------------|--------------------------------|
| Modern CPU | 10 billion | 7.3 × 10^46 seconds | ≈ 2.3 × 10^39 years |
| Modern GPU | 100 billion | 7.3 × 10^45 seconds | ≈ 2.3 × 10^38 years |
**Note**: Even with massive parallelization (e.g., botnets or supercomputers), brute-forcing a 32-character password from this keyspace is practically infeasible.
---
## 4. Comparison with Shorter Passwords
| Length | Keyspace (62 Characters) | Average Guesses | Time on GPU (100 GigaHashes/s) |
|--------|--------------------------|-----------------------|-------------------------------|
| 16 | 4.7 × 10^28 | 2.35 × 10^28 | ~74 years |
| 24 | 1.3 × 10^43 | 6.5 × 10^42 | ~2.1 million years |
| 32 | 1.46 × 10^57 | 7.3 × 10^56 | ~2.3 trillion years |
---
## 5. Threat Model
### 5.1. Brute-Force Attacks
- **Conclusion**: Brute-force attacks are not a viable threat for 32-character passwords.
- **Mitigation**: Ensure the system enforces rate-limiting to prevent automated guessing.
### 5.2. Social Engineering and Side-Channel Attacks
- **Social Engineering**: Phishing, keyloggers, or shoulder surfing are more realistic threats than brute-force attacks.
- **Side-Channel Attacks**: Timing attacks or power analysis could theoretically reduce security if the password verification is poorly implemented.
- **Mitigation**: Use constant-time comparison functions for password verification.
### 5.3. Password Storage
- **Hashing**: Always store passwords using strong, adaptive hashing algorithms like:
- Argon2 (recommended for new systems)
- bcrypt or PBKDF2 (with high work factors)
- **Salting**: Use a unique salt per password to prevent rainbow table attacks.
---
## 6. Practical Recommendations
### 6.1. For Users
- **Password Managers**: Encourage the use of password managers to store and manage generated passwords.
- **Multi-Factor Authentication (MFA)**: Always enable MFA where possible to add an extra layer of security.
### 6.2. For Developers
- **Rate Limiting**: Implement rate limiting on authentication endpoints to slow down brute-force attempts.
- **Secure Transmission**: Ensure passwords are transmitted over TLS/SSL to prevent interception.
- **Password Policies**: Enforce policies that discourage password reuse and encourage regular updates.
### 6.3. For DFIR and Incident Response
- **Logging and Monitoring**: Log failed login attempts and monitor for unusual activity.
- **Incident Response Plan**: Have a plan in place for compromised accounts, including forced password resets and user notification.
---
## 7. Additional Considerations
### 7.1. Extended Character Set
If special characters are included (e.g., !@#$%^&*), the keyspace increases to:
72^32 ≈ 1.9 × 10^60
This further improves security but is not necessary for most use cases given the already massive keyspace.
### 7.2. Entropy Calculation
The entropy of a 32-character password from a 62-character set is:
log2(62^32) ≈ 192.6 bits
This exceeds the 128-bit security level recommended by NIST for cryptographic applications.
---
## 8. Conclusion
The passwords generated by this application are extremely secure against brute-force attacks due to their length and character diversity. The primary risks lie in human factors (e.g., phishing, reuse) and implementation flaws (e.g., weak hashing, lack of rate limiting).
For DFIR and high-security environments, combine these passwords with:
- Multi-Factor Authentication (MFA)
- Regular audits of authentication logs
- User education on social engineering risks
---
## 9. References
- [NIST Special Publication 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html) (Digital Identity Guidelines)
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
- [Argon2: The Memory-Hard Function for Password Hashing](https://github.com/P-H-C/phc-winner-argon2)

2
go.mod
View File

@@ -1,3 +1,3 @@
module gitea.scu.si/FlorianWalther/Web-Password module gitea.scu.si/Florian.Walther/Web-Password
go 1.24.1 go 1.24.1

BIN
img/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

347
main.go
View File

@@ -1,269 +1,154 @@
package main package main
import ( import (
"os"
"strconv"
"strings"
"sync"
"crypto/rand" "crypto/rand"
"fmt" "html/template"
"log" "log"
"net/http" "net/http"
"encoding/json"
) )
const ( const (
passwordLength = 32 passwordLength = 32
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
//chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?$%&=#+<>-:,.;_*@"
) )
var (
templates = make(map[string]*template.Template)
AppVersion = "development"
counterFile = "/data/counter.txt"
mu sync.Mutex
)
// Diese Funktion wird nur intern aufgerufen, wenn der Mutex bereits gesperrt ist
func getCount() int {
data, err := os.ReadFile(counterFile)
if err != nil {
return 0
}
count, _ := strconv.Atoi(strings.TrimSpace(string(data)))
return count
}
// Öffentliche Funktion für das Template (mit Lock)
func GetPasswordCount() int {
mu.Lock()
defer mu.Unlock()
return getCount()
}
// Öffentliche Funktion zum Erhöhen (mit Lock)
func IncrementPasswordCount() {
mu.Lock()
defer mu.Unlock()
// Wir rufen jetzt die interne Funktion auf, die NICHT versucht,
// den Mutex erneut zu sperren
count := getCount()
count++
os.WriteFile(counterFile, []byte(strconv.Itoa(count)), 0644)
}
func loadTemplates() {
// 1. FuncMap definieren
funcMap := template.FuncMap{
"getAppVersion": func() string { return AppVersion },
"getPassCount": func() int { return GetPasswordCount() },
}
// 2. Templates mit FuncMap laden
// Wir nutzen New("base.html"), da base.html meist das Haupt-Layout definiert
templates["index.html"] = template.Must(template.New("base.html").Funcs(funcMap).ParseFiles(
"templates/base.html",
"templates/index.html",
))
templates["help.html"] = template.Must(template.New("base.html").Funcs(funcMap).ParseFiles(
"templates/base.html",
"templates/help.html",
))
log.Printf("Alle Templates erfolgreich geladen")
}
func generatePassword() string { func generatePassword() string {
log.Printf("called generatePassword\n")
password := make([]byte, passwordLength) password := make([]byte, passwordLength)
_, err := rand.Read(password) _, err := rand.Read(password)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for i := 0; i < passwordLength; i++ { for i := 0; i < passwordLength; i++ {
password[i] = chars[int(password[i])%len(chars)] password[i] = chars[int(password[i])%len(chars)]
} }
IncrementPasswordCount()
return string(password) return string(password)
} }
func passwordHandler(w http.ResponseWriter, r *http.Request) { func passwordHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("called passwordHandler\n")
password := generatePassword() password := generatePassword()
fmt.Fprint(w, password) currentCount := GetPasswordCount()
response := map[string]interface{}{
"password": password,
"count": currentCount,
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(response)
if err != nil {
log.Printf("Fehler beim Senden des JSON: %v", err)
http.Error(w, "Interner Fehler", http.StatusInternalServerError)
return
}
}
func passwordAPIHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("called passwordHandler\n")
password := generatePassword()
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(password))
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("call indexHandler: Request %s %s\n", r.Method, r.URL)
password := generatePassword()
//password := "load..."
data := struct {
Password string
}{
Password: password,
}
log.Printf("prepare template for index\n")
err := templates["index.html"].ExecuteTemplate(w, "base.html", data)
if err != nil {
log.Printf("Fehler beim Rendern des Templates: %v", err)
http.Error(w, "Interner Serverfehler", http.StatusInternalServerError)
}
} }
func helpHandler(w http.ResponseWriter, r *http.Request) { func helpHandler(w http.ResponseWriter, r *http.Request) {
helpHTML := ` log.Printf("call helpHandler\n")
<!DOCTYPE html> err := templates["help.html"].ExecuteTemplate(w, "base.html", nil)
<html> if err != nil {
<head> log.Printf("Fehler beim Rendern des Templates: %v", err)
<title>Hilfe</title> http.Error(w, "Interner Serverfehler", http.StatusInternalServerError)
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
color: #333;
} }
.help-container {
text-align: left;
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 800px;
width: 90%;
min-width: 600px;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: #444;
}
pre {
font-family: 'Courier New', Courier, monospace;
background: #f0f0f0;
padding: 0.8rem;
border-radius: 4px;
overflow-x: auto;
}
a {
color: #007BFF;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="help-container">
<h1>Hilfe: API-Endpunkt</h1>
<p>
Diese Anwendung bietet einen API-Endpunkt, um Passwörter direkt über die Kommandozeile abzurufen.
Der Endpunkt gibt das Passwort im Plain-Text-Format zurück.
</p>
<h2>Endpunkt:</h2>
<p><code>http://localhost:8080/api/password</code></p>
<h2>Beispiele:</h2>
<h3>Mac/Linux (Terminal):</h3>
<pre>echo $(curl -s http://localhost:8080/api/password)</pre>
<h3>Windows (PowerShell):</h3>
<pre>Invoke-RestMethod -Uri http://localhost:8080/api/password</pre>
<h3>Windows (cmd):</h3>
<pre>curl http://localhost:8080/api/password</pre>
<p>
<a href="/">Zurück zur Passwort-Generierung</a>
</p>
</div>
</body>
</html>
`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, helpHTML)
}
func webHandler(w http.ResponseWriter, r *http.Request) {
password := generatePassword()
html := fmt.Sprintf(
`<DOCTYPE html>
<html>
<head>
<title>Passwort-Generator</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
color: #333;
}
.container {
text-align: center;
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 90%%;
min-width: 600px;
position: relative;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: #444;
}
#password {
font-family: 'Courier New', Courier, monospace;
font-size: 1.2rem;
letter-spacing: 1px;
margin: 1rem auto;
padding: 0.8rem;
background: #f0f0f0;
border-radius: 4px;
border: 1px solid #ddd;
width: 90%%;
word-break: break-all;
}
.copy-button {
background: #4CAF50;
color: white;
border: none;
padding: 0.6rem 1.2rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
margin: 0.3rem;
}
.copy-button:hover {
background: #45a049;
}
.renew-button {
background: #007BFF;
color: white;
border: none;
padding: 0.6rem 1.2rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
margin: 0.3rem;
}
.renew-button:hover {
background: #0056b3;
}
.help-link {
position: absolute;
top: 1rem;
right: 1rem;
font-size: 1.2rem;
color: #999;
text-decoration: none;
}
.help-link:hover {
color: #444;
}
.buttons {
display: flex;
justify-content: center;
gap: 0.5rem;
}
#toast {
visibility: hidden;
min-width: 150px;
background-color: #4CAF50;
color: white;
text-align: center;
border-radius: 4px;
padding: 0.5rem;
position: fixed;
top: 20px;
right: 20px;
z-index: 1;
font-size: 0.9rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body>
<div class="container">
<a href="/help" class="help-link">API</a>
<h1>Generiertes Passwort</h1>
<div id="password">%s</div>
<div class="buttons">
<button class="copy-button" onclick="copyToClipboard()">In Zwischenablage kopieren</button>
<button class="renew-button" onclick="generateNewPassword()">Neues Passwort generieren</button>
</div>
<div id="toast">✓ Kopiert!</div>
</div>
<script>
function copyToClipboard() {
const password = document.getElementById("password").innerText;
navigator.clipboard.writeText(password).then(() => {
const toast = document.getElementById("toast");
toast.style.visibility = "visible";
setTimeout(() => {
toast.style.visibility = "hidden";
}, 1500); // Toast verschwindet nach 1,5 Sekunden
}).catch(err => {
console.error("Fehler beim Kopieren: ", err);
alert("Kopieren fehlgeschlagen. Bitte manuell kopieren: " + password);
});
}
function generateNewPassword() {
fetch("/api/password")
.then(response => response.text())
.then(password => {
document.getElementById("password").innerText = password;
})
.catch(error => console.error("Fehler:", error));
}
</script>
</body>
</html>`,
password,
)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, html)
} }
func main() { func main() {
http.HandleFunc("/", webHandler) loadTemplates()
http.HandleFunc("/api/password", passwordHandler) fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
http.HandleFunc("/", indexHandler)
http.HandleFunc("/api/password", passwordAPIHandler)
http.HandleFunc("/json/password", passwordHandler)
http.HandleFunc("/help", helpHandler) http.HandleFunc("/help", helpHandler)
log.Println("Server läuft auf http://localhost:8080") log.Println("Server läuft auf http://localhost:8080")
log.Println("Plain-Text-Passwort: curl http://localhost:8080/api/password")
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))
} }

87
misc/MoreUsage.md Normal file
View File

@@ -0,0 +1,87 @@
## bash alias
You can configure an bash alias in your `~/.bashrc` like this:
```
## genpasswd alias
alias genpasswd='echo $(curl -s https://passwd.scu.si/api/password)'
```
After making above changes you have to reload your ~/bashrc, in order to activate your changes.
```
. ~/.bashrc
```
Now you can enter `genpasswd` and get a fresh password from the API Endpoint.
## get 10 fresh passwords
```bash
for i in {1..10}; do echo $(curl -s https://passwd.scu.si/api/password); done
```
# building the app
you can build the app yourself like this:
```
go build -o password-generator ./
```
# build a docker container
```
docker build -t password-generator .
```
# start the docker container
```
docker run -p 8080:8080 password-generator
```
## docker-compose
There are two example docker-compose files in the [misc](./) directory.
### docker-compose.yml
A basic variant that just brings up the container and export port 8080.
The basic variant can be used without modifications.
### docker-compose.traefik.yml
The other one is meant to be used behind a traefik reverse proxy.
This variant has lables to configure traefik accordingly.
The traefik variant needs to be adjusted to your environment before
you can use it successfully.
### initial pull
```
docker compose pull
```
### start up
```
docker compose up -d
```
### bring down
```
docker compose down
```
### update container
In order to update your container to the current version, do this:
```
docker compose pull
docker compose up -d
```

View File

@@ -0,0 +1,24 @@
services:
password-generator:
image: gitea.scu.si/florian.walther/password-generator:latest
container_name: password-generator
restart: always
volumes:
- ./app_data:/data
expose:
- "8080:8080"
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik_backend"
- "traefik.http.routers.webpass.rule=Host(`passwd.scu.si`)"
- "traefik.http.routers.webpass.entrypoints=web,websecure"
- "traefik.http.routers.webpass.tls=true"
- "traefik.http.routers.webpass.tls.certresolver=myresolver"
- "traefik.http.services.webpass.loadbalancer.server.port=8080"
networks:
- traefik_backend
networks:
traefik_backend:
external: true

11
misc/docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
services:
password-generator:
image: gitea.scu.si/florian.walther/password-generator:latest
container_name: password-generator
restart: always
volumes:
- ./app_data:/data
ports:
- "8080:8080"
# Falls die Registry privat ist, muss der Host zuvor mit
# 'docker login gitea.scu.si' angemeldet worden sein.

BIN
static/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

232
static/style.css Normal file
View File

@@ -0,0 +1,232 @@
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--container-bg: white;
--button-bg: #007BFF;
--button-hover: #0056b3;
--copy-button-bg: #4CAF50;
--copy-button-hover: #45a049;
--password-bg: #f0f0f0;
--border-color: #ddd;
--shadow-color: rgba(0, 0, 0, 0.1);
}
.dark {
--bg-color: #121212;
--text-color: #e0e0e0;
--container-bg: #1e1e1e;
--button-bg: #2a7df4;
--button-hover: #1a5fb4;
--copy-button-bg: #4caf60;
--copy-button-hover: #3d8b40;
--password-bg: #2d2d2d;
--border-color: #444;
--shadow-color: rgba(0, 0, 0, 0.3);
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
margin-bottom: 60px;
}
.container {
text-align: center;
background: var(--container-bg);
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px var(--shadow-color);
width: 50%;
min-width: 600px;
position: relative;
}
footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--bg-color);
border-top: 1px solid var(--border-color);
padding: 10px 20px;
box-sizing: border-box;
box-shadow: 0 2px 10px var(--shadow-color);
z-index: 1000;
}
.footer-container {
display: flex;
justify-content: flex-end; /* Schiebt alles nach rechts */
gap: 20px; /* Abstand zwischen Counter und Version */
align-items: center;
}
.footer-item {
display: flex;
align-items: center;
gap: 8px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 13px;
color:x#4b5563;
}
.label {
font-weight: 500;
}
/* Die Badges (Status-Pillen) */
.value {
padding: 2px 8px;
border-radius: 12px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-weight: 600;
font-size: 11px;
}
.badge-blue {
background-color: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.badge-gray {
background-color: var(--bg-color);
color: var(--text-color);
border: 1px solid var(border-color);
}
.claim {
font-family: monospace; /* Monospace sieht für Versionen oft "technischer" aus */
font-size: 12px;
color: var(--text-color);
}
#password {
font-family: 'Courier New', Courier, monospace;
font-size: 1.2rem;
letter-spacing: 1px;
margin: 1rem auto;
padding: 0.8rem;
background: var(--password-bg);
border-radius: 4px;
border: 1px solid var(--border-color);
width: 90%;
word-break: break-all;
color: var(--text-color);
}
.buttons {
display: flex;
justify-content: center;
gap: 0.5rem;
}
.copy-button {
background: var(--copy-button-bg);
color: white;
border: none;
padding: 0.6rem 1.2rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.copy-button:hover {
background: var(--copy-button-hover);
}
.renew-button {
background: var(--button-bg);
color: white;
border: none;
padding: 0.6rem 1.2rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.renew-button:hover {
background: var(--button-hover);
}
.code-link {
position: absolute;
top: 1rem;
left: 1rem;
font-size: 1.2rem;
color: var(--text-color);
opacity: 0.7;
text-decoration: none;
}
.code-link:hover {
opacity: 1;
}
.help-link {
position: absolute;
top: 1rem;
right: 1rem;
font-size: 1.2rem;
color: var(--text-color);
opacity: 0.7;
text-decoration: none;
}
.help-link:hover {
opacity: 1;
}
#toast {
visibility: hidden;
min-width: 150px;
background-color: var(--copy-button-bg);
color: white;
text-align: center;
border-radius: 4px;
padding: 0.5rem;
position: fixed;
top: 20px;
right: 20px;
z-index: 1;
font-size: 0.9rem;
box-shadow: 0 2px 10px var(--shadow-color);
}
#theme-toggle {
position: absolute;
top: 1rem;
left: 1rem;
background: transparent;
border: none;
color: var(--text-color);
font-size: 1.2rem;
cursor: pointer;
}
pre {
font-family: 'Courier New', Courier, monospace;
background: var(--password-bg);
padding: 0.8rem;
border-radius: 4px;
color: var(--text-color);
border: 1px solid var(--border-color);
}
a {
color: var(--button-bg);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}

66
templates/base.html Normal file
View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ block "title" . }}Passwort-Generator{{ end }}</title>
<link rel="icon" href="/static/key.png" type="image/png">
<link rel="stylesheet" href="/static/style.css">
<script>
document.addEventListener('DOMContentLoaded', function() {
const root = document.documentElement;
const savedTheme = localStorage.getItem('theme');
const themeToggle = document.getElementById('theme-toggle');
// Setze das Theme basierend auf localStorage oder Systemeinstellung
if (savedTheme === 'dark') {
root.classList.add('dark');
} else if (savedTheme === 'light') {
root.classList.remove('dark');
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
root.classList.add('dark');
}
// Toggle-Button-Logik
themeToggle.addEventListener('click', function() {
if (root.classList.contains('dark')) {
root.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
root.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
});
// Systemtheme-Änderungen abhören
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
e.matches ? root.classList.add('dark') : root.classList.remove('dark');
}
});
});
</script>
{{ block "head" . }}{{ end }}
</head>
<body>
<button id="theme-toggle">🌓</button>
{{ block "body" . }}{{end}}
<!-- <footer>Version: {{getAppVersion}} | made with golang and ♥️ {{ block "footer" . }}{{ end }}</footer> -->
<footer>
<div class="footer-container">
<div class="footer-item">
<span class="label">Passwörter generiert:</span>
<span id="global-counter" class="value badge-blue">{{getPassCount}}</span>
</div>
<div class="footer-item">
<span class="label">Version:</span>
<span class="value badge-gray">{{getAppVersion}}</span>
</div>
<div class="footer-item">
<span class="claim">made with golang and ♥️ </span>
</div>
</div>
</footer>
</body>
</html>

35
templates/help.html Normal file
View File

@@ -0,0 +1,35 @@
{{ define "help.html" }}
{{ block "title" . }}Hilfe{{ end }}
{{ block "head" . }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const currentHost = window.location.host;
const apiEndpoint = window.location.protocol + "//" + currentHost + "/api/password";
document.getElementById("api-endpoint").textContent = apiEndpoint;
document.getElementById("curl-example").textContent = "curl " + apiEndpoint;
document.getElementById("powershell-example").textContent = "Invoke-RestMethod -Uri " + apiEndpoint;
document.getElementById("cmd-example").textContent = "curl " + apiEndpoint;
});
</script>
{{ end }}
{{ block "body" . }}
<div class="container">
<h1>Hilfe: API-Endpunkt</h1>
<p>Diese Anwendung bietet einen API-Endpunkt, um Passwörter direkt über die Kommandozeile abzurufen.</p>
<h2>Endpunkt:</h2>
<p><code id="api-endpoint"></code></p>
<h2>Beispiele:</h2>
<h3>Mac/Linux (Terminal):</h3>
<pre id="curl-example"></pre>
<h3>Windows (PowerShell):</h3>
<pre id="powershell-example"></pre>
<h3>Windows (cmd):</h3>
<pre id="cmd-example"></pre>
<p><a href="/">Zurück zur Passwort-Generierung</a></p>
</div>
{{ end }}
{{ end }}

52
templates/index.html Normal file
View File

@@ -0,0 +1,52 @@
{{ define "index.html" }}
{{ block "title" . }}Passwort-Generator{{ end }}
{{ block "head" . }}
<script>
function copyToClipboard() {
const password = document.getElementById("password").innerText;
navigator.clipboard.writeText(password).then(() => {
const toast = document.getElementById("toast");
toast.style.visibility = "visible";
setTimeout(() => { toast.style.visibility = "hidden"; }, 1500);
}).catch(err => {
console.error("Fehler beim Kopieren: ", err);
alert("Kopieren fehlgeschlagen. Bitte manuell kopieren: " + password);
});
}
function generateNewPassword() {
fetch("/json/password")
.then(response => response.json()) // Jetzt .json() statt .text()
.then(data => {
// Passwort aktualisieren
document.getElementById("password").innerText = data.password;
// Counter im Footer aktualisieren
// Wir suchen das Element mit der Klasse 'badge-blue' (oder gib ihm eine ID)
const counterElement = document.querySelector(".badge-blue");
if (counterElement) {
//counterElement.innerText = data.count;
document.getElementById("global-counter").innerText = data.count;
}
})
.catch(error => console.error("Fehler:", error));
}
</script>
{{ end }}
{{ block "body" . }}
<div class="container">
<a href="/help" class="help-link">?</a>
<a href="https://gitea.scu.si/Florian.Walther/Web-Password" class="code-link">Sourcecode</a>
<h1>Generiertes Passwort</h1>
<div id="password">{{ .Password }}</div>
<div class="buttons">
<button class="copy-button" onclick="copyToClipboard()">In Zwischenablage kopieren</button>
<button class="renew-button" onclick="generateNewPassword()">Neues Passwort generieren</button>
</div>
<div id="toast">✓ Kopiert!</div>
</div>
{{ end }}
{{ end }}