Initial commit

This commit is contained in:
user
2026-07-12 14:06:45 +04:00
commit a75028d68f
148 changed files with 3275 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
from core import PasswordHasher, IntegrityChecker, CryptoManager, AuditLogger, AccountManager
from gui import MainWindow
class App:
"""
Класс точки входа для запуска менеджера учётных записей.
"""
def __init__(self):
self.db_file = None
self.hash_file = "data/accounts.db.hash"
self.log_file = "data/audit.log"
self.password_hasher = PasswordHasher()
self.integrity_checker = IntegrityChecker()
self.crypto_manager = CryptoManager()
self.audit_logger = AuditLogger(self.log_file)
self.account_manager = AccountManager(self.db_file)
self.gui = MainWindow(
account_manager=self.account_manager,
password_hasher=self.password_hasher,
audit_logger=self.audit_logger,
crypto_manager=self.crypto_manager,
integrity_checker=self.integrity_checker,
)
def run(self):
self.gui.run()
if __name__ == "__main__":
App().run()
@@ -0,0 +1,5 @@
from .password_hasher import PasswordHasher
from .integrity_checker import IntegrityChecker
from .crypto_manager import CryptoManager
from .audit_logger import AuditLogger
from .account_manager import AccountManager
@@ -0,0 +1,62 @@
import json
from pathlib import Path
class AccountManager:
"""
Класс для управления учётными записями пользователей.
"""
def __init__(self, db_file: str | None):
self.db_file = Path(db_file) if db_file else None
if isinstance(self.db_file, Path) and not self.db_file.exists():
self._save_db({})
def set_db_file(self, db_file: str | None) -> None:
self.db_file = Path(db_file) if db_file else None
if isinstance(self.db_file, Path) and not self.db_file.exists():
self._save_db({})
def _load_db(self) -> dict:
if not isinstance(self.db_file, Path):
return {}
if self.db_file.exists():
return json.loads(self.db_file.read_text(encoding="utf-8"))
return {}
def _save_db(self, data: dict) -> None:
if not isinstance(self.db_file, Path):
raise RuntimeError("База данных не открыта")
self.db_file.write_text(json.dumps(
data, indent=2, ensure_ascii=False), encoding="utf-8")
def add_user(self, username: str, password_hash: str, salt: str) -> None:
data = self._load_db()
data[username] = {"hash": password_hash, "salt": salt}
self._save_db(data)
def update_user(self, username: str, password_hash: str, salt: str) -> bool:
data = self._load_db()
if username not in data:
return False
data[username] = {"hash": password_hash, "salt": salt}
self._save_db(data)
return True
def delete_user(self, username: str) -> bool:
data = self._load_db()
if username not in data:
return False
del data[username]
self._save_db(data)
return True
def get_user(self, username: str) -> dict | None:
data = self._load_db()
return data.get(username)
def list_users(self) -> list[str]:
if not isinstance(self.db_file, Path):
return []
data = self._load_db()
return list(data.keys())
@@ -0,0 +1,17 @@
from datetime import datetime
from pathlib import Path
class AuditLogger:
"""
Класс для ведения журнала событий.
"""
def __init__(self, log_file: str):
self.log_file = Path(log_file)
def log(self, event: str) -> None:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{timestamp}] {event}\n"
with open(self.log_file, "a", encoding="utf-8") as f:
f.write(line)
@@ -0,0 +1,23 @@
from cryptography.fernet import Fernet
from pathlib import Path
class CryptoManager:
"""
Класс для симметричного шифрования и расшифрования файлов.
"""
def generate_key(self) -> bytes:
return Fernet.generate_key()
def encrypt_file(self, file_path: str, key: bytes, output_path: str) -> None:
fernet = Fernet(key)
data = Path(file_path).read_bytes()
encrypted = fernet.encrypt(data)
Path(output_path).write_bytes(encrypted)
def decrypt_file(self, file_path: str, key: bytes, output_path: str) -> None:
fernet = Fernet(key)
data = Path(file_path).read_bytes()
decrypted = fernet.decrypt(data)
Path(output_path).write_bytes(decrypted)
@@ -0,0 +1,26 @@
import hashlib
from pathlib import Path
class IntegrityChecker:
"""
Класс для проверки целостности файлов по хэш-сумме.
"""
def calculate_hash(self, file_path: str) -> str:
hasher = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hasher.update(chunk)
return hasher.hexdigest()
def save_hash(self, file_path: str, hash_file: str) -> None:
file_hash = self.calculate_hash(file_path)
Path(hash_file).write_text(file_hash, encoding="utf-8")
def verify_hash(self, file_path: str, hash_file: str) -> bool:
if not Path(hash_file).exists():
return False
expected = Path(hash_file).read_text(encoding="utf-8").strip()
actual = self.calculate_hash(file_path)
return expected == actual
@@ -0,0 +1,19 @@
import hashlib
import secrets
class PasswordHasher:
"""
Класс для безопасного хэширования паролей с использованием соли.
"""
def generate_salt(self, length: int = 16) -> str:
return secrets.token_hex(length)
def hash_password(self, password: str, salt: str) -> str:
hasher = hashlib.sha256()
hasher.update(salt.encode("utf-8") + password.encode("utf-8"))
return hasher.hexdigest()
def verify_password(self, password: str, salt: str, expected_hash: str) -> bool:
return self.hash_password(password, salt) == expected_hash
@@ -0,0 +1,6 @@
{
"user2": {
"hash": "4d3596717c7887c07c607485fa8c287547a5e65559b3269844261417b1e7a97b",
"salt": "a1bb56a86e8d504634668696d9e65189"
}
}
@@ -0,0 +1 @@
109fb5ce85641ad0ed60cf8ae800289517d33c2cb100f39e5c7c74cb88b9b722
@@ -0,0 +1,3 @@
[2025-10-01 21:51:42] Добавлен пользователь: user
[2025-10-01 21:51:48] Добавлен пользователь: user2
[2025-10-01 21:51:55] Добавлен пользователь: user3
@@ -0,0 +1 @@
gAAAAABo3Wp_Jc6ye1Yf6rYq3GQ64_S6XHAQjm7WSCvS6fK48kMk5NmP7zFyX5AGShawV7uvFEVvx2v5Efn_hFWdML4I_iAf1x6jZheZLJuWzMLtsm4x25wEAo7c8vokR8YV-MaiJWKw_c9kdAzPoPpkfuZSAuwSU6hD_oNHooG8SPCWVTbbvmqwnquMchLpXaK_gfyYOgye_ERB5s5GBxMICwM47W87tGoKnFL6jtyGk8iIowPYD0xlrAhUwvuTeDfxlIhyOZBc72R5iTmiSbOgxNu6DyJy9-49kdSau35LTVWRThg7MEWxK1xTC5P6stXkhovNsjujsdcSBk8Rx-AgKBmSRPGznkC7zYTNw6gQhYujsi_WtaQ=
@@ -0,0 +1 @@
ir7gZ6qT1gP-wAcNvKB3Ua_k7zJ52bH_GmgdSO9iryo=
@@ -0,0 +1,3 @@
from .main_window import MainWindow
__all__ = ["MainWindow"]
@@ -0,0 +1,16 @@
import tkinter as tk
class ControlsPanel:
"""
Панель управления действиями над пользователями.
"""
def __init__(self, master, on_add, on_update, on_delete):
self.frame = tk.Frame(master)
tk.Button(self.frame, text="Добавить",
command=on_add).pack(side=tk.LEFT, padx=5)
tk.Button(self.frame, text="Изменить",
command=on_update).pack(side=tk.LEFT, padx=5)
tk.Button(self.frame, text="Удалить", command=on_delete).pack(
side=tk.LEFT, padx=5)
@@ -0,0 +1,92 @@
from __future__ import annotations
from pathlib import Path
from tkinter import messagebox, filedialog as fd
class CryptoTools:
"""Операции шифрования и расшифрования файлов."""
def __init__(self, crypto_manager, audit_logger):
self.crypto_manager = crypto_manager
self.audit_logger = audit_logger
def _generate_and_save_key(self) -> bytes | None:
key = self.crypto_manager.generate_key()
path = fd.asksaveasfilename(
title="Сохранить ключ шифрования",
defaultextension=".key",
filetypes=[("Key file", "*.key"), ("All files", "*.*")],
initialfile="encryption.key",
)
if not path:
return None
try:
Path(path).write_bytes(key)
except Exception as e:
messagebox.showerror("Сохранение ключа",
f"Не удалось сохранить ключ:\n{e}")
return None
messagebox.showinfo("Ключ сохранён", f"Ключ сохранён в:\n{path}")
return key
def _load_key_from_file(self) -> bytes | None:
path = fd.askopenfilename(
title="Выберите файл ключа",
filetypes=[("Key file", "*.key"), ("All files", "*.*")],
)
if not path:
return None
try:
return Path(path).read_bytes()
except Exception as e:
messagebox.showerror(
"Чтение ключа", f"Не удалось прочитать ключ:\n{e}")
return None
def _select_output_path(self, suggested: Path, title: str, defext: str, pattern: str) -> Path | None:
path = fd.asksaveasfilename(
title=title,
defaultextension=defext,
filetypes=[(f"*{defext}", pattern), ("All files", "*.*")],
initialdir=str(suggested.parent),
initialfile=suggested.name,
)
return Path(path) if path else None
def encrypt_file(self):
src = fd.askopenfilename(title="Выберите файл для шифрования")
if not src:
return
key = self._generate_and_save_key()
if key is None:
return
suggested = Path(src).with_suffix(Path(src).suffix + ".enc")
out = self._select_output_path(
suggested, "Сохранить зашифрованный файл", ".enc", "*.enc")
if out is None:
return
self.crypto_manager.encrypt_file(src, key, str(out))
self.audit_logger.log(f"Зашифрован файл → {out.name}")
messagebox.showinfo("Шифрование файла", f"Создан файл: {out}")
def decrypt_file(self):
enc = fd.askopenfilename(title="Выберите зашифрованный файл", filetypes=[
("Encrypted", "*.enc"), ("All files", "*.*")])
if not enc:
return
key = self._load_key_from_file()
if key is None:
return
suggested = Path(enc).with_suffix(".dec")
out = self._select_output_path(
suggested, "Сохранить расшифрованный файл", ".dec", "*.dec")
if out is None:
return
try:
self.crypto_manager.decrypt_file(enc, key, str(out))
except Exception as e:
messagebox.showerror("Расшифрование файла", f"Ошибка:\n{e}")
return
self.audit_logger.log(f"Расшифрован файл → {out.name}")
messagebox.showinfo("Расшифрование файла", f"Создан файл: {out}")
@@ -0,0 +1,73 @@
from __future__ import annotations
from pathlib import Path
from tkinter import messagebox, filedialog as fd
class IntegrityTools:
"""Операции сохранения и проверки хэша БД."""
def __init__(self, integrity_checker, audit_logger, db_path: str | None):
self.integrity_checker = integrity_checker
self.audit_logger = audit_logger
self.db_path = Path(db_path) if db_path else None
def set_db_path(self, db_path: str | None) -> None:
self.db_path = Path(db_path) if db_path else None
def _hash_path(self) -> Path:
return self.db_path.with_suffix(self.db_path.suffix + ".hash")
def save_db_hash(self):
if not self.db_path:
messagebox.showwarning("Сохранить хэш", "База данных не выбрана.")
return
if not self.db_path.exists():
messagebox.showwarning(
"Сохранить хэш", f"Файл БД не найден:\n{self.db_path}")
return
suggested = self.db_path.with_suffix(self.db_path.suffix + ".hash")
path = fd.asksaveasfilename(
title="Сохранить контрольную сумму БД",
defaultextension=".hash",
filetypes=[("Hash file", "*.hash"), ("All files", "*.*")],
initialdir=str(suggested.parent),
initialfile=suggested.name,
)
if not path:
return
try:
self.integrity_checker.save_hash(str(self.db_path), str(path))
except Exception as e:
messagebox.showerror("Сохранить хэш", f"Ошибка сохранения:\n{e}")
return
self.audit_logger.log(
f"Сохранена контрольная сумма БД → {Path(path).name}")
messagebox.showinfo("Сохранить хэш", f"Хэш сохранён в {path}")
def verify_db_hash(self):
if not self.db_path:
messagebox.showwarning(
"Проверка целостности", "База данных не выбрана.")
return
if not self.db_path.exists():
messagebox.showwarning(
"Проверка целостности", f"Файл БД не найден:\n{self.db_path}")
return
suggested = self.db_path.with_suffix(self.db_path.suffix + ".hash")
path = fd.askopenfilename(
title="Выбрать файл контрольной суммы БД",
filetypes=[("Hash file", "*.hash"), ("All files", "*.*")],
initialdir=str(suggested.parent),
initialfile=suggested.name,
)
if not path:
return
ok = self.integrity_checker.verify_hash(str(self.db_path), str(path))
self.audit_logger.log("Проверка целостности БД: " +
("OK" if ok else "НЕСОВПАДЕНИЕ"))
if ok:
messagebox.showinfo("Проверка целостности", "БД в целостности.")
else:
messagebox.showerror("Проверка целостности",
"БД была изменена извне.")
@@ -0,0 +1,135 @@
from __future__ import annotations
import tkinter as tk
from tkinter import messagebox, simpledialog, filedialog as fd
from pathlib import Path
from .user_list_view import UserListView
from .controls_panel import ControlsPanel
from .integrity_tools import IntegrityTools
from .crypto_tools import CryptoTools
class MainWindow:
"""
Главное окно менеджера учётных записей.
"""
def __init__(self, account_manager, password_hasher, audit_logger, crypto_manager, integrity_checker):
self.account_manager = account_manager
self.password_hasher = password_hasher
self.audit_logger = audit_logger
self.integrity_tools = IntegrityTools(
integrity_checker, audit_logger, account_manager.db_file)
self.crypto_tools = CryptoTools(crypto_manager, audit_logger)
self.root = tk.Tk()
self.root.title("Менеджер учётных записей")
self._build_menu()
self.users_view = UserListView(self.root)
self.users_view.frame.pack()
self.controls = ControlsPanel(
self.root,
on_add=self.add_user,
on_update=self.update_user,
on_delete=self.delete_user
)
self.controls.frame.pack(pady=5)
self.refresh_users()
def _build_menu(self):
menubar = tk.Menu(self.root)
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(
label="Открыть базу данных...", command=self.open_db)
file_menu.add_command(label="Закрыть базу данных",
command=self.close_db)
menubar.add_cascade(label="Файл", menu=file_menu)
tools = tk.Menu(menubar, tearoff=0)
tools.add_command(label="Сохранить хэш БД",
command=self.integrity_tools.save_db_hash)
tools.add_command(label="Проверить целостность БД",
command=self.integrity_tools.verify_db_hash)
tools.add_separator()
tools.add_command(label="Зашифровать файл...",
command=self.crypto_tools.encrypt_file)
tools.add_command(label="Расшифровать файл...",
command=self.crypto_tools.decrypt_file)
menubar.add_cascade(label="Инструменты", menu=tools)
self.root.config(menu=menubar)
# --- File ops ---
def open_db(self):
path = fd.askopenfilename(
title="Открыть базу данных",
filetypes=[("JSON", "*.json"), ("DB/TXT", "*.db *.txt"),
("Все файлы", "*.*")],
initialdir="data"
)
if not path:
return
self.account_manager.set_db_file(path)
self.integrity_tools.set_db_path(path)
self.audit_logger.log(f"Открыта база данных: {Path(path).name}")
self.refresh_users()
# --- Account ops ---
def refresh_users(self):
self.users_view.set_items(self.account_manager.list_users())
def add_user(self):
username = simpledialog.askstring(
"Добавить пользователя", "Введите имя пользователя:")
if not username:
return
password = simpledialog.askstring(
"Пароль", "Введите пароль:", show="*")
if not password:
return
salt = self.password_hasher.generate_salt()
password_hash = self.password_hasher.hash_password(password, salt)
self.account_manager.add_user(username, password_hash, salt)
self.audit_logger.log(f"Добавлен пользователь: {username}")
self.refresh_users()
def update_user(self):
username = self.users_view.get_selected()
if not username:
messagebox.showwarning("Изменить", "Выберите пользователя")
return
password = simpledialog.askstring(
"Новый пароль", f"Введите новый пароль для {username}:", show="*")
if not password:
return
salt = self.password_hasher.generate_salt()
password_hash = self.password_hasher.hash_password(password, salt)
if self.account_manager.update_user(username, password_hash, salt):
self.audit_logger.log(f"Обновлён пароль пользователя: {username}")
self.refresh_users()
def delete_user(self):
username = self.users_view.get_selected()
if not username:
messagebox.showwarning("Удалить", "Выберите пользователя")
return
if self.account_manager.delete_user(username):
self.audit_logger.log(f"Удалён пользователь: {username}")
self.refresh_users()
def run(self):
self.root.mainloop()
def close_db(self):
self.account_manager.set_db_file(None)
self.integrity_tools.set_db_path(None)
self.audit_logger.log("Закрыта база данных")
self.users_view.set_items([])
@@ -0,0 +1,21 @@
import tkinter as tk
class UserListView:
"""
Виджет списка пользователей.
"""
def __init__(self, master):
self.frame = tk.Frame(master)
self._listbox = tk.Listbox(self.frame, width=40)
self._listbox.pack(padx=10, pady=10)
def set_items(self, items: list[str]) -> None:
self._listbox.delete(0, tk.END)
for it in items:
self._listbox.insert(tk.END, it)
def get_selected(self) -> str | None:
sel = self._listbox.curselection()
return self._listbox.get(sel[0]) if sel else None
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
# Создание виртуального окружения в .venv (если ещё нет)
if [ ! -d ".venv" ]; then
python3 -m venv .venv
fi
# Активация окружения
. .venv/bin/activate
# Обновление pip и установка зависимостей
pip install --upgrade pip
pip install -r requirements.txt
echo "Виртуальное окружение создано и зависимости установлены."
echo "Активируйте его командой: . .venv/bin/activate"
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['app.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='lab5-manager',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
@@ -0,0 +1,2 @@
cryptography>=42.0.0
pytest>=7.0
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# Проверка наличия окружения
if [ ! -d ".venv" ]; then
echo "Виртуальное окружение не найдено. Сначала выполните ./install.sh"
exit 1
fi
# Активация окружения
. .venv/bin/activate
# Запуск приложения
python app.py
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# Проверка наличия окружения
if [ ! -d ".venv" ]; then
echo "Виртуальное окружение не найдено. Сначала выполните ./install.sh"
exit 1
fi
# Активация окружения
. .venv/bin/activate
# Запуск тестов
pytest -v tests
@@ -0,0 +1,5 @@
import sys
import os
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
@@ -0,0 +1,23 @@
from core.account_manager import AccountManager
def test_crud_operations(tmp_path):
db_file = tmp_path / "accounts.json"
manager = AccountManager(db_file)
# Добавление
manager.add_user("alice", "hash1", "salt1")
assert "alice" in manager.list_users()
# Чтение
user = manager.get_user("alice")
assert user["hash"] == "hash1"
# Обновление
assert manager.update_user("alice", "hash2", "salt2")
user = manager.get_user("alice")
assert user["hash"] == "hash2"
# Удаление
assert manager.delete_user("alice")
assert "alice" not in manager.list_users()
@@ -0,0 +1,12 @@
from core.audit_logger import AuditLogger
def test_log_event(tmp_path):
log_file = tmp_path / "audit.log"
logger = AuditLogger(log_file)
logger.log("Test event")
content = log_file.read_text(encoding="utf-8")
assert "Test event" in content
assert "[" in content # должно быть время
@@ -0,0 +1,17 @@
from core.crypto_manager import CryptoManager
def test_encrypt_decrypt_file(tmp_path):
file = tmp_path / "plain.txt"
file.write_text("super secret", encoding="utf-8")
enc_file = tmp_path / "plain.enc"
dec_file = tmp_path / "plain.dec"
crypto = CryptoManager()
key = crypto.generate_key()
crypto.encrypt_file(file, key, enc_file)
crypto.decrypt_file(enc_file, key, dec_file)
assert dec_file.read_text(encoding="utf-8") == "super secret"
@@ -0,0 +1,17 @@
from pathlib import Path
from core.integrity_checker import IntegrityChecker
def test_calculate_and_verify_hash(tmp_path: Path):
file = tmp_path / "data.txt"
file.write_text("hello", encoding="utf-8")
hash_file = tmp_path / "data.hash"
checker = IntegrityChecker()
checker.save_hash(file, hash_file)
assert checker.verify_hash(file, hash_file)
# Нарушим целостность
file.write_text("changed", encoding="utf-8")
assert not checker.verify_hash(file, hash_file)
@@ -0,0 +1,12 @@
import pytest
from core.password_hasher import PasswordHasher
def test_hash_and_verify():
hasher = PasswordHasher()
salt = hasher.generate_salt()
pwd = "secret123"
hashed = hasher.hash_password(pwd, salt)
assert hasher.verify_password(pwd, salt, hashed)
assert not hasher.verify_password("wrong", salt, hashed)