Metadata-Version: 2.4
Name: icare-licensing
Version: 1.3.0
Summary: Offline-first, cryptographically secure licensing client for Python apps using the Icare Licensing Server
Author: Icare Dev Team
License-Expression: MIT
Project-URL: Homepage, https://github.com/icare-dev/icare-licensing
Project-URL: Bug Tracker, https://github.com/icare-dev/icare-licensing/issues
Project-URL: Changelog, https://github.com/icare-dev/icare-licensing/blob/main/README.md#changelog
Keywords: licensing,software-licensing,license-management,rsa,cryptography,offline-licensing,hardware-binding,drm
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Security :: Cryptography
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.25.0
Requires-Dist: cryptography>=3.0.0

# Icare Licensing Client

`icare-licensing` is an **offline-first, cryptographically secure** licensing client library for Python applications. It integrates seamlessly with the Icare Licensing Server using RSA-SHA256 signatures, OS-native stable hardware bindings, and automated offline-resilient sync policies.

[![PyPI version](https://img.shields.io/pypi/v/icare-licensing)](https://pypi.org/project/icare-licensing/)
[![Python](https://img.shields.io/pypi/pyversions/icare-licensing)](https://pypi.org/project/icare-licensing/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)

---

## Features

- **OS-Native Stable Hardware Binding (HWID)**: Generates a stable, reproducible machine signature using OS-level identifiers — not fragile MAC addresses.
- **Offline Cryptographic Validation**: Validates license state offline using RSA-SHA256 without calling the server on every startup.
- **Automated Sync Daemon**: Runs in the background and pings the verification endpoint every 24 hours.
- **3-Day Offline Grace Period**: Remains fully functional offline for up to 3 days before requiring internet.
- **Instant Revocation Purge**: Deletes local signatures and metadata immediately upon server-side license revocation or deletion to prevent offline bypass.

---

## Installation

```bash
pip install icare-licensing
```

---

## Hardware ID (HWID) Strategy

The library generates a stable, cross-platform `HWID` using the following priority chain:

| Priority | Platform | Source |
|---|---|---|
| 1st | **Windows** | `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` — set once at OS install |
| 2nd | **Linux** | `/etc/machine-id` — set once at first boot |
| 3rd | **macOS** | `IOPlatformSerialNumber` from `ioreg` |
| 4th | **All (fallback)** | MAC address — **only if hardware-backed** (not a randomly generated address) |

> **Why not `uuid.getnode()`?**
> Python's `uuid.getnode()` silently returns a **random 48-bit number** when no hardware network interface is found (e.g. in VMs, Docker containers, or systems with only virtual NICs). This would cause a legitimately activated license to become invalid on the next restart. This library detects and rejects randomly-generated MAC addresses automatically.

---

## License Storage & File Location Policy

By default, the client writes `license.lic` and `license_metadata.json` relative to the **Current Working Directory (CWD)**.

### Critical Production Guidelines

For desktop or system-wide deployments, always initialize with an **absolute path** pointing to a user-writable directory:

```python
import os
from icare_licensing import LicensingClient

app_folder = os.path.join(os.path.expanduser("~"), ".my_app")
os.makedirs(app_folder, exist_ok=True)

client = LicensingClient(
    server_url="https://licensing.mycompany.com",
    license_file_path=os.path.join(app_folder, "license.lic")
)
```

> The metadata file (`license_metadata.json`) is automatically created in the same directory as `license.lic`.

---

## Licensing States

| Constant | Meaning |
|---|---|
| `STATUS_VALID` | Cryptographically valid, correct HWID, not expired, synced |
| `STATUS_MISSING` | No `license.lic` file found — device needs activation |
| `STATUS_EXPIRED` | License expiration date has passed |
| `STATUS_INVALID` | Signature corrupt, HWID mismatch, or actively revoked on server |
| `STATUS_REQUIRES_SYNC` | Offline for longer than `max_offline_days` — internet required |
| `STATUS_UNKNOWN` | Initial unresolved state |

---

## Core Developer API

### `LicensingClient(server_url, license_file_path, status_callback, max_offline_days)`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `server_url` | `str` | **required** | Base URL of the Icare Licensing Server |
| `license_file_path` | `str` | `"license.lic"` | Absolute path to store the signed license key |
| `status_callback` | `callable` | `None` | Function called when license status transitions |
| `max_offline_days` | `int` | `3` | Days the app can run offline before `REQUIRES_SYNC` |

### Methods

| Method | Description |
|---|---|
| `get_hardware_id()` | Returns the stable OS-native HWID for this machine |
| `activate_device(key)` | Binds an activation key to this machine's HWID. Returns `(bool, str)` |
| `start_monitoring(interval_seconds)` | Starts the background daemon thread (default: every 5 minutes) |
| `stop_monitoring()` | Stops the background daemon thread cleanly |
| `runCheck()` | Manually triggers a verification check |

---

## Integration Template

```python
import os
import sys
from icare_licensing import (
    LicensingClient,
    STATUS_VALID,
    STATUS_REQUIRES_SYNC,
    STATUS_MISSING,
    STATUS_INVALID,
    STATUS_EXPIRED
)

def on_license_status_change(new_status):
    """
    Callback triggered when the license status transitions.
    NOTE: If running a GUI application (e.g., Tkinter/PyQt), route
    UI-altering updates safely to the main thread.
    """
    if new_status == STATUS_VALID:
        print("License valid — premium features unlocked.")

    elif new_status == STATUS_REQUIRES_SYNC:
        print("Warning: internet connection required to re-verify license.")

    elif new_status in (STATUS_MISSING, STATUS_INVALID, STATUS_EXPIRED):
        print("No valid license. Redirecting to activation screen.")
        # Lock down core software capabilities here

def main():
    # 1. Setup secure, writable storage path
    app_folder = os.path.join(os.path.expanduser("~"), ".my_app")
    os.makedirs(app_folder, exist_ok=True)

    # 2. Instantiate client
    client = LicensingClient(
        server_url="http://localhost:8081",
        license_file_path=os.path.join(app_folder, "license.lic"),
        status_callback=on_license_status_change,
        max_offline_days=3
    )

    # 3. Force an immediate check on startup
    client._run_check()

    if client.current_status == STATUS_MISSING:
        key = input("Enter Activation Key: ")
        success, msg = client.activate_device(key)
        print(msg)

    # 4. Start background protection thread (checks every 5 minutes)
    client.start_monitoring(interval_seconds=300)

if __name__ == "__main__":
    main()
```

---

## Changelog

### v1.3.0
- **[Breaking Fix]** Replaced `uuid.getnode()`-based HWID with OS-native stable machine identifiers:
  - Windows: `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid`
  - Linux: `/etc/machine-id`
  - macOS: `IOPlatformSerialNumber`
  - Fallback: Hardware-backed MAC address only (random MACs are detected and rejected)
- Removed `platform.processor()` and `platform.machine()` from HWID computation (they add no uniqueness)
- Added explicit `RuntimeError` instead of silently returning an unstable HWID

### v1.2.0
- Initial public release
- RSA-SHA256 offline signature verification
- 24h background sync daemon
- 3-day offline grace period
- Instant revocation purge on server rejection
- 5-retry online sync with 10s backoff

---

## License

MIT — see [LICENSE](LICENSE) for details.
