package storage import ( "context" "crypto/aes" "crypto/cipher" "crypto/rand" "database/sql" "encoding/base64" "errors" "fmt" "io" _ "github.com/lib/pq" "netisjwt/internal/statuslist" ) var ErrNotFound = errors.New("status list not found") type PostgresStore struct { db *sql.DB gc cipher.AEAD } // NewPostgresStore ustvari repozitorij s pripravljeno AES-GCM sifro. func NewPostgresStore(db *sql.DB, aesKeyBase64 string) (*PostgresStore, error) { key, err := base64.StdEncoding.DecodeString(aesKeyBase64) if err != nil { return nil, fmt.Errorf("invalid STATUS_AES_KEY_B64: %w", err) } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("invalid AES key: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } return &PostgresStore{db: db, gc: gcm}, nil } // PoveziInMigriraj odpre povezavo na bazo in izvede schema migracijo. func PoveziInMigriraj(ctx context.Context, dsn string, aesKeyBase64 string) (*PostgresStore, error) { db, err := sql.Open("postgres", dsn) if err != nil { return nil, err } if err := db.PingContext(ctx); err != nil { return nil, err } if _, err := db.ExecContext(ctx, SchemaSQL); err != nil { return nil, err } return NewPostgresStore(db, aesKeyBase64) } // Zapri zapre povezavo na podatkovno bazo. func (s *PostgresStore) Zapri() error { return s.db.Close() } // Ustvari shrani novo status listo. func (s *PostgresStore) Ustvari(ctx context.Context, statusID string, list *statuslist.StatusList) error { encrypted, err := s.sifriraj(list.Bajti()) if err != nil { return err } _, err = s.db.ExecContext( ctx, `INSERT INTO status_lists(status_id, bit_length, data_encrypted) VALUES ($1, $2, $3)`, statusID, list.DolzinaBitov(), encrypted, ) return err } // SeznamIDjev vrne vse shranjene id-je seznamov. func (s *PostgresStore) SeznamIDjev(ctx context.Context) ([]string, error) { rows, err := s.db.QueryContext(ctx, `SELECT status_id FROM status_lists ORDER BY created_at ASC`) if err != nil { return nil, err } defer rows.Close() out := make([]string, 0) for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } out = append(out, id) } return out, rows.Err() } // Dobi prebere status listo po statusID. func (s *PostgresStore) Dobi(ctx context.Context, statusID string) (*statuslist.StatusList, error) { var bitLength int var encrypted []byte err := s.db.QueryRowContext( ctx, `SELECT bit_length, data_encrypted FROM status_lists WHERE status_id = $1`, statusID, ).Scan(&bitLength, &encrypted) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } raw, err := s.desifriraj(encrypted) if err != nil { return nil, err } return statuslist.IzSurovihPodatkov(raw, bitLength) } // Posodobi prepiše obstoječo status listo. func (s *PostgresStore) Posodobi(ctx context.Context, statusID string, list *statuslist.StatusList) error { encrypted, err := s.sifriraj(list.Bajti()) if err != nil { return err } res, err := s.db.ExecContext( ctx, `UPDATE status_lists SET bit_length = $2, data_encrypted = $3 WHERE status_id = $1`, statusID, list.DolzinaBitov(), encrypted, ) if err != nil { return err } affected, err := res.RowsAffected() if err != nil { return err } if affected == 0 { return ErrNotFound } return nil } // sifriraj zasciti bajte z AES-GCM in pripne nonce. func (s *PostgresStore) sifriraj(plain []byte) ([]byte, error) { nonce := make([]byte, s.gc.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, err } ciphertext := s.gc.Seal(nil, nonce, plain, nil) return append(nonce, ciphertext...), nil } // desifriraj obnovi originalne bajte iz AES-GCM payloada. func (s *PostgresStore) desifriraj(encrypted []byte) ([]byte, error) { nonceSize := s.gc.NonceSize() if len(encrypted) < nonceSize { return nil, errors.New("encrypted payload too short") } nonce := encrypted[:nonceSize] ciphertext := encrypted[nonceSize:] return s.gc.Open(nil, nonce, ciphertext, nil) }