Files
bare/internal/auth/auth.go
T
mayatnikovandClaude Opus 5 597c55301c Этап 1: аккаунты — argon2id, сессии, ключевой блоб, вход и регистрация
Сервер: миграция 001 со всей схемой storage.md, store на modernc.org/sqlite
(WAL, foreign_keys, один писатель), фоновая чистка раз в час, argon2id
с параметрами ADR-021 и сверкой constant-time, сессии по SHA-256 токена,
cookie bare_session, глобальная проверка Origin, девять эндпоинтов аккаунта.
Ник в журнал не попадает: для /api/ пишется шаблон маршрута.

Клиент: crypto.js по crypto.md построчно — мастер из пароля, два независимых
ключа из мастера, ключевой блоб с ником в AAD, отпечаток от сырой точки;
db.js со всеми хранилищами версии 1; экран входа и регистрации, настройки
со сменой пароля, выходом и удалением аккаунта.

Пароль не покидает клиент: проверено на боевом сервере — ни пароля, ни priv.d
ни в одном теле запроса, вход на втором устройстве даёт тот же отпечаток.

ADR-027: код internal для 500, причина только в журнале.
ADR-028: тексты состояний клиента сведены в ui.md.
ADR-029: вход под другим ником стирает историю только после подтверждения.
ADR-030: верхняя граница итераций KDF, проверка границ на обеих сторонах.
ADR-031: служебный выход перед повторным входом не заканчивает сеанс.
ADR-032: каталог состояния 0700, файлы базы 0600.

Прямые зависимости: modernc.org/sqlite, golang.org/x/crypto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
2026-08-22 14:06:07 +03:00

97 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package auth — argon2id, сессии, cookie и проверки на входе (ADR-021).
//
// Пароля здесь нет: клиент присылает authKey, выведенный из пароля
// (ADR-015). Сервер хранит argon2id от authKey — чтобы дамп базы не давал
// готового ключа для входа.
package auth
import (
"crypto/rand"
"crypto/subtle"
"fmt"
"golang.org/x/crypto/argon2"
"github.com/xmatic-squad/bare/internal/store"
)
// Параметры argon2id из ADR-021. Вход — 32 случайных байта с точки зрения
// сервера, поэтому параметры умеренные.
const (
SaltLen = 16 // байт
KeyLen = 32 // байт
)
// Params — параметры одного хеша. Пишутся рядом с ним в users.auth_params
// и читаются оттуда при проверке: повышение параметров — перехеш при
// очередном входе, а не миграция всех аккаунтов разом.
type Params struct {
Memory uint32 // КиБ
Time uint32
Threads uint8
}
// Current — параметры для новых хешей.
var Current = Params{Memory: 19456, Time: 2, Threads: 1}
// String — форма записи в базе: "argon2id,m=19456,t=2,p=1".
func (p Params) String() string {
return fmt.Sprintf("argon2id,m=%d,t=%d,p=%d", p.Memory, p.Time, p.Threads)
}
// ParseParams разбирает строку из users.auth_params.
func ParseParams(s string) (Params, error) {
var p Params
n, err := fmt.Sscanf(s, "argon2id,m=%d,t=%d,p=%d", &p.Memory, &p.Time, &p.Threads)
if err != nil || n != 3 {
return Params{}, fmt.Errorf("auth: не разобрать параметры %q", s)
}
if p.Memory == 0 || p.Time == 0 || p.Threads == 0 {
return Params{}, fmt.Errorf("auth: нулевой параметр в %q", s)
}
// Sscanf не жалуется на хвост после последнего числа; сверка с обратной
// записью делает разбор точным.
if p.String() != s {
return Params{}, fmt.Errorf("auth: не разобрать параметры %q", s)
}
return p, nil
}
// Hash считает argon2id от authKey с текущими параметрами и новой солью.
func Hash(authKey []byte) (store.Credential, error) {
salt := make([]byte, SaltLen)
if _, err := rand.Read(salt); err != nil {
return store.Credential{}, fmt.Errorf("auth: соль: %w", err)
}
return store.Credential{
Hash: derive(authKey, salt, Current),
Salt: salt,
Params: Current.String(),
}, nil
}
// Verify сверяет authKey с хешем из базы. Второе значение — нужен ли
// перехеш: параметры записи отстали от текущих.
func Verify(authKey []byte, cred store.Credential) (ok, rehash bool) {
p, err := ParseParams(cred.Params)
if err != nil || len(cred.Hash) != KeyLen || len(cred.Salt) == 0 {
return false, false
}
got := derive(authKey, cred.Salt, p)
if subtle.ConstantTimeCompare(got, cred.Hash) != 1 {
return false, false
}
return true, p != Current || len(cred.Salt) != SaltLen
}
// Waste считает столько же, сколько Verify, и выбрасывает результат.
// Вход с несуществующим ником не должен отвечать заметно быстрее входа
// с неверным authKey: одна ошибка на все случаи (docs/protocol.md).
func Waste(authKey []byte) {
derive(authKey, make([]byte, SaltLen), Current)
}
func derive(authKey, salt []byte, p Params) []byte {
return argon2.IDKey(authKey, salt, p.Time, p.Memory, p.Threads, KeyLen)
}