168 lines
4.0 KiB
Go
168 lines
4.0 KiB
Go
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)
|
|
}
|