major fixes test
This commit is contained in:
+253
-9
@@ -1,16 +1,260 @@
|
||||
#!/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
|
||||
|
||||
# Generates a long random hex key suitable for use as a client access key.
|
||||
# Run this once per client (main/test/etc.) and put the result into:
|
||||
# - server-side config.json (as the dict key, mapped to the folder)
|
||||
# - updater.py CLIENT_KEYS (as the value, mapped to the display name)
|
||||
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="Generate a random hex access key for a client.")
|
||||
parser.add_argument("--bytes", type=int, default=32,
|
||||
help="Number of random bytes (default 32 -> 64 hex chars).")
|
||||
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()
|
||||
|
||||
key = secrets.token_hex(args.bytes)
|
||||
print(key)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user