Ctrl K
ComunidadGuías…

internal/store/postgres/authorization.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package postgres

import (
	"context"
	"crypto/sha256"
	"encoding/binary"
	"errors"
	"fmt"

	"github.com/jackc/pgx/v5"

	"telesrv/internal/domain"
	"telesrv/internal/store"
	"telesrv/internal/store/postgres/sqlcgen"
)

// AuthorizationStore implements store.AuthorizationStore with PostgreSQL.
type AuthorizationStore struct {
	db sqlcgen.DBTX
	q  *sqlcgen.Queries
}

// NewAuthorizationStore creates an AuthorizationStore backed by a pgx pool (or transaction).
func NewAuthorizationStore(db sqlcgen.DBTX) *AuthorizationStore {
	return &AuthorizationStore{db: db, q: sqlcgen.New(db)}
}

func (s *AuthorizationStore) Bind(ctx context.Context, a domain.Authorization) error {
	if a.Hash == 0 {
		a.Hash = authorizationHash(a.AuthKeyID)
	}
	err := withAuthIdentityTx(ctx, s.db, "bind authorization", func(tx pgx.Tx) error {
		return bindAuthorization(ctx, tx, a)
	})
	if err != nil {
		return fmt.Errorf("upsert authorization: %w", err)
	}
	return nil
}

// bindAuthorization commits the auth_key→user binding and the device update baseline as one state boundary.
//
// The lock order is fixed: target user advisory/row → auth_keys parent row →
// user_update_watermarks → user_update_retention → target update_states. The order of the watermark
// and retention row locks matches pruneConfirmedUserPrefixTx, so the new authorization's
// observed baseline and retained floor cannot cross-commit into a silent hole. The parent-row
// lock also serializes concurrent logins/account switches on the same raw auth key while the
// first authorization does not yet exist. The user lock uses the same order as the account
// tombstone; therefore Bind either commits first and is undone by a subsequent delete
// transaction, or waits for the delete to commit and then sees the tombstone and rejects it. It
// cannot rebind the account after the delete transaction has enumerated authorizations.
func bindAuthorization(ctx context.Context, db sqlcgen.DBTX, a domain.Authorization) error {
	keyID := authKeyIDToInt64(a.AuthKeyID)
	tx, ok := db.(pgx.Tx)
	if !ok {
		return fmt.Errorf("bind authorization requires a transaction")
	}
	if err := lockUsersForUpdate(ctx, tx, a.UserID); err != nil {
		return fmt.Errorf("lock authorization user: %w", err)
	}
	var active bool
	if err := db.QueryRow(ctx, `
SELECT deleted_at IS NULL
FROM users
WHERE id = $1
FOR UPDATE`, a.UserID).Scan(&active); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.ErrUserNotFound
		}
		return fmt.Errorf("lock authorization user row: %w", err)
	}
	if !active {
		return domain.ErrAccountDeleted
	}
	if err := lockPermanentAuthIdentities(ctx, tx, []int64{keyID}); err != nil {
		return err
	}
	var (
		lockedKeyID        int64
		expiresAt          int
		authLayer          int
		layerObservationID int64
	)
	if err := db.QueryRow(ctx, `
SELECT auth_key_id, expires_at, layer, layer_observation_id
FROM auth_keys
WHERE auth_key_id = $1
FOR UPDATE`, keyID).Scan(&lockedKeyID, &expiresAt, &authLayer, &layerObservationID); err != nil {
		return fmt.Errorf("lock auth key for authorization: %w", err)
	}
	if expiresAt != 0 {
		return store.ErrAuthKeyNotPermanent
	}
	if authLayer < 0 || layerObservationID < 0 || (layerObservationID > 0 && authLayer == 0) {
		return fmt.Errorf(
			"authorization auth-key layer invariant violation: auth key %x has layer %d observation %d",
			a.AuthKeyID, authLayer, layerObservationID,
		)
	}

	if _, err := db.Exec(ctx, `
INSERT INTO user_update_watermarks (user_id, contiguous_pts)
VALUES ($1, 0)
ON CONFLICT (user_id) DO NOTHING`, a.UserID); err != nil {
		return fmt.Errorf("ensure authorization user update watermark: %w", err)
	}
	var currentPts int
	if err := db.QueryRow(ctx, `
SELECT contiguous_pts
FROM user_update_watermarks
WHERE user_id = $1
FOR UPDATE`, a.UserID).Scan(&currentPts); err != nil {
		return fmt.Errorf("lock authorization user update watermark: %w", err)
	}
	if _, err := db.Exec(ctx, `
INSERT INTO user_update_retention (user_id)
VALUES ($1)
ON CONFLICT (user_id) DO NOTHING`, a.UserID); err != nil {
		return fmt.Errorf("ensure authorization user update retention: %w", err)
	}
	var retainedFloor int
	if err := db.QueryRow(ctx, `
SELECT retained_through_pts
FROM user_update_retention
WHERE user_id = $1
FOR UPDATE`, a.UserID).Scan(&retainedFloor); err != nil {
		return fmt.Errorf("lock authorization user update retention: %w", err)
	}
	if retainedFloor > currentPts {
		return fmt.Errorf(
			"authorization update baseline invariant violation: user %d retained floor %d exceeds contiguous watermark %d",
			a.UserID, retainedFloor, currentPts,
		)
	}

	// Every Bind is an explicit login baseline: delivered pts advance to the locked account's contiguous watermark;
	// observed advances only to the deleted retained floor, not disguising the live tail as client acknowledgement.
	// A legacy state exceeding the account's contiguous watermark must fail fast; GREATEST must not
	// be used to preserve an illegal future cursor. The WHERE also closes the "concurrent insert after pre-check" race.
	tag, err := db.Exec(ctx, `
INSERT INTO update_states (auth_key_id, user_id, pts, qts, date, seq, observed_pts)
VALUES ($1, $2, $3, 0, EXTRACT(EPOCH FROM now())::int, 0, $4)
ON CONFLICT (auth_key_id, user_id) DO UPDATE SET
  pts = GREATEST(update_states.pts, EXCLUDED.pts),
  qts = GREATEST(update_states.qts, EXCLUDED.qts),
  date = GREATEST(update_states.date, EXCLUDED.date),
  seq = GREATEST(update_states.seq, EXCLUDED.seq),
  observed_pts = GREATEST(update_states.observed_pts, EXCLUDED.observed_pts),
  updated_at = now()
WHERE update_states.pts >= 0
  AND update_states.pts <= $3
  AND update_states.observed_pts <= $3`, keyID, a.UserID, currentPts, retainedFloor)
	if err != nil {
		return fmt.Errorf("upsert authorization update baseline: %w", err)
	}
	if tag.RowsAffected() != 1 {
		return fmt.Errorf(
			"authorization update baseline invariant violation: auth key %x user %d has pts or observed_pts outside contiguous watermark %d",
			a.AuthKeyID, a.UserID, currentPts,
		)
	}

	if _, err := db.Exec(ctx, `
DELETE FROM update_states
WHERE auth_key_id = $1
  AND user_id <> $2`, keyID, a.UserID); err != nil {
		return fmt.Errorf("delete stale cross-user update states: %w", err)
	}

	if _, err := db.Exec(ctx, `
INSERT INTO authorizations (auth_key_id, user_id, hash, layer, device_model, platform, system_version, api_id, app_version, ip, password_pending)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
ON CONFLICT (auth_key_id) DO UPDATE SET
  user_id = EXCLUDED.user_id,
  hash = EXCLUDED.hash,
  layer = EXCLUDED.layer,
  device_model = EXCLUDED.device_model,
  platform = EXCLUDED.platform,
  system_version = EXCLUDED.system_version,
  api_id = EXCLUDED.api_id,
  app_version = EXCLUDED.app_version,
  ip = EXCLUDED.ip,
  password_pending = EXCLUDED.password_pending,
	created_at = now(),
  active_at = now()`,
		keyID, a.UserID, a.Hash, int32(authLayer), a.DeviceModel, a.Platform, a.SystemVersion, int32(a.APIID), a.AppVersion, a.IP, a.PasswordPending,
	); err != nil {
		return fmt.Errorf("write authorization: %w", err)
	}
	return nil
}

func (s *AuthorizationStore) ByAuthKey(ctx context.Context, id [8]byte) (domain.Authorization, bool, error) {
	row := s.db.QueryRow(ctx, `
SELECT user_id, hash, layer, device_model, platform, system_version, api_id, app_version, ip, password_pending, created_at, active_at
FROM authorizations WHERE auth_key_id = $1`, authKeyIDToInt64(id))
	a := domain.Authorization{AuthKeyID: id}
	if err := row.Scan(
		&a.UserID, &a.Hash, &a.Layer, &a.DeviceModel, &a.Platform, &a.SystemVersion,
		&a.APIID, &a.AppVersion, &a.IP, &a.PasswordPending, &a.CreatedAt, &a.ActiveAt,
	); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.Authorization{}, false, nil
		}
		return domain.Authorization{}, false, fmt.Errorf("get authorization: %w", err)
	}
	return a, true, nil
}

func (s *AuthorizationStore) UpdateClientInfo(ctx context.Context, id [8]byte, info domain.AuthKeyClientInfo) error {
	if _, err := s.db.Exec(ctx, `
UPDATE authorizations SET
  layer = CASE WHEN $2 > 0 THEN $2 ELSE layer END,
  device_model = CASE WHEN $3 <> '' THEN $3 ELSE device_model END,
  platform = CASE WHEN $4 <> '' THEN $4 ELSE platform END,
  system_version = CASE WHEN $5 <> '' THEN $5 ELSE system_version END,
  api_id = CASE WHEN $6 <> 0 THEN $6 ELSE api_id END,
  app_version = CASE WHEN $7 <> '' THEN $7 ELSE app_version END,
  ip = CASE WHEN $8 <> '' THEN $8 ELSE ip END,
  active_at = now()
WHERE auth_key_id = $1`,
		authKeyIDToInt64(id), int32(info.Layer), info.DeviceModel, info.Platform,
		info.SystemVersion, int32(info.APIID), info.AppVersion, info.IP,
	); err != nil {
		return fmt.Errorf("update authorization client info: %w", err)
	}
	return nil
}

// MarkPasswordPassed atomically promotes only the pending identity whose
// password was just verified. A concurrent cross-user Bind must not let A's
// proof clear B's password_pending flag.
func (s *AuthorizationStore) MarkPasswordPassed(ctx context.Context, id [8]byte, expectedUserID int64) error {
	tag, err := s.db.Exec(ctx, `
	UPDATE authorizations SET password_pending = false, created_at = now(), active_at = now()
	WHERE auth_key_id = $1 AND user_id = $2 AND password_pending`, authKeyIDToInt64(id), expectedUserID)
	if err != nil {
		return fmt.Errorf("mark authorization password passed: %w", err)
	}
	if tag.RowsAffected() != 1 {
		return store.ErrAuthorizationStateChanged
	}
	return nil
}

func (s *AuthorizationStore) ListByUser(ctx context.Context, userID int64) ([]domain.Authorization, error) {
	rows, err := s.q.ListAuthorizationsByUser(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("list authorizations by user: %w", err)
	}
	out := make([]domain.Authorization, 0, len(rows))
	for _, row := range rows {
		out = append(out, authorizationFromRow(row))
	}
	return out, nil
}

func (s *AuthorizationStore) Delete(ctx context.Context, id [8]byte) error {
	if err := s.q.DeleteAuthorization(ctx, authKeyIDToInt64(id)); err != nil {
		return fmt.Errorf("delete authorization: %w", err)
	}
	return nil
}

func (s *AuthorizationStore) DeleteByHash(ctx context.Context, userID, hash int64) (domain.Authorization, bool, error) {
	row := s.db.QueryRow(ctx, `
DELETE FROM authorizations
WHERE user_id = $1 AND hash = $2
RETURNING auth_key_id, user_id, hash, layer, device_model, platform, system_version, api_id, app_version, ip, created_at, active_at`, userID, hash)
	var a domain.Authorization
	var authKeyID int64
	if err := row.Scan(
		&authKeyID, &a.UserID, &a.Hash, &a.Layer, &a.DeviceModel, &a.Platform, &a.SystemVersion,
		&a.APIID, &a.AppVersion, &a.IP, &a.CreatedAt, &a.ActiveAt,
	); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.Authorization{}, false, nil
		}
		return domain.Authorization{}, false, fmt.Errorf("delete authorization by hash: %w", err)
	}
	a.AuthKeyID = authKeyIDFromInt64(authKeyID)
	return a, true, nil
}

// RevokeByHash is the persistence entry point for remote device kick: it deletes the business authorization and
// the device update state, but keeps the permanent/temp protocol key and binding. This way a kicked client
// can still complete MTProto decryption after reconnecting and gets AUTH_KEY_UNREGISTERED from the RPC gate; if
// the protocol key were deleted first, the client would only get a transport -404 and could not reliably clear its local login state.
func (s *AuthorizationStore) RevokeByHash(ctx context.Context, userID, hash int64) (domain.Authorization, bool, error) {
	var (
		a     domain.Authorization
		found bool
	)
	err := withAuthIdentityTx(ctx, s.db, "revoke authorization by hash", func(tx pgx.Tx) error {
		var err error
		a, found, err = revokeByHashTx(ctx, tx, userID, hash)
		return err
	})
	if err != nil {
		return domain.Authorization{}, false, err
	}
	return a, found, nil
}

// revokeByHashTx deliberately uses separate READ COMMITTED statements. The first
// lookup is only a candidate. Bind locks auth_keys before changing authorization
// ownership, so revocation must lock the same parent row and then re-read the
// owner/hash from a fresh statement snapshot. Otherwise an A->B re-login that
// commits while revoke waits can be deleted using A's stale target snapshot.
func revokeByHashTx(ctx context.Context, tx pgx.Tx, userID, hash int64) (domain.Authorization, bool, error) {
	var candidate int64
	if err := tx.QueryRow(ctx, `
SELECT auth_key_id
FROM authorizations
WHERE user_id = $1 AND hash = $2`, userID, hash).Scan(&candidate); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.Authorization{}, false, nil
		}
		return domain.Authorization{}, false, fmt.Errorf("select revoke candidate by hash: %w", err)
	}
	if err := lockPermanentAuthIdentities(ctx, tx, []int64{candidate}); err != nil {
		return domain.Authorization{}, false, err
	}

	var locked int64
	if err := tx.QueryRow(ctx, `
SELECT auth_key_id
FROM auth_keys
WHERE auth_key_id = $1
FOR UPDATE`, candidate).Scan(&locked); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.Authorization{}, false, nil
		}
		return domain.Authorization{}, false, fmt.Errorf("lock revoke auth key by hash: %w", err)
	}

	a, found, err := scanRevokedAuthorization(tx.QueryRow(ctx, `
SELECT auth_key_id, user_id, hash, layer, device_model, platform, system_version,
       api_id, app_version, ip, password_pending, created_at, active_at
FROM authorizations
WHERE auth_key_id = $1 AND user_id = $2 AND hash = $3
FOR UPDATE`, candidate, userID, hash))
	if err != nil {
		return domain.Authorization{}, false, fmt.Errorf("revalidate revoke authorization by hash: %w", err)
	}
	if !found {
		return domain.Authorization{}, false, nil
	}
	if err := deleteRevokedAuthorizationStateTx(ctx, tx, []int64{candidate}); err != nil {
		return domain.Authorization{}, false, err
	}
	return a, true, nil
}

func (s *AuthorizationStore) DeleteByUserExcept(ctx context.Context, userID int64, keepAuthKeyID [8]byte) ([]domain.Authorization, error) {
	rows, err := s.db.Query(ctx, `
DELETE FROM authorizations
WHERE user_id = $1 AND auth_key_id <> $2
RETURNING auth_key_id, user_id, hash, layer, device_model, platform, system_version, api_id, app_version, ip, created_at, active_at`, userID, authKeyIDToInt64(keepAuthKeyID))
	if err != nil {
		return nil, fmt.Errorf("delete authorizations by user: %w", err)
	}
	defer rows.Close()
	out := make([]domain.Authorization, 0)
	for rows.Next() {
		var a domain.Authorization
		var authKeyID int64
		if err := rows.Scan(
			&authKeyID, &a.UserID, &a.Hash, &a.Layer, &a.DeviceModel, &a.Platform, &a.SystemVersion,
			&a.APIID, &a.AppVersion, &a.IP, &a.CreatedAt, &a.ActiveAt,
		); err != nil {
			return nil, fmt.Errorf("scan deleted authorization: %w", err)
		}
		a.AuthKeyID = authKeyIDFromInt64(authKeyID)
		out = append(out, a)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate deleted authorizations: %w", err)
	}
	return out, nil
}

// RevokeByUserExcept bulk-deletes business authorizations, keeping the current device for keepAuthKeyID;
// the revoked devices' protocol key/binding is kept so they get an RPC 401 on reconnect and complete the client logout.
func (s *AuthorizationStore) RevokeByUserExcept(ctx context.Context, userID int64, keepAuthKeyID [8]byte) ([]domain.Authorization, error) {
	var out []domain.Authorization
	err := withAuthIdentityTx(ctx, s.db, "revoke authorizations by user", func(tx pgx.Tx) error {
		var err error
		out, err = revokeByUserExceptTx(ctx, tx, userID, authKeyIDToInt64(keepAuthKeyID))
		return err
	})
	return out, err
}

func revokeByUserExceptTx(ctx context.Context, tx pgx.Tx, userID, keepAuthKeyID int64) ([]domain.Authorization, error) {
	candidateRows, err := tx.Query(ctx, `
SELECT auth_key_id
FROM authorizations
WHERE user_id = $1 AND auth_key_id <> $2
ORDER BY auth_key_id`, userID, keepAuthKeyID)
	if err != nil {
		return nil, fmt.Errorf("select revoke candidates by user: %w", err)
	}
	candidates := make([]int64, 0)
	for candidateRows.Next() {
		var id int64
		if err := candidateRows.Scan(&id); err != nil {
			candidateRows.Close()
			return nil, fmt.Errorf("scan revoke candidate by user: %w", err)
		}
		candidates = append(candidates, id)
	}
	if err := candidateRows.Err(); err != nil {
		candidateRows.Close()
		return nil, fmt.Errorf("iterate revoke candidates by user: %w", err)
	}
	candidateRows.Close()
	if len(candidates) == 0 {
		return []domain.Authorization{}, nil
	}
	if err := lockPermanentAuthIdentities(ctx, tx, candidates); err != nil {
		return nil, err
	}

	// Advisory keys are already all held in final int32-hash order. Parent rows
	// are then locked by their real bigint IDs for deterministic batch behavior.
	lockRows, err := tx.Query(ctx, `
SELECT auth_key_id
FROM auth_keys
WHERE auth_key_id = ANY($1::bigint[])
ORDER BY auth_key_id
FOR UPDATE`, candidates)
	if err != nil {
		return nil, fmt.Errorf("lock revoke auth keys by user: %w", err)
	}
	for lockRows.Next() {
		var ignored int64
		if err := lockRows.Scan(&ignored); err != nil {
			lockRows.Close()
			return nil, fmt.Errorf("scan locked revoke auth key: %w", err)
		}
	}
	if err := lockRows.Err(); err != nil {
		lockRows.Close()
		return nil, fmt.Errorf("iterate locked revoke auth keys: %w", err)
	}
	lockRows.Close()

	// This is intentionally a new statement snapshot after all parent locks.
	// Keys that changed owner while waiting are omitted and must remain intact.
	rows, err := tx.Query(ctx, `
SELECT auth_key_id, user_id, hash, layer, device_model, platform, system_version,
       api_id, app_version, ip, password_pending, created_at, active_at
FROM authorizations
WHERE user_id = $1
  AND auth_key_id <> $2
  AND auth_key_id = ANY($3::bigint[])
ORDER BY created_at, auth_key_id
FOR UPDATE`, userID, keepAuthKeyID, candidates)
	if err != nil {
		return nil, fmt.Errorf("revalidate revoke authorizations by user: %w", err)
	}
	out := make([]domain.Authorization, 0)
	for rows.Next() {
		a, err := scanRevokedAuthorizationRow(rows)
		if err != nil {
			rows.Close()
			return nil, err
		}
		out = append(out, a)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return nil, fmt.Errorf("iterate revoked authorizations: %w", err)
	}
	rows.Close()
	if len(out) == 0 {
		return out, nil
	}
	targets := make([]int64, len(out))
	for i := range out {
		targets[i] = authKeyIDToInt64(out[i].AuthKeyID)
	}
	if err := deleteRevokedAuthorizationStateTx(ctx, tx, targets); err != nil {
		return nil, err
	}
	return out, nil
}

func deleteRevokedAuthorizationStateTx(ctx context.Context, tx pgx.Tx, authKeyIDs []int64) error {
	if len(authKeyIDs) == 0 {
		return nil
	}
	if _, err := tx.Exec(ctx, `
DELETE FROM update_states
WHERE auth_key_id = ANY($1::bigint[])`, authKeyIDs); err != nil {
		return fmt.Errorf("delete revoked update states: %w", err)
	}
	tag, err := tx.Exec(ctx, `
DELETE FROM authorizations
WHERE auth_key_id = ANY($1::bigint[])`, authKeyIDs)
	if err != nil {
		return fmt.Errorf("delete revoked authorizations: %w", err)
	}
	if tag.RowsAffected() != int64(len(authKeyIDs)) {
		return fmt.Errorf("delete revoked authorizations: deleted %d of %d locked targets", tag.RowsAffected(), len(authKeyIDs))
	}
	return nil
}

// deleteProtocolAuthIdentitiesTx permanently removes permanent identities and
// their derived temp keys. Remote account.resetAuthorization/
// auth.resetAuthorizations must not use this helper: those clients need the
// protocol key long enough to reconnect and receive an RPC-level 401.
func deleteProtocolAuthIdentitiesTx(ctx context.Context, tx pgx.Tx, authKeyIDs []int64) error {
	if len(authKeyIDs) == 0 {
		return nil
	}
	if _, err := tx.Exec(ctx, `
DELETE FROM auth_keys
WHERE auth_key_id IN (
	SELECT temp_auth_key_id
	FROM temp_auth_key_bindings
	WHERE perm_auth_key_id = ANY($1::bigint[])
)`, authKeyIDs); err != nil {
		return fmt.Errorf("delete temporary auth keys: %w", err)
	}
	if _, err := tx.Exec(ctx, `
DELETE FROM update_states
WHERE auth_key_id = ANY($1::bigint[])`, authKeyIDs); err != nil {
		return fmt.Errorf("delete protocol identity update states: %w", err)
	}
	tag, err := tx.Exec(ctx, `
DELETE FROM auth_keys
WHERE auth_key_id = ANY($1::bigint[])`, authKeyIDs)
	if err != nil {
		return fmt.Errorf("delete permanent auth keys: %w", err)
	}
	if tag.RowsAffected() != int64(len(authKeyIDs)) {
		return fmt.Errorf("delete permanent auth keys: deleted %d of %d locked targets", tag.RowsAffected(), len(authKeyIDs))
	}
	return nil
}

func authorizationFromRow(row sqlcgen.Authorization) domain.Authorization {
	return domain.Authorization{
		AuthKeyID:     authKeyIDFromInt64(row.AuthKeyID),
		UserID:        row.UserID,
		Hash:          row.Hash,
		Layer:         int(row.Layer),
		DeviceModel:   row.DeviceModel,
		Platform:      row.Platform,
		SystemVersion: row.SystemVersion,
		APIID:         int(row.ApiID),
		AppVersion:    row.AppVersion,
		IP:            row.Ip,
		CreatedAt:     row.CreatedAt.Time,
		ActiveAt:      row.ActiveAt.Time,
	}
}

type authorizationScanner interface {
	Scan(dest ...any) error
}

func scanRevokedAuthorization(row authorizationScanner) (domain.Authorization, bool, error) {
	a, err := scanRevokedAuthorizationRow(row)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.Authorization{}, false, nil
		}
		return domain.Authorization{}, false, err
	}
	return a, true, nil
}

func scanRevokedAuthorizationRow(row authorizationScanner) (domain.Authorization, error) {
	var a domain.Authorization
	var authKeyID int64
	if err := row.Scan(
		&authKeyID, &a.UserID, &a.Hash, &a.Layer, &a.DeviceModel, &a.Platform, &a.SystemVersion,
		&a.APIID, &a.AppVersion, &a.IP, &a.PasswordPending, &a.CreatedAt, &a.ActiveAt,
	); err != nil {
		return domain.Authorization{}, err
	}
	a.AuthKeyID = authKeyIDFromInt64(authKeyID)
	return a, nil
}

func authorizationHash(authKeyID [8]byte) int64 {
	sum := sha256.Sum256(authKeyID[:])
	hash := int64(binary.LittleEndian.Uint64(sum[:8]))
	if hash == 0 {
		return 1
	}
	return hash
}