Files
2026-09-12 03:11:49 +03:00

261 lines
13 KiB
Python
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.
#!/usr/bin/env python3
"""
generate_key.py
----------------------------------
Генерирует ключи для simple-updater и умеет сразу сохранять их в
config.json (см. --save), чтобы не копировать хэши руками между файлами.
Без аргументов — печатает один 64-символьный hex access-key (используется
в config.json's access_keys для маршрутизации клиентских папок). Это НЕ
криптографический секрет в строгом смысле - он обязан жить внутри
updater.exe, чтобы обновлятор работал, и его можно извлечь декомпиляцией.
Он просто не даёт угадать URL клиентской папки наугад.
С флагом --signing-keypair генерирует пару Ed25519-ключей для подписи
манифестов:
- приватный ключ - с --save сохраняется в отдельный текстовый файл
(по умолчанию SIGNING_PRIVATE_KEY.txt, см. config.json's
signing_keys.private_key_file) НЕ внутри config.json. Остаётся
ТОЛЬКО на машине, где вы генерируете манифесты
(generate_manifest.py / generate_updater_manifest.py). Никогда не
попадает ни на сервер, ни в updater.exe, ни в config.json - именно
чтобы его нельзя было случайно закоммитить или разослать вместе с
остальными настройками.
- публичный ключ - идёт в config.json (signing_keys.public) и
вписывается в updater.py перед компиляцией exe. Его можно
раскрывать сколько угодно: имея только публичный ключ, подделать
подпись невозможно (в отличие от HMAC, где один и тот же секрет и
подписывает, и проверяет - и потому достать его из exe означало бы
получить возможность подделывать манифесты).
Это меняет модель угроз по сравнению с HMAC: даже если updater.exe
полностью декомпилирован, из него можно извлечь только публичный ключ,
которым НИЧЕГО подписать нельзя - только проверить чужую подпись.
Примеры:
python generate_key.py --client Main --save
python generate_key.py --signing-keypair --save
"""
import os
import json
import secrets
import argparse
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
_CONFIG_PATH = os.path.join(_SCRIPT_DIR, "config.json")
def generate_access_key() -> str:
return secrets.token_hex(32) # 64 hex chars
def generate_signing_keypair():
"""
Returns (private_hex, public_hex) - both raw 32-byte Ed25519 keys,
hex-encoded. private_hex goes only in generate_manifest.py's config
(kept secret, ideally outside version control). public_hex goes in
updater.py and is safe to publish/decompile.
"""
private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()
private_bytes = private_key.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption(),
)
public_bytes = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
return private_bytes.hex(), public_bytes.hex()
def _private_key_file_path(cfg):
filename = cfg.get("signing_keys", {}).get("private_key_file", "SIGNING_PRIVATE_KEY.txt")
return os.path.join(_SCRIPT_DIR, filename), filename
def write_private_key_file(path, private_hex):
"""
Writes the Ed25519 private signing key to a standalone .txt file next
to the scripts, NOT into config.json. This key is the one real secret
in the whole project - it must never end up in a file that gets
zipped up, committed, or handed to anyone else, and config.json is
exactly the kind of file that tends to get copied around wholesale
(backed up, committed "just to be safe", sent to a teammate along with
the rest of the project). A dedicated file makes the secret visually
obvious and easy to .gitignore / delete on its own, separate from the
non-secret settings in config.json.
"""
sep = "=" * 70
lines = [
sep,
"СЕКРЕТНЫЙ КЛЮЧ - НЕ ДЕЛИТЕСЬ И НЕ КОММИТЬТЕ ЭТОТ ФАЙЛ",
sep,
"",
"Это приватный Ed25519-ключ для подписи manifest.json /",
"updater_manifest.json (generate_manifest.py, generate_updater_manifest.py,",
"quickstart.py). Кто угодно с этим ключом может подписать поддельный",
"манифест, который ваши игроки примут как настоящий, - то есть",
"фактически получит возможность выполнить произвольный код на машинах",
"всех игроков через игровые файлы или сам updater.exe.",
"",
"ЧТО С ЭТИМ ФАЙЛОМ ДЕЛАТЬ:",
" - Держите его ТОЛЬКО на машине, где вы генерируете манифесты.",
" - Никогда не кладите рядом с config.json в архив/бэкап/репозиторий,",
" который может куда-то уехать целиком.",
" - Добавьте имя файла в .gitignore, если используете git.",
" - Если ключ больше не нужен на этой машине (например, манифесты",
" теперь генерируются на другом сервере) - удалите файл. Он не",
" нужен ни серверу (server.py), ни updater.exe - только скриптам",
" генерации манифестов.",
" - Если ключ утёк - сгенерируйте новую пару (generate_key.py",
" --signing-keypair --save), заново сгенерируйте ВСЕ манифесты и",
" пересоберите updater.exe с новым публичным ключом.",
"",
sep,
"",
]
banner = "\n".join(lines) + "\n"
with open(path, "w", encoding="utf-8") as f:
f.write(banner)
f.write(f"SIGNING_PRIVATE_KEY={private_hex}\n")
def read_private_key_file(path):
"""
Reads the hex key back out of a file written by write_private_key_file.
Tolerates the human-readable banner around it - only the
SIGNING_PRIVATE_KEY=... line matters.
"""
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("SIGNING_PRIVATE_KEY="):
return line.split("=", 1)[1].strip()
return None
def load_config():
if not os.path.exists(_CONFIG_PATH):
raise SystemExit(f"config.json not found at {_CONFIG_PATH} - can't use --save without it.")
with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def save_config(cfg):
"""
Writes config.json back out, preserving key order/indentation style
(indent=4) so diffs stay readable. Not atomic-write-protected like the
runtime files elsewhere in this project - this is a one-off dev-time
tool run interactively, not something that needs to survive a crash
mid-write the way a live server/updater does.
"""
with open(_CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=4)
f.write("\n")
def save_access_key(client_name, key):
cfg = load_config()
cfg.setdefault("access_keys", {})
existing = cfg["access_keys"].get(client_name)
if existing:
print(f"WARNING: overwriting existing access key for '{client_name}' in config.json.")
print(" Any already-built updater.exe with the old key will stop being able")
print(" to reach the server until it's rebuilt with the new one.")
cfg["access_keys"][client_name] = key
cfg.setdefault("clients", {})
if client_name not in cfg["clients"]:
# Keep clients/access_keys in sync - a key with no matching folder
# entry would be silently skipped by server.py's startup check.
default_folder = f"game_client_{client_name.lower()}" if client_name.lower() != "main" else "game_client"
cfg["clients"][client_name] = default_folder
print(f"Also added clients.{client_name} = \"{default_folder}\" (edit this in config.json if needed).")
save_config(cfg)
print(f"Saved access_keys.{client_name} to config.json.")
def save_signing_keypair(private_hex, public_hex):
"""
Public half goes into config.json as usual - it's not a secret.
Private half goes into a standalone .txt file NEXT TO config.json,
never into config.json itself (see write_private_key_file's docstring
for why). Prints a loud reminder either way, since this overwriting
the private key file, if one already existed, invalidates every
manifest signed with the old key.
"""
cfg = load_config()
cfg.setdefault("signing_keys", {})
key_path, key_filename = _private_key_file_path(cfg)
if cfg["signing_keys"].get("public") or os.path.exists(key_path):
print("WARNING: overwriting existing signing keypair.")
print(" Manifests signed with the old private key will fail verification")
print(" against exes built with the new public key, and vice versa -")
print(" regenerate all manifests and rebuild updater.exe after this.")
cfg["signing_keys"]["public"] = public_hex
cfg["signing_keys"].setdefault("private_key_file", key_filename)
save_config(cfg)
write_private_key_file(key_path, private_hex)
print(f"Saved signing_keys.public to config.json.")
print(f"Saved PRIVATE key to {key_filename} - read the warning inside that file.")
print(f"{key_filename} is NOT part of config.json and should never be committed or shared.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Генератор ключей для simple-updater.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--signing-keypair",
action="store_true",
help=(
"Сгенерировать пару Ed25519-ключей для подписи манифестов "
"вместо access key. Печатает и приватный, и публичный ключ."
),
)
parser.add_argument(
"--client",
metavar="NAME",
help="Имя клиента (как 'Main' или 'Test') для сохранения access key под ним в config.json. Требуется вместе с --save, если не указан --signing-keypair.",
)
parser.add_argument(
"--save",
action="store_true",
help="Сохранить сгенерированный ключ прямо в config.json вместо/в дополнение к печати в консоль.",
)
args = parser.parse_args()
if args.signing_keypair:
private_hex, public_hex = generate_signing_keypair()
if args.save:
save_signing_keypair(private_hex, public_hex)
else:
print("Приватный ключ (держите в секрете, только для генерации манифестов):")
print(f" {private_hex}")
print()
print("Публичный ключ (безопасно зашивать в updater.exe):")
print(f" {public_hex}")
print()
print("Без --save ключи никуда не сохраняются - скопируйте их сами,")
print("или перезапустите с --save чтобы приватный ключ ушёл в отдельный")
print(".txt файл, а публичный - в config.json.")
else:
key = generate_access_key()
print(key)
if args.save:
if not args.client:
raise SystemExit("--save для access key требует --client NAME (например --client Main).")
save_access_key(args.client, key)