package storage import ( "context" "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/base64" "errors" "fmt" "io" "os" "path/filepath" "strconv" "strings" "time" ) // ErrURLExpired wird von VerifySignedURL geliefert, wenn eine signierte URL // nach Ablauf ihrer Gueltigkeit verwendet wird (Pruefung 2). var ErrURLExpired = errors.New("storage: signierte url ist abgelaufen") // ErrInvalidSignature wird geliefert, wenn die Signatur einer URL nicht zum // Schluessel passt (manipulierte oder falsche URL). var ErrInvalidSignature = errors.New("storage: signatur der url ist ungueltig") // LocalDriver legt Objekte im lokalen Dateisystem ab — der Entwicklungs- // Treiber (Akzeptanzkriterium 1), keine externe Abhaengigkeit noetig. type LocalDriver struct { baseDir string signingSecret []byte publicBaseURL string } // NewLocalDriver erzeugt einen LocalDriver. signingSecret authentifiziert // die von SignedURL ausgestellten URLs (HMAC-SHA256, konstant-zeit- // verglichen bei der Verifikation — timing-safe wie projektweite Konvention, // siehe Core IAM-15). func NewLocalDriver(baseDir string, signingSecret []byte, publicBaseURL string) *LocalDriver { return &LocalDriver{baseDir: baseDir, signingSecret: signingSecret, publicBaseURL: publicBaseURL} } func (d *LocalDriver) path(key string) string { return filepath.Join(d.baseDir, filepath.FromSlash(key)) } func (d *LocalDriver) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) (int64, error) { full := d.path(key) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { return 0, fmt.Errorf("storage: verzeichnis anlegen: %w", err) } f, err := os.Create(full) if err != nil { return 0, fmt.Errorf("storage: datei anlegen: %w", err) } defer func() { _ = f.Close() }() written, err := io.Copy(f, r) if err != nil { return 0, fmt.Errorf("storage: schreiben: %w", err) } return written, nil } func (d *LocalDriver) Get(ctx context.Context, key string) (io.ReadCloser, error) { f, err := os.Open(d.path(key)) if err != nil { if os.IsNotExist(err) { return nil, ErrNotFound } return nil, fmt.Errorf("storage: lesen: %w", err) } return f, nil } func (d *LocalDriver) Delete(ctx context.Context, key string) error { if err := os.Remove(d.path(key)); err != nil { if os.IsNotExist(err) { return ErrNotFound } return fmt.Errorf("storage: loeschen: %w", err) } return nil } func (d *LocalDriver) SignedURL(ctx context.Context, key string, ttl time.Duration) (string, error) { expiry := time.Now().Add(ttl).Unix() sig := d.sign(key, expiry) return fmt.Sprintf("%s/%s?exp=%d&sig=%s", strings.TrimRight(d.publicBaseURL, "/"), key, expiry, sig), nil } func (d *LocalDriver) sign(key string, expiry int64) string { mac := hmac.New(sha256.New, d.signingSecret) mac.Write([]byte(key)) mac.Write([]byte(strconv.FormatInt(expiry, 10))) return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } // VerifySignedURL prueft key/expiry/sig, wie sie z.B. aus den Query- // Parametern einer von SignedURL ausgestellten URL stammen (Pruefung 2: // abgelaufene URL wird abgewiesen). Die eigentliche HTTP-Auslieferung ist // nicht Bestandteil dieser Kachel (siehe DOC-01) — hier wird nur die // Signatur-/Ablauflogik bereitgestellt und getestet. func (d *LocalDriver) VerifySignedURL(key string, expiry int64, sig string) error { expected := d.sign(key, expiry) if subtle.ConstantTimeCompare([]byte(expected), []byte(sig)) != 1 { return ErrInvalidSignature } if time.Now().Unix() > expiry { return ErrURLExpired } return nil }