This commit is contained in:
h4nz4
2026-03-12 20:09:16 +01:00
commit f9b7eaa20d
23 changed files with 1481 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
package storage
const SchemaSQL = `
CREATE TABLE IF NOT EXISTS status_lists (
status_id TEXT PRIMARY KEY,
bit_length INTEGER NOT NULL,
data_encrypted BYTEA NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
`

View File

@@ -0,0 +1,167 @@
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)
}

14
internal/storage/store.go Normal file
View File

@@ -0,0 +1,14 @@
package storage
import (
"context"
"netisjwt/internal/statuslist"
)
type Store interface {
Ustvari(ctx context.Context, statusID string, list *statuslist.StatusList) error
SeznamIDjev(ctx context.Context) ([]string, error)
Dobi(ctx context.Context, statusID string) (*statuslist.StatusList, error)
Posodobi(ctx context.Context, statusID string, list *statuslist.StatusList) error
}