Files
Nighthawk42andClaude Opus 4.8 023ccd9f74 Rewrite as a packaged tool with an encrypted vault (v2.0.0)
Replace the single-file v1.3.1 script with a proper src/bnet_auth_tool/
package: config (YAML settings + platformdirs), crypto (scrypt + AES-256-GCM
with a versioned header; legacy PBKDF2 100k/600k still decrypt), storage
(single encrypted vault), fileio (atomic 0600 writes), api, totp, migrate, and
a cli with an interactive menu plus argparse subcommands.

Security/audit fixes: drop catch-all excepts, stop leaking server bodies/tokens
in errors, atomic permission-hardened writes, explicit Ctrl-C handling,
versioned format header, best-effort passphrase scrubbing.

Add packaging (pyproject for uv, organized requirements.txt fallback, uv.lock),
.gitignore, settings.yaml, CLAUDE.md/AGENTS.md, 22 pytest tests, and ruff
config. Online attach/retrieve is preserved but kept labelled unverified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:56:35 -04:00

88 lines
3.0 KiB
Python

"""Legacy file discovery and migration into the vault."""
from __future__ import annotations
import json
from bnet_auth_tool.crypto import EncryptionManager
from bnet_auth_tool.migrate import (
discover_legacy_files,
migrate_files,
normalise_entry,
)
from bnet_auth_tool.storage import Vault
PLAIN = {
"serial": "US-1111",
"restoreCode": "AAAA-BBBB",
"deviceSecret": "deadbeef",
"base32Secret": "32W353Y",
"totpUrl": "otpauth://totp/Battle.net:US-1111?secret=32W353Y",
"timestamp": "2024-01-01T00:00:00+00:00",
}
def _vault(tmp_path, settings, passphrase="pw"):
return Vault(EncryptionManager(passphrase, settings.crypto), tmp_path / "vault.json").load()
def test_discover_matches_expected_names(tmp_path):
(tmp_path / "battlenet_authenticator_US-1.json").write_text("{}")
(tmp_path / "reconstructed_US-2.json").write_text("{}")
(tmp_path / "unrelated.json").write_text("{}")
names = {p.name for p in discover_legacy_files(tmp_path)}
assert names == {"battlenet_authenticator_US-1.json", "reconstructed_US-2.json"}
def test_normalise_derives_missing_fields(settings):
entry = normalise_entry({"serial": "US-9", "deviceSecret": "deadbeef"}, settings)
assert entry["base32Secret"] == "32W353Y"
assert entry["totpUrl"].startswith("otpauth://totp/Battle.net:US-9")
def test_migrate_plaintext_file(tmp_path, settings):
f = tmp_path / "battlenet_authenticator_US-1111.json"
f.write_text(json.dumps(PLAIN))
vault = _vault(tmp_path, settings)
outcomes = migrate_files([f], vault, settings, lambda p: None)
vault.save()
assert outcomes[0].status == "imported"
assert vault.get("US-1111")["deviceSecret"] == "deadbeef"
assert vault.get("US-1111")["addedAt"] == PLAIN["timestamp"]
def test_migrate_encrypted_file(tmp_path, settings):
f = tmp_path / "battlenet_authenticator_US-2222.json"
blob = EncryptionManager("filepw", settings.crypto).encrypt({**PLAIN, "serial": "US-2222"})
f.write_bytes(blob)
vault = _vault(tmp_path, settings)
outcomes = migrate_files([f], vault, settings, lambda p: "filepw")
vault.save()
assert outcomes[0].status == "imported"
assert vault.get("US-2222") is not None
def test_migrate_skips_without_passphrase(tmp_path, settings):
f = tmp_path / "battlenet_authenticator_US-3333.json"
f.write_bytes(EncryptionManager("x", settings.crypto).encrypt({**PLAIN, "serial": "US-3333"}))
vault = _vault(tmp_path, settings)
outcomes = migrate_files([f], vault, settings, lambda p: None)
assert outcomes[0].status == "skipped"
assert len(vault) == 0
def test_migrate_no_overwrite_existing(tmp_path, settings):
f = tmp_path / "battlenet_authenticator_US-1111.json"
f.write_text(json.dumps(PLAIN))
vault = _vault(tmp_path, settings)
vault.add({"serial": "US-1111", "deviceSecret": "old"})
outcomes = migrate_files([f], vault, settings, lambda p: None)
assert outcomes[0].status == "skipped"
assert vault.get("US-1111")["deviceSecret"] == "old"