This report covers three research areas:
- Chromium Password Forms Best Practices - Complete recommendations extracted
- Go Password Generation Libraries - Top options analyzed
- Go Configuration GUI Libraries - Assessment of alternatives
CRITICAL ISSUES:
- ❌ Missing autocomplete attributes on ALL password and username fields
- ❌ Duplicate IDs - Line 449 and 454 both use generic names that could conflict
- ❌ No differentiation between "new password" vs "current password" contexts
Current State:
Line 432: <input type="text" id="postgresUsername" value="recorder" required>
Line 437: <input type="password" id="postgresPassword" value="recorder" required>
Line 449: <input type="text" id="webrtcUsername" value="testuser" required>
Line 454: <input type="password" id="webrtcPassword" value="testing123" required>
Line 494: <input type="email" id="emailTo" placeholder="your-email@example.com">
Line 498: <input type="email" id="emailFrom" placeholder="security@webcam.local">For WebRTC Username/Password (these are NEW credentials being SET):
<label for="webrtcUsername">Username <span class="required">*</span></label>
<input
type="text"
id="webrtcUsername"
name="webrtcUsername"
value="testuser"
autocomplete="username"
required>
<label for="webrtcPassword">Password <span class="required">*</span></label>
<input
type="password"
id="webrtcPassword"
name="webrtcPassword"
value="testing123"
autocomplete="new-password"
required>For PostgreSQL Credentials:
<label for="postgresUsername">Username <span class="required">*</span></label>
<input
type="text"
id="postgresUsername"
name="postgresUsername"
value="recorder"
autocomplete="username"
required>
<label for="postgresPassword">Password <span class="required">*</span></label>
<input
type="password"
id="postgresPassword"
name="postgresPassword"
value="recorder"
autocomplete="new-password"
required>For MinIO Secret Key:
<label for="minioSecretKey">Secret Key <span class="required">*</span></label>
<input
type="password"
id="minioSecretKey"
name="minioSecretKey"
value="minioadmin"
autocomplete="new-password"
required>For Email Fields:
<label for="emailTo">To Email</label>
<input
type="email"
id="emailTo"
name="emailTo"
placeholder="your-email@example.com"
autocomplete="email">
<label for="emailFrom">From Email</label>
<input
type="email"
id="emailFrom"
name="emailFrom"
placeholder="security@webcam.local"
autocomplete="email">All inputs MUST have a name attribute for proper form submission and password manager functionality:
- Add
name="webrtcUsername"to username inputs - Add
name="webrtcPassword"to password inputs - Add
name="postgresUsername",name="postgresPassword", etc.
Use the right autocomplete value based on context:
- New credentials being configured:
autocomplete="new-password" - Existing credentials to authenticate:
autocomplete="current-password" - Username fields:
autocomplete="username" - Email fields:
autocomplete="email"
✅ DO:
- Group related fields in a single
<form>element - Use
autocomplete="username"for username fields - Use
autocomplete="current-password"for login/authentication - Use
autocomplete="new-password"for registration/configuration - Use
autocomplete="email"for email fields - Add unique
idattributes to all fields - Add
nameattributes to all form fields - Make form submission clear (navigation or History API)
- Use hidden fields for implicit information (e.g., email-first flow)
❌ DON'T:
- Combine multiple processes (registration + login) in one form
- Skip the
<form>element - Use duplicate IDs
- Insert fake/honeypot fields (confuses password managers)
- Use incorrect autocomplete attributes
- Navigate to separate page on login failure
Basic Sign-in Form:
<form id="login" action="/login" method="post">
<input id="username" type="text" name="username" autocomplete="username" required>
<input id="password" type="password" name="password" autocomplete="current-password" required>
<button type="submit">Sign In</button>
</form>Registration/Setup Form:
<form id="signup" action="/signup" method="post">
<input id="username" type="text" name="username" autocomplete="username" required>
<input id="password" type="password" name="password" autocomplete="new-password" required>
<button type="submit">Create Account</button>
</form>With Password Confirmation:
<form id="signup" action="/signup" method="post">
<input id="username" type="text" name="username" autocomplete="username" required>
<input id="password" type="password" name="password" autocomplete="new-password" required>
<input id="password-confirm" type="password" name="password-confirm" autocomplete="new-password" required>
<button type="submit">Create Account</button>
</form>Why:
- No external dependencies
- Cryptographically secure
- Simple to implement
- Full control over logic
- Well-tested by Go team
Implementation Example:
package password
import (
"crypto/rand"
"errors"
"math/big"
)
type Config struct {
Length int
IncludeUpper bool
IncludeLower bool
IncludeDigits bool
IncludeSymbols bool
MinDigits int
MinSymbols int
}
const (
lowerChars = "abcdefghijklmnopqrstuvwxyz"
upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
digitChars = "0123456789"
symbolChars = "!@#$%^&*()-_=+[]{}|;:,.<>?"
)
func Generate(cfg Config) (string, error) {
if cfg.Length < 8 {
return "", errors.New("password must be at least 8 characters")
}
// Build character set
var charset string
if cfg.IncludeLower {
charset += lowerChars
}
if cfg.IncludeUpper {
charset += upperChars
}
if cfg.IncludeDigits {
charset += digitChars
}
if cfg.IncludeSymbols {
charset += symbolChars
}
if len(charset) == 0 {
return "", errors.New("must include at least one character type")
}
// Generate password
password := make([]byte, cfg.Length)
charsetLen := big.NewInt(int64(len(charset)))
for i := range password {
idx, err := rand.Int(rand.Reader, charsetLen)
if err != nil {
return "", err
}
password[i] = charset[idx.Int64()]
}
// TODO: Add ensureMinimumRequirements() if needed
return string(password), nil
}
// Simple one-liner for default password
func GenerateDefault() (string, error) {
return Generate(Config{
Length: 16,
IncludeUpper: true,
IncludeLower: true,
IncludeDigits: true,
IncludeSymbols: true,
MinDigits: 2,
MinSymbols: 2,
})
}Usage:
// Default secure password
pass, err := password.GenerateDefault()
// Returns: "aB3$xYz9!mNoPqR"
// Custom requirements
pass, err := password.Generate(password.Config{
Length: 20,
IncludeUpper: true,
IncludeLower: true,
IncludeDigits: true,
IncludeSymbols: false, // No symbols for compatibility
})If you prefer a library:
Installation:
go get github.com/sethvargo/go-password/passwordUsage:
import "github.com/sethvargo/go-password/password"
// Generate(length, numDigits, numSymbols, noUpper, allowRepeat)
pass, err := password.Generate(16, 3, 3, false, false)
// Returns 16-char password with 3 digits, 3 symbols, uppercase allowed, no repeatsPros:
- Simple API
- Well-tested
- Active maintenance
Cons:
- External dependency
- Less control than custom implementation
- elithrar/simple-scrypt - For password HASHING, not generation
- crypto/bcrypt - For password HASHING, not generation
- golang.org/x/crypto/argon2 - For password HASHING, not generation
Note: You'll need a hashing library when STORING passwords, but for GENERATING them, use crypto/rand.
What You Have:
- ✅
public/config.html- Clean, custom HTML/CSS - ✅
internal/api/config_handler.go- REST API for config management - ✅
internal/gui/email_setup.go- Lorca-based desktop GUI - ✅ Separation of concerns
- ✅ No unnecessary dependencies
Verdict: Keep your current approach!
-
Simple and Maintainable
- ~500 lines of HTML
- Clean REST API
- Easy to understand
-
No Framework Bloat
- No heavy dependencies
- Fast loading
- Easy debugging
-
Flexible
- Can add JavaScript features as needed
- Can style however you want
- Not locked into framework patterns
Repository: https://github.com/GoAdminGroup/go-admin
- Heavy framework (50+ dependencies)
- Database-driven
- Overkill for simple config
- Full MVC framework
- Way too heavy
- Requires restructuring entire app
- Would require rewriting HTTP layer
- More complexity than benefit
Repository: https://github.com/spf13/viper
- Good for reading configs from multiple sources
- Supports YAML, JSON, TOML, env vars
- Has file watching capability
- Maybe useful if you want hot-reload
Example:
import "github.com/spf13/viper"
viper.SetConfigName("config")
viper.SetConfigType("json")
viper.AddConfigPath("$HOME/.webcam2")
viper.AutomaticEnv() // Read from env vars
if err := viper.ReadInConfig(); err != nil {
log.Fatal(err)
}
// Watch for changes
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
log.Println("Config file changed:", e.Name)
// Reload config
})Assessment: Optional, not necessary for your current needs.
What it is: JavaScript library for dynamic HTML updates Repository: https://htmx.org/
Why consider it:
- Add dynamic updates without full page reload
- No React/Vue complexity
- Works great with Go's html/template
- ~14kb library
Example:
<!-- Form with HTMX -->
<form hx-post="/api/config" hx-target="#result" hx-swap="innerHTML">
<input name="width" value="640">
<button type="submit">Save</button>
</form>
<div id="result"></div>// Go handler returns HTML fragment
func (h *ConfigHandler) UpdateConfig(w http.ResponseWriter, r *http.Request) {
// Update config...
// Return HTML fragment instead of JSON
w.Write([]byte(`<div class="alert success">Saved!</div>`))
}Assessment: Nice-to-have, not required.
Keep your current approach with these optional enhancements:
- Add password generation button (JavaScript + Go API endpoint)
- Add client-side validation (vanilla JavaScript)
- Consider HTMX for dynamic updates (optional)
- Fix autocomplete attributes (high priority)
-
✅ Fix autocomplete attributes in
config.html- Add
autocomplete="username"to username fields - Add
autocomplete="new-password"to password fields - Add
autocomplete="email"to email fields - Add
nameattributes to all inputs
- Add
-
✅ Implement password generation using crypto/rand
- Create
internal/password/generator.go - Add API endpoint
/api/generate-password - Add "Generate" button in config.html
- Create
-
✅ Add password hashing for stored credentials
- Use
golang.org/x/crypto/bcryptorgolang.org/x/crypto/argon2 - Hash WebRTC passwords before storing in config
- CRITICAL: Never store plain-text passwords
- Use
-
⚠️ Add client-side validation- Password strength indicator
- Form validation before submission
- Prevent invalid inputs
-
⚠️ Add password visibility toggle- Show/hide password buttons
- Better UX for long passwords
-
⚠️ Consider HTMX for better UX- Save without full page reload
- Real-time validation feedback
-
🔵 Add Viper if you need:
- Hot-reload of config files
- Environment variable overrides
- Multiple config formats (YAML, TOML, etc.)
-
🔵 Add password requirements UI
- Show requirements as user types
- Visual feedback (green checkmarks)
File: internal/api/password_handler.go
package api
import (
"encoding/json"
"net/http"
"github.com/mikeyg42/webcam/internal/password"
)
type GeneratePasswordRequest struct {
Length int `json:"length"`
IncludeSymbols bool `json:"includeSymbols"`
}
type GeneratePasswordResponse struct {
Password string `json:"password"`
Error string `json:"error,omitempty"`
}
func GeneratePassword(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req GeneratePasswordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
json.NewEncoder(w).Encode(GeneratePasswordResponse{
Error: "Invalid request",
})
return
}
// Default to 16 characters if not specified
if req.Length == 0 {
req.Length = 16
}
pass, err := password.Generate(password.Config{
Length: req.Length,
IncludeUpper: true,
IncludeLower: true,
IncludeDigits: true,
IncludeSymbols: req.IncludeSymbols,
MinDigits: 2,
MinSymbols: 2,
})
if err != nil {
json.NewEncoder(w).Encode(GeneratePasswordResponse{
Error: err.Error(),
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(GeneratePasswordResponse{
Password: pass,
})
}Add to config.html near password fields:
<div class="form-group">
<label>Password <span class="required">*</span></label>
<div style="display: flex; gap: 10px;">
<input
type="password"
id="webrtcPassword"
name="webrtcPassword"
autocomplete="new-password"
required
style="flex: 1;">
<button
type="button"
onclick="generatePassword('webrtcPassword')"
style="padding: 10px 20px;">
🔑 Generate
</button>
</div>
<small>Min 8 characters</small>
</div>
<script>
async function generatePassword(fieldId) {
try {
const response = await fetch('/api/generate-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ length: 16, includeSymbols: true })
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
return;
}
// Set password and switch to text briefly to show it
const field = document.getElementById(fieldId);
field.value = data.password;
field.type = 'text';
// Switch back to password after 2 seconds
setTimeout(() => {
field.type = 'password';
}, 2000);
} catch (error) {
alert('Failed to generate password: ' + error);
}
}
</script>Add to config.html:
<div class="form-group">
<label>Password <span class="required">*</span></label>
<div style="display: flex; gap: 10px; align-items: center;">
<input
type="password"
id="webrtcPassword"
name="webrtcPassword"
autocomplete="new-password"
required
style="flex: 1;">
<button
type="button"
onclick="togglePasswordVisibility('webrtcPassword')"
style="padding: 10px;">
👁️
</button>
<button
type="button"
onclick="generatePassword('webrtcPassword')"
style="padding: 10px 20px;">
🔑 Generate
</button>
</div>
</div>
<script>
function togglePasswordVisibility(fieldId) {
const field = document.getElementById(fieldId);
field.type = field.type === 'password' ? 'text' : 'password';
}
</script>❌ NEVER DO THIS:
// DON'T store plain-text passwords!
config.WebRTCPassword = "testing123"
saveToFile(config)✅ DO THIS:
import "golang.org/x/crypto/bcrypt"
// Hash password before storing
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
config.WebRTCPasswordHash = string(hashedPassword)
saveToFile(config)
// Verify password later
err := bcrypt.CompareHashAndPassword([]byte(config.WebRTCPasswordHash), []byte(inputPassword))
if err != nil {
// Password incorrect
}File: /Users/mikeglendinning/projects/webcam2/internal/api/config_handler.go
Lines 246-247:
Password: "", // Don't send password✅ Good! You're not sending passwords to frontend.
But lines 301-302:
if req.WebRTC.Password != "" {
cfg.WebrtcAuth.Password = req.WebRTC.Password
}Fix Required:
if req.WebRTC.Password != "" {
// Hash before storing
hashedPassword, err := bcrypt.GenerateFromPassword(
[]byte(req.WebRTC.Password),
bcrypt.DefaultCost,
)
if err != nil {
return err
}
cfg.WebrtcAuth.PasswordHash = string(hashedPassword)
}| Feature | Current Status | Recommendation | Priority |
|---|---|---|---|
| Autocomplete attributes | ❌ Missing | Add to all fields | HIGH |
| Name attributes | ❌ Missing | Add to all inputs | HIGH |
| Unique IDs | ✅ Good | Keep | - |
| Password generation | ❌ None | Implement with crypto/rand | HIGH |
| Password hashing | ❌ Plain-text | Use bcrypt/argon2 | CRITICAL |
| GUI framework | ✅ Custom | Keep current approach | - |
| Password visibility | ❌ None | Add toggle button | MEDIUM |
| Client validation | Enhance | MEDIUM | |
| HTMX | ❌ None | Consider for UX | LOW |
| Viper config | ❌ None | Optional | LOW |
-
Immediate (Security Critical):
- Add password hashing to config storage
- Never store plain-text passwords
-
High Priority:
- Fix autocomplete attributes in config.html
- Add name attributes to all form inputs
- Implement password generation API
-
Medium Priority:
- Add "Generate Password" buttons
- Add password visibility toggles
- Enhance client-side validation
-
Optional Enhancements:
- Consider HTMX for better UX
- Add password strength indicator
- Add real-time form validation
- Chromium Password Forms: https://www.chromium.org/developers/design-documents/create-amazing-password-forms/
- Autocomplete Values: https://www.chromium.org/developers/design-documents/form-styles-that-chromium-understands/
- Go crypto/rand: https://pkg.go.dev/crypto/rand
- Go bcrypt: https://pkg.go.dev/golang.org/x/crypto/bcrypt
- sethvargo/go-password: https://github.com/sethvargo/go-password
- HTMX: https://htmx.org/
- Web.dev Forms: https://web.dev/learn/forms/autofill/