qsl designer
This commit is contained in:
+169
@@ -0,0 +1,169 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"hamlog/internal/applog"
|
||||
"hamlog/internal/secret"
|
||||
)
|
||||
|
||||
// Secret-vault settings keys (NOT themselves sensitive — they hold the salt and
|
||||
// an encrypted verifier token, never a password).
|
||||
const (
|
||||
keySecretSalt = "secret.salt" // base64 PBKDF2 salt
|
||||
keySecretVerifier = "secret.verifier" // enc:v1: token, validates the passphrase
|
||||
)
|
||||
|
||||
// sensitiveSettingKeys are the password fields encrypted at rest when the user
|
||||
// sets a passphrase. Everything else stays plaintext.
|
||||
var sensitiveSettingKeys = map[string]bool{
|
||||
keyQRZPassword: true,
|
||||
keyHQPassword: true,
|
||||
keyEmailPassword: true,
|
||||
keyExtClublogPassword: true,
|
||||
keyExtLoTWKeyPassword: true,
|
||||
keyExtLoTWWebPassword: true,
|
||||
}
|
||||
|
||||
func isSensitiveSetting(key string) bool { return sensitiveSettingKeys[key] }
|
||||
|
||||
// SecretStatus tells the UI whether a passphrase is configured and unlocked.
|
||||
type SecretStatus struct {
|
||||
HasPassphrase bool `json:"has_passphrase"`
|
||||
Unlocked bool `json:"unlocked"`
|
||||
}
|
||||
|
||||
// GetSecretStatus is polled at launch: if HasPassphrase && !Unlocked the UI
|
||||
// shows the unlock prompt.
|
||||
func (a *App) GetSecretStatus() SecretStatus {
|
||||
if a.settings == nil {
|
||||
return SecretStatus{}
|
||||
}
|
||||
salt, _ := a.settings.GetRaw(a.ctx, keySecretSalt)
|
||||
return SecretStatus{HasPassphrase: salt != "", Unlocked: a.settings.Unlocked()}
|
||||
}
|
||||
|
||||
// reloadAfterSecretChange rebuilds anything that read passwords at startup while
|
||||
// they were still locked (lookup providers + external-service config).
|
||||
func (a *App) reloadAfterSecretChange() {
|
||||
a.reloadLookupProviders()
|
||||
if a.extsvc != nil {
|
||||
a.extsvc.SetConfig(a.loadExternalServices())
|
||||
}
|
||||
}
|
||||
|
||||
// UnlockSecrets validates the passphrase against the stored verifier and, on
|
||||
// success, installs the cipher so passwords decrypt for the rest of the session.
|
||||
func (a *App) UnlockSecrets(passphrase string) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("not initialized")
|
||||
}
|
||||
saltB64, _ := a.settings.GetRaw(a.ctx, keySecretSalt)
|
||||
verifier, _ := a.settings.GetRaw(a.ctx, keySecretVerifier)
|
||||
if saltB64 == "" || verifier == "" {
|
||||
return fmt.Errorf("no passphrase configured")
|
||||
}
|
||||
salt, err := base64.StdEncoding.DecodeString(saltB64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("corrupt salt")
|
||||
}
|
||||
key, err := secret.DeriveKey(passphrase, salt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("derive key: %w", err)
|
||||
}
|
||||
c, err := secret.New(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cipher: %w", err)
|
||||
}
|
||||
if !c.CheckVerifier(verifier) {
|
||||
return fmt.Errorf("wrong passphrase")
|
||||
}
|
||||
a.settings.Unlock(c)
|
||||
a.reloadAfterSecretChange()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPassphrase sets a new passphrase or changes an existing one, then
|
||||
// re-encrypts every stored secret under the new key. Changing an existing
|
||||
// passphrase requires the vault to be unlocked first (so current secrets are
|
||||
// readable as plaintext).
|
||||
func (a *App) SetPassphrase(passphrase string) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("not initialized")
|
||||
}
|
||||
if len(passphrase) < 4 {
|
||||
return fmt.Errorf("passphrase too short (min 4 characters)")
|
||||
}
|
||||
saltB64, _ := a.settings.GetRaw(a.ctx, keySecretSalt)
|
||||
if saltB64 != "" && !a.settings.Unlocked() {
|
||||
return fmt.Errorf("unlock with the current passphrase first")
|
||||
}
|
||||
// Snapshot current plaintext (Get decrypts when unlocked, passes through
|
||||
// when no passphrase yet).
|
||||
current := map[string]string{}
|
||||
for k := range sensitiveSettingKeys {
|
||||
if v, _ := a.settings.Get(a.ctx, k); v != "" {
|
||||
current[k] = v
|
||||
}
|
||||
}
|
||||
salt, err := secret.NewSalt()
|
||||
if err != nil {
|
||||
return fmt.Errorf("salt: %w", err)
|
||||
}
|
||||
key, err := secret.DeriveKey(passphrase, salt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("derive key: %w", err)
|
||||
}
|
||||
c, err := secret.New(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cipher: %w", err)
|
||||
}
|
||||
// Persist salt + verifier first so an interrupted migration is still
|
||||
// recoverable (the prompt appears next launch; plaintext secrets read
|
||||
// through transparently until re-encrypted).
|
||||
if err := a.settings.SetRaw(a.ctx, keySecretSalt, base64.StdEncoding.EncodeToString(salt)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.settings.SetRaw(a.ctx, keySecretVerifier, c.MakeVerifier()); err != nil {
|
||||
return err
|
||||
}
|
||||
a.settings.Unlock(c)
|
||||
for k, v := range current {
|
||||
if err := a.settings.Set(a.ctx, k, v); err != nil { // now encrypts
|
||||
applog.Printf("secret: re-encrypt %q failed: %v", k, err)
|
||||
}
|
||||
}
|
||||
a.reloadAfterSecretChange()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePassphrase decrypts all secrets back to plaintext and clears the vault.
|
||||
// Requires the current passphrase (validated) so it can't be cleared blindly.
|
||||
func (a *App) RemovePassphrase(passphrase string) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("not initialized")
|
||||
}
|
||||
if !a.settings.Unlocked() {
|
||||
if err := a.UnlockSecrets(passphrase); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Read decrypted, then store back as plaintext and drop salt/verifier.
|
||||
plain := map[string]string{}
|
||||
for k := range sensitiveSettingKeys {
|
||||
if v, _ := a.settings.Get(a.ctx, k); v != "" {
|
||||
plain[k] = v
|
||||
}
|
||||
}
|
||||
a.settings.Lock()
|
||||
for k, v := range plain {
|
||||
if err := a.settings.SetRaw(a.ctx, k, v); err != nil {
|
||||
applog.Printf("secret: decrypt-on-remove %q failed: %v", k, err)
|
||||
}
|
||||
}
|
||||
_ = a.settings.SetRaw(a.ctx, keySecretSalt, "")
|
||||
_ = a.settings.SetRaw(a.ctx, keySecretVerifier, "")
|
||||
a.reloadAfterSecretChange()
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user