Two coupled hardening upgrades. 1. Asymmetric signatures (HMAC → Ed25519) The previous HMAC scheme used a symmetric secret that any motivated reverse engineer could pull out of the shipped binary and use to mint blobs for any tier / name / email. With Ed25519, the binary ships only the public verification key; the signing key never leaves the seller's environment, so binary compromise no longer yields forgery. - src/license/crypto.py rewritten around cryptography.hazmat.primitives.asymmetric.ed25519. Same public API surface (sign/verify/encode_blob/decode_blob), same canonical JSON encoding — drop-in for the manager / cli / GUI layers. - DATATOOLS_LICENSE_PRIVKEY (seller-side) and DATATOOLS_LICENSE_PUBKEY (build-time) env vars supply the keys; the in-source dev keypair (src/license/_dev_keypair.py) deterministically derives from a seed phrase for repro builds and tests. - Blob prefix bumped DTLIC1: → DTLIC2:. Decoding a DTLIC1 blob surfaces a clear "old format" error rather than a confusing signature mismatch. - scripts/generate_keypair.py mints fresh production keypairs for the seller (run once, stash the private key offline). Adds cryptography>=41,<46 to requirements.txt (was an undeclared transitive dep). 2. Production-safe tripwire assert_production_safe() refuses to boot a frozen / shipped build when either: - DATATOOLS_DEV_MODE=1 is set (would unconditionally bypass every license check — fine in source/test but catastrophic in a buyer install). - The active verification key is still the embedded dev key (the build pipeline forgot to set DATATOOLS_LICENSE_PUBKEY). No-op in source / pytest runs (sys.frozen is unset) so test fixtures and dev workflows keep working without ceremony. Called from src/cli_license_guard.guard() and from hide_streamlit_chrome — so it fires on every CLI invocation and every GUI page load. Tests: 49 license-layer unit tests (was 40); added Ed25519 wrong-key rejection, dev-keypair seed pin, blob v2 prefix, v1 rejection with clear message, and four production-safe scenarios (no-op in source, fires on DEV_MODE in frozen, fires on dev key in frozen, passes in frozen with prod pubkey). Total: 2024 → 2033. Docs (REQUIREMENTS §17a, DEVELOPER licensing recipe, DECISIONS §9b + decision log) updated with the new threat-model write-up, key-storage workflow, and tripwire behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
2.0 KiB
Python
69 lines
2.0 KiB
Python
"""License module — registration, activation, expiration, feature gating.
|
|
|
|
Public API the rest of the app uses:
|
|
|
|
- :func:`get_manager` — singleton :class:`LicenseManager` instance.
|
|
- :func:`current_state` — quick snapshot for status badges / tests.
|
|
- :func:`require_feature` — raise :class:`LicenseError` if a feature
|
|
isn't unlocked by the active license.
|
|
- :class:`License`, :class:`Tier`, :class:`FeatureFlag` — schema.
|
|
- :class:`LicenseError` and subclasses — typed failures the UI can
|
|
branch on (not yet activated vs. expired vs. tampered).
|
|
|
|
The license model is:
|
|
|
|
1. The seller (creator) runs ``scripts/generate_license.py`` to mint a
|
|
signed **license blob** keyed to a buyer's name + email.
|
|
2. The buyer pastes the blob into the activation page on first launch.
|
|
3. The app verifies the HMAC signature locally (no internet), then
|
|
writes a canonical ``~/.datatools/license.json`` and the app
|
|
unlocks.
|
|
|
|
The signature is HMAC-SHA256 with a build-time secret. Combined with
|
|
the 30-day refund policy, this is honor-system DRM — see
|
|
``docs/DECISIONS.md`` for the trade-off discussion.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .errors import (
|
|
ExpiredLicenseError,
|
|
InvalidLicenseError,
|
|
LicenseError,
|
|
NotActivatedError,
|
|
UnsupportedFeatureError,
|
|
)
|
|
from .features import FEATURES_BY_TIER, all_features_for_tier
|
|
from .manager import (
|
|
LicenseManager,
|
|
ProductionBuildError,
|
|
assert_production_safe,
|
|
current_state,
|
|
get_manager,
|
|
require_feature,
|
|
)
|
|
from .schema import FeatureFlag, License, Tier
|
|
|
|
__all__ = [
|
|
# Manager
|
|
"LicenseManager",
|
|
"ProductionBuildError",
|
|
"assert_production_safe",
|
|
"current_state",
|
|
"get_manager",
|
|
"require_feature",
|
|
# Schema
|
|
"FeatureFlag",
|
|
"License",
|
|
"Tier",
|
|
# Feature registry
|
|
"FEATURES_BY_TIER",
|
|
"all_features_for_tier",
|
|
# Errors
|
|
"LicenseError",
|
|
"NotActivatedError",
|
|
"ExpiredLicenseError",
|
|
"InvalidLicenseError",
|
|
"UnsupportedFeatureError",
|
|
]
|