From 1029563cb11d574b33af34c82742a231bb6a35ec Mon Sep 17 00:00:00 2001 From: nihonium Date: Wed, 14 Jan 2026 12:55:21 +0300 Subject: [PATCH] feat: first working version --- cmd/add_user/main.go | 45 ++++++++++ cmd/authapp/main.go | 88 ++++++++++++++++++++ cmd/init_users/main.go | 39 +++++++++ configs/config.toml | 21 +++++ go.mod | 11 +++ go.sum | 8 ++ internal/auth/auth.go | 54 ++++++++++++ internal/config/config.go | 53 ++++++++++++ internal/db/sqlite.go | 170 ++++++++++++++++++++++++++++++++++++++ internal/ui/console.go | 70 ++++++++++++++++ internal/utils/hash.go | 26 ++++++ 11 files changed, 585 insertions(+) create mode 100644 cmd/add_user/main.go create mode 100644 cmd/authapp/main.go create mode 100644 cmd/init_users/main.go create mode 100644 configs/config.toml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/auth/auth.go create mode 100644 internal/config/config.go create mode 100644 internal/db/sqlite.go create mode 100644 internal/ui/console.go create mode 100644 internal/utils/hash.go diff --git a/cmd/add_user/main.go b/cmd/add_user/main.go new file mode 100644 index 0000000..faa4bed --- /dev/null +++ b/cmd/add_user/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "flag" + "fmt" + "log" + + "linux-auth/internal/db" + "linux-auth/internal/utils" +) + +/** + * @brief CLI утилита для добавления нового пользователя в базу + * + * Позволяет добавлять пользователей интерактивно или через аргументы. + */ +func main() { + // ---------- Аргументы ---------- + username := flag.String("username", "", "Логин нового пользователя") + password := flag.String("password", "", "Пароль нового пользователя") + dbPath := flag.String("db", "./data/users.db", "Путь к файлу базы данных") + flag.Parse() + + // ---------- Проверка аргументов ---------- + if *username == "" || *password == "" { + fmt.Println("Использование: go run cmd/add_user/main.go -username USER -password PASS [-db ./data/users.db]") + return + } + + // ---------- Инициализация БД ---------- + err := db.Init(*dbPath) + if err != nil { + log.Fatalf("Ошибка инициализации БД: %v\n", err) + } + defer db.Close() + + // ---------- Создание пользователя ---------- + hash := utils.HashPassword(*password) + err = db.CreateUser(*username, hash) + if err != nil { + log.Fatalf("Не удалось создать пользователя %s: %v\n", *username, err) + } + + fmt.Printf("Пользователь %s успешно добавлен.\n", *username) +} diff --git a/cmd/authapp/main.go b/cmd/authapp/main.go new file mode 100644 index 0000000..182a4ff --- /dev/null +++ b/cmd/authapp/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "linux-auth/internal/auth" + "linux-auth/internal/config" + "linux-auth/internal/db" + "linux-auth/internal/ui" +) + +/** + * @brief Точка входа в приложение аутентификации + * + * Программа загружает конфигурацию, инициализирует БД + * и запускает цикл аутентификации пользователя. + */ +func main() { + // ---------- Работа с аргументами ---------- + configPath := flag.String( + "config", + "configs/config.toml", + "Путь к конфигурационному файлу", + ) + flag.Parse() + + // ---------- Загрузка конфигурации ---------- + cfg, err := config.Load(*configPath) + if err != nil { + log.Fatalf("Ошибка загрузки конфигурации: %v\n", err) + } + + if cfg.App.Name != "" { + fmt.Println("=== ", cfg.App.Name, " ===") + } + + // ---------- Инициализация БД ---------- + err = db.Init(cfg.Database.Path) + if err != nil { + log.Fatalf("Ошибка инициализации БД: %v\n", err) + } + defer db.Close() + + // ---------- Основной цикл аутентификации ---------- + attempts := 0 + + for { + // Проверка лимита попыток + if attempts >= cfg.App.MaxLoginAttempts { + fmt.Println("Превышено количество попыток входа.") + os.Exit(1) + } + + // Ввод логина и пароля + username, password, err := ui.ReadCredentials() + if err != nil { + fmt.Println("Ошибка ввода данных.") + attempts++ + continue + } + + // Проверка аутентификации + ok, err := auth.Authenticate(username, password) + if err != nil { + fmt.Printf("Ошибка аутентификации: %v\n", err) + attempts++ + continue + } + + if ok { + fmt.Printf("Доступ разрешён. Добро пожаловать, %s!\n", username) + break + } else { + fmt.Println("Неверный логин или пароль.") + attempts++ + } + + // Предложение очистки формы (логическое) + if ui.AskReset() { + continue + } + } + + fmt.Println("Работа приложения завершена.") +} diff --git a/cmd/init_users/main.go b/cmd/init_users/main.go new file mode 100644 index 0000000..5ba1954 --- /dev/null +++ b/cmd/init_users/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "log" + + "linux-auth/internal/db" + "linux-auth/internal/utils" +) + +func main() { + // Путь к файлу базы данных + dbPath := "./data/users.db" + + // Инициализация БД + err := db.Init(dbPath) + if err != nil { + log.Fatalf("Ошибка инициализации БД: %v\n", err) + } + defer db.Close() + + // Создаём пользователей + users := map[string]string{ + "admin": "admin123", + "user1": "password1", + } + + for username, password := range users { + hash := utils.HashPassword(password) + err := db.CreateUser(username, hash) + if err != nil { + fmt.Printf("Не удалось создать пользователя %s: %v\n", username, err) + } else { + fmt.Printf("Пользователь %s успешно создан.\n", username) + } + } + + fmt.Println("Инициализация пользователей завершена.") +} diff --git a/configs/config.toml b/configs/config.toml new file mode 100644 index 0000000..a0098b1 --- /dev/null +++ b/configs/config.toml @@ -0,0 +1,21 @@ +[app] +# Название приложения +name = "AuthApp" +# Максимальное количество попыток входа +max_login_attempts = 3 +# Время блокировки пользователя (сек) +lock_timeout_sec = 300 + +[database] +# Тип базы данных (для будущей расширяемости) +type = "sqlite" +# Путь к файлу базы данных SQLite +path = "./data/users.db" + +[security] +# Тип хеширования пароля +password_hash = "sha256" + +[ui] +# Показать приветственное сообщение при старте +show_welcome = true diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..71d543c --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module linux-auth + +go 1.25.0 + +require ( + github.com/mattn/go-sqlite3 v1.14.33 + github.com/pelletier/go-toml/v2 v2.2.4 + golang.org/x/term v0.39.0 +) + +require golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cce9012 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..c5797da --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,54 @@ +package auth + +import ( + "errors" + + "linux-auth/internal/db" + "linux-auth/internal/utils" +) + +/** + * @brief Проверка аутентификации пользователя + * + * Сравнивает введённый пароль с хешем из БД. + * Учитывает блокировку пользователя и количество неудачных попыток. + * + * @param username Логин пользователя + * @param password Введённый пароль + * @return true если доступ разрешён, false если логин/пароль неверны + * @return ошибка если произошла системная ошибка (например, БД) + */ +func Authenticate(username, password string) (bool, error) { + user, err := db.GetUser(username) + if err != nil { + // Пользователь не найден + return false, nil + } + + // Проверка блокировки + if user.Locked { + return false, errors.New("пользователь заблокирован") + } + + // Проверка пароля + if !utils.CheckPassword(password, user.PasswordHash) { + // Инкрементируем счётчик неудачных попыток + err := db.IncrementFail(username) + if err != nil { + return false, err + } + + // Если достигнут максимум — блокируем + const MaxAttempts = 3 // Можно передавать через config.toml + user.FailedAttempts++ + if user.FailedAttempts >= MaxAttempts { + _ = db.LockUser(username) + } + + return false, nil + } + + // Успешный вход — сбрасываем счётчик неудачных попыток + _ = db.ResetFails(username) + return true, nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..0a96963 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,53 @@ +package config + +import ( + "os" + + "github.com/pelletier/go-toml/v2" +) + +/** + * @brief Структура конфигурации приложения + */ +type Config struct { + App struct { + Name string `toml:"name"` + MaxLoginAttempts int `toml:"max_login_attempts"` + LockTimeoutSec int `toml:"lock_timeout_sec"` + } `toml:"app"` + + Database struct { + Type string `toml:"type"` + Path string `toml:"path"` + } `toml:"database"` + + Security struct { + PasswordHash string `toml:"password_hash"` + } `toml:"security"` + + UI struct { + ShowWelcome bool `toml:"show_welcome"` + } `toml:"ui"` +} + +/** + * @brief Загрузка конфигурации из TOML файла + * + * @param path путь к файлу конфигурации + * @return cfg указатель на структуру Config + * @return error ошибка при загрузке + */ +func Load(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var cfg Config + err = toml.Unmarshal(data, &cfg) + if err != nil { + return nil, err + } + + return &cfg, nil +} diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go new file mode 100644 index 0000000..0b5ae32 --- /dev/null +++ b/internal/db/sqlite.go @@ -0,0 +1,170 @@ +package db + +import ( + "database/sql" + "errors" + "fmt" + + _ "github.com/mattn/go-sqlite3" +) + +/** + * @brief Структура пользователя + */ +type User struct { + ID int + Username string + PasswordHash string + FailedAttempts int + Locked bool +} + +var database *sql.DB + +/** + * @brief Инициализация соединения с SQLite БД + * @param path Путь к файлу базы данных + * @return ошибка при неудаче + */ +func Init(path string) error { + var err error + + database, err = sql.Open("sqlite3", path) + if err != nil { + return err + } + + // Проверка соединения + if err = database.Ping(); err != nil { + return err + } + + return createTables() +} + +/** + * @brief Закрытие соединения с БД + */ +func Close() { + if database != nil { + _ = database.Close() + } +} + +/** + * @brief Создание таблиц при первом запуске + */ +func createTables() error { + query := ` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + failed_attempts INTEGER DEFAULT 0, + locked INTEGER DEFAULT 0 + ); + ` + + _, err := database.Exec(query) + return err +} + +/** + * @brief Получить пользователя по имени + * @param username Логин пользователя + * @return структура User или ошибка + */ +func GetUser(username string) (*User, error) { + query := ` + SELECT id, username, password_hash, failed_attempts, locked + FROM users + WHERE username = ?; + ` + + row := database.QueryRow(query, username) + + var user User + var locked int + + err := row.Scan( + &user.ID, + &user.Username, + &user.PasswordHash, + &user.FailedAttempts, + &locked, + ) + + if err == sql.ErrNoRows { + return nil, errors.New("пользователь не найден") + } + if err != nil { + return nil, err + } + + user.Locked = locked != 0 + return &user, nil +} + +/** + * @brief Увеличить счётчик неудачных попыток входа + * @param username Логин пользователя + */ +func IncrementFail(username string) error { + query := ` + UPDATE users + SET failed_attempts = failed_attempts + 1 + WHERE username = ?; + ` + + _, err := database.Exec(query, username) + return err +} + +/** + * @brief Сбросить счётчик неудачных попыток + * @param username Логин пользователя + */ +func ResetFails(username string) error { + query := ` + UPDATE users + SET failed_attempts = 0 + WHERE username = ?; + ` + + _, err := database.Exec(query, username) + return err +} + +/** + * @brief Заблокировать пользователя + * @param username Логин пользователя + */ +func LockUser(username string) error { + query := ` + UPDATE users + SET locked = 1 + WHERE username = ?; + ` + + _, err := database.Exec(query) + return err +} + +/** + * @brief Добавить нового пользователя (для инициализации) + * @param username Логин + * @param passwordHash Хеш пароля + */ +func CreateUser(username, passwordHash string) error { + query := ` + INSERT INTO users (username, password_hash) + VALUES (?, ?); + ` + + _, err := database.Exec(query, username, passwordHash) + if err != nil { + return fmt.Errorf("не удалось создать пользователя: %w", err) + } + + return nil +} diff --git a/internal/ui/console.go b/internal/ui/console.go new file mode 100644 index 0000000..2378943 --- /dev/null +++ b/internal/ui/console.go @@ -0,0 +1,70 @@ +package ui + +import ( + "bufio" + "fmt" + "os" + "strings" + + "syscall" + + "golang.org/x/term" +) + +var reader = bufio.NewReader(os.Stdin) + +/** + * @brief Считывание логина и пароля с консоли + * + * Логин вводится обычным текстом, пароль — с маской. + * + * @return login введённый логин + * @return password введённый пароль + * @return err ошибка при вводе + */ +func ReadCredentials() (login string, password string, err error) { + // Ввод логина + fmt.Print("Логин: ") + login, err = reader.ReadString('\n') + if err != nil { + return "", "", err + } + login = strings.TrimSpace(login) + + // Ввод пароля с маской + fmt.Print("Пароль: ") + bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() + if err != nil { + return "", "", err + } + password = strings.TrimSpace(string(bytePassword)) + + return login, password, nil +} + +/** + * @brief Предложить пользователю очистку формы + * + * Пользователь может ввести "y" или "n". + * + * @return true если пользователь выбрал сброс + */ +func AskReset() bool { + for { + fmt.Print("Очистить форму и попробовать снова? (y/n): ") + resp, err := reader.ReadString('\n') + if err != nil { + return false + } + resp = strings.ToLower(strings.TrimSpace(resp)) + + if resp == "y" || resp == "yes" { + return true + } else if resp == "n" || resp == "no" { + return false + } else { + fmt.Println("Введите y или n.") + } + } +} diff --git a/internal/utils/hash.go b/internal/utils/hash.go new file mode 100644 index 0000000..26d54d5 --- /dev/null +++ b/internal/utils/hash.go @@ -0,0 +1,26 @@ +package utils + +import ( + "crypto/sha256" + "encoding/hex" +) + +/** + * @brief Вычислить SHA-256 хеш пароля + * @param password Строка с паролем + * @return Хеш в виде строки HEX + */ +func HashPassword(password string) string { + hash := sha256.Sum256([]byte(password)) + return hex.EncodeToString(hash[:]) +} + +/** + * @brief Проверить совпадение пароля и хеша + * @param password Введённый пароль + * @param hash Хеш пароля из БД + * @return true если совпадает + */ +func CheckPassword(password, hash string) bool { + return HashPassword(password) == hash +}