Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
174 lines
5.9 KiB
Go
174 lines
5.9 KiB
Go
// Package crypto stellt die kryptografischen Grundbausteine von Syncova bereit:
|
|
// Passwort-Hashing, verschlüsselte Ablage von Secrets und Token-Erzeugung.
|
|
//
|
|
// Grundsätze (PROMPT.md §12, §41):
|
|
// - Passwörter werden niemals im Klartext gespeichert oder geloggt.
|
|
// - Vergleiche laufen in konstanter Zeit, damit sie keine Rückschlüsse erlauben.
|
|
// - Verwendete Parameter wandern mit dem Hash, damit sie später verschärft
|
|
// werden können, ohne bestehende Anmeldungen zu brechen.
|
|
package crypto
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
// Argon2id-Parameter.
|
|
//
|
|
// Die Werte orientieren sich an den OWASP-Empfehlungen (64 MiB Speicher,
|
|
// 3 Durchgänge). Speicherbedarf ist hier die wirksamste Verteidigung, weil er
|
|
// Angriffe mit Grafikkarten und Spezialhardware unwirtschaftlich macht.
|
|
const (
|
|
// argon2Time ist die Anzahl der Durchgänge.
|
|
argon2Time uint32 = 3
|
|
// argon2Memory ist der Speicherbedarf in KiB (64 MiB).
|
|
argon2Memory uint32 = 64 * 1024
|
|
// argon2Threads ist der Parallelitätsgrad.
|
|
argon2Threads uint8 = 4
|
|
// argon2KeyLength ist die Länge des erzeugten Hashes in Byte.
|
|
argon2KeyLength uint32 = 32
|
|
// argon2SaltLength ist die Länge des Zufallssalzes in Byte.
|
|
argon2SaltLength uint32 = 16
|
|
)
|
|
|
|
// ErrPasswordMismatch meldet, dass ein Passwort nicht zum Hash passt.
|
|
//
|
|
// Der Fehler ist bewusst unspezifisch: er unterscheidet nicht zwischen
|
|
// "falsches Passwort" und "unbekannter Benutzer", damit die API keine
|
|
// Benutzernamen preisgibt.
|
|
var ErrPasswordMismatch = errors.New("passwort stimmt nicht überein")
|
|
|
|
// ErrInvalidPasswordHash meldet einen beschädigten oder unbekannten Hash.
|
|
var ErrInvalidPasswordHash = errors.New("passwort-hash ist ungültig oder wurde mit einem unbekannten Verfahren erzeugt")
|
|
|
|
// HashPassword erzeugt einen Argon2id-Hash im PHC-Format.
|
|
//
|
|
// Das Ergebnis enthält Verfahren, Version, Parameter, Salz und Hash:
|
|
//
|
|
// $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
|
|
//
|
|
// Dadurch bleibt ein alter Hash prüfbar, auch nachdem die Parameter für neue
|
|
// Passwörter verschärft wurden.
|
|
func HashPassword(plainPassword string) (string, error) {
|
|
// Ein leeres Passwort wäre eine stillschweigend wirkungslose Absicherung.
|
|
if plainPassword == "" {
|
|
return "", errors.New("das passwort darf nicht leer sein")
|
|
}
|
|
|
|
passwordSalt := make([]byte, argon2SaltLength)
|
|
if _, randomError := rand.Read(passwordSalt); randomError != nil {
|
|
return "", fmt.Errorf("es konnte kein sicheres salz erzeugt werden: %w", randomError)
|
|
}
|
|
|
|
passwordHash := argon2.IDKey([]byte(plainPassword), passwordSalt, argon2Time, argon2Memory, argon2Threads, argon2KeyLength)
|
|
|
|
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
|
argon2.Version,
|
|
argon2Memory,
|
|
argon2Time,
|
|
argon2Threads,
|
|
base64.RawStdEncoding.EncodeToString(passwordSalt),
|
|
base64.RawStdEncoding.EncodeToString(passwordHash),
|
|
), nil
|
|
}
|
|
|
|
// VerifyPassword prüft ein Passwort gegen einen gespeicherten Hash.
|
|
//
|
|
// Der Vergleich läuft in konstanter Zeit: eine von der Übereinstimmung abhängige
|
|
// Laufzeit würde einem Angreifer verraten, wie viele Zeichen bereits stimmen.
|
|
func VerifyPassword(plainPassword string, encodedHash string) error {
|
|
hashParameters, passwordSalt, expectedHash, parseError := decodePasswordHash(encodedHash)
|
|
if parseError != nil {
|
|
return parseError
|
|
}
|
|
|
|
computedHash := argon2.IDKey(
|
|
[]byte(plainPassword),
|
|
passwordSalt,
|
|
hashParameters.time,
|
|
hashParameters.memory,
|
|
hashParameters.threads,
|
|
uint32(len(expectedHash)),
|
|
)
|
|
|
|
if subtle.ConstantTimeCompare(computedHash, expectedHash) != 1 {
|
|
return ErrPasswordMismatch
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NeedsRehash meldet, ob ein Hash mit schwächeren Parametern als den aktuellen
|
|
// erzeugt wurde.
|
|
//
|
|
// Damit lässt sich ein Passwort bei der nächsten erfolgreichen Anmeldung
|
|
// stillschweigend auf die aktuellen Parameter heben.
|
|
func NeedsRehash(encodedHash string) bool {
|
|
hashParameters, _, _, parseError := decodePasswordHash(encodedHash)
|
|
if parseError != nil {
|
|
// Ein unlesbarer Hash muss in jedem Fall ersetzt werden.
|
|
return true
|
|
}
|
|
|
|
return hashParameters.time < argon2Time ||
|
|
hashParameters.memory < argon2Memory ||
|
|
hashParameters.threads < argon2Threads
|
|
}
|
|
|
|
// argon2Parameters bündelt die aus einem Hash gelesenen Parameter.
|
|
type argon2Parameters struct {
|
|
// memory ist der Speicherbedarf in KiB.
|
|
memory uint32
|
|
// time ist die Anzahl der Durchgänge.
|
|
time uint32
|
|
// threads ist der Parallelitätsgrad.
|
|
threads uint8
|
|
}
|
|
|
|
// decodePasswordHash zerlegt einen Hash im PHC-Format.
|
|
func decodePasswordHash(encodedHash string) (argon2Parameters, []byte, []byte, error) {
|
|
// Erwartet werden sechs Teile: leer, Verfahren, Version, Parameter, Salz, Hash.
|
|
hashParts := strings.Split(encodedHash, "$")
|
|
if len(hashParts) != 6 {
|
|
return argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
|
}
|
|
|
|
if hashParts[1] != "argon2id" {
|
|
return argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
|
}
|
|
|
|
var encodedVersion int
|
|
if _, scanError := fmt.Sscanf(hashParts[2], "v=%d", &encodedVersion); scanError != nil {
|
|
return argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
|
}
|
|
|
|
// Eine abweichende Argon2-Version würde einen anderen Hash liefern.
|
|
if encodedVersion != argon2.Version {
|
|
return argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
|
}
|
|
|
|
var hashParameters argon2Parameters
|
|
if _, scanError := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d",
|
|
&hashParameters.memory, &hashParameters.time, &hashParameters.threads); scanError != nil {
|
|
return argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
|
}
|
|
|
|
passwordSalt, saltError := base64.RawStdEncoding.DecodeString(hashParts[4])
|
|
if saltError != nil {
|
|
return argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
|
}
|
|
|
|
expectedHash, hashError := base64.RawStdEncoding.DecodeString(hashParts[5])
|
|
if hashError != nil || len(expectedHash) == 0 {
|
|
return argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
|
}
|
|
|
|
return hashParameters, passwordSalt, expectedHash, nil
|
|
}
|