Metadata-Version: 2.4
Name: altifyhwid
Version: 1.1.0
Summary: Windows hardware identity matching across three or more hardware categories
Author: Altify
Keywords: hardware,hwid,windows,device,identity
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: Microsoft :: Windows
Classifier: Topic :: System :: Hardware
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# Altify HWID

Recognize a Windows device using three or more matching hardware categories.
Python 3.10 or newer. No third-party runtime dependencies.

## Install

After the maintainer publishes this project to PyPI:

```shell
python -m pip install altifyhwid
```

The PyPI distribution name is `altifyhwid`; the Python import is `altifyhwid`.

## Use

```python
import altifyhwid as hwid

device_id = hwid.get_id()
print(device_id)

result = hwid.identify()
print(result.to_dict())
```

Hardware collection is cached for 60 seconds per process. Database matching
still runs on each call. When both the ID and details are needed, call
`identify()` once and read `result.device_id`. Use `get_id(cache_ttl=0)` for a
fresh scan or `clear_cache()` to discard cached snapshots.

```python
from altifyhwid import hwid

print(hwid())
```

Default database on Windows:
`%LOCALAPPDATA%\Altify\HWID\devices.sqlite3`.
Preserve this database to preserve device IDs. Override it with `db_path` or
the `ALTIFY_HWID_DB` environment variable. Explicit `db_path` takes priority.

```python
import altifyhwid

# Reuse the registry created by the earlier standalone script:
device_id = altifyhwid.get_id(
    db_path=r"C:\MyApp\settings\AltifyHWID\devices.sqlite3"
)

# Match only, never write (no enrollment, no refresh, no last_seen update):
device_id = altifyhwid.get_id(read_only=True, create=False)
```

`identify()` accepts `create=False` (match only), `refresh=False` (do not
store newly seen hardware on a match), and `read_only=True` (open the
database read-only; implies both). `IdentityResult.stored_kinds` lists the
identifier kinds written to the registry by that call.

## Command line

```shell
python -m altifyhwid --json
python -m altifyhwid scan
python -m altifyhwid scan --raw
python -m altifyhwid --no-tpm --no-monitors
python -m altifyhwid list
python -m altifyhwid merge KEEP_ID ABSORB_ID
python -m altifyhwid delete DEVICE_ID
python -m altifyhwid -v
altifyhwid --json
```

`scan` reports categories, counts, and collection warnings without saving an
identity. `--raw` also prints hardware identifiers and diagnostic context.
Programmatic diagnostic context is available with
`collect(include_diagnostics=True)`. TPM is queried by default, but a missing
or inaccessible endorsement public key contributes no match evidence.
`--no-refresh`, `--no-create`, and `--read-only` mirror the API flags.
`-v` logs collector timings, PowerShell exit codes with a stderr tail, and
registry activity to stderr. Exit code 0 is success, 2 is a reported error
(JSON on stderr), 1 is an unexpected error. Console entry point
`altifyhwid` requires the Scripts directory of the environment on PATH; the
`python -m altifyhwid` form works without that extra PATH requirement.

## Matching

The categories are firmware, storage, RAM, network, display, TPM public key,
and CPU serial where available. Related SMBIOS fields together count as one
firmware vote. Multiple disks or RAM modules do not multiply category votes.
CPU/GPU models and ordinary CPU ProcessorId values do not count as unique IDs.

A new enrollment gets a random UUID. Later scans reuse it when at least three
categories match that enrollment. Use `min_matches=4` or higher to require
more evidence.

On a match the registry also stores identifiers it has not seen before for
that device (a replaced disk, new RAM, a new network adapter), so gradual
hardware changes keep the same ID as long as each scan still matches three
categories. Old identifiers are kept, so a reverted swap still matches. Each
device keeps at most 128 identifiers per kind; beyond that the oldest ones not
present in the current scan are dropped. Pass `refresh=False` to disable this.

Insufficient usable categories raise `InsufficientHardwareError`. `create=False`
requires an existing match and otherwise raises `NoMatchError`.

Multiple qualifying records raise `AmbiguousMatchError`; `.candidates` lists
the two strongest, best first, each with `match_count`. Resolve it once with
the registry tools below (usually `merge` the weaker into the stronger).

## Registry maintenance

```python
from altifyhwid import Registry

registry = Registry()                     # same defaults as identify()
registry.list_devices()                   # dicts with device_id, created_at,
                                          # last_seen, token_count, groups, kinds
registry.list_devices(all_namespaces=True)
registry.merge("ALTIFY-KEEP...", "ALTIFY-DUPLICATE...")
registry.delete("ALTIFY-OLD...")
registry.backup(r"D:\backups\devices.sqlite3")
```

`merge` moves the identifiers of the absorbed enrollments onto the kept one and
removes the absorbed records. Both must be in the namespace of the registry.
`delete` and `merge` refuse a `read_only=True` registry.

## Integrity, backups, and schema

Each registry path is checked with `PRAGMA quick_check` once per process. A
corrupt or non-SQLite file raises `RegistryError` naming the newest backup.

After any change to enrollments (a new device, stored identifiers, merge,
delete) the registry writes `devices.sqlite3.bak1` next to the database, at
most once per `backup_interval` seconds (default one day), and rotates older
copies to `.bak2` and so on up to `backup_copies` (default 2; 0 disables).
Backup failures are logged and never fail the identity call. To restore, stop
the application and copy `.bak1` over `devices.sqlite3`.

The database schema is version 2. A schema 1 registry (created by 1.0.x) is
migrated automatically on the first writable open; a `read_only=True` open
refuses it until then. A registry written by a newer module version is refused
rather than modified.

Only HMAC tokens are persisted, but the key is in the same database: this is
pseudonymization, not encryption. No data is uploaded by this module. Collection
uses read-only Windows queries, bounded worker threads, and timeouts. TPM and
some provider queries can be unavailable under ordinary user permissions.

## Logging

The module logs to `logging.getLogger("altifyhwid")` and stays silent unless
the application configures logging. `WARNING` covers failed collector batches
(exit code, duration, last 2 KiB of PowerShell stderr), ambiguous matches,
rollback problems, and failed automatic backups. `INFO` covers enrollments,
migrations, backups, and scan summaries. `DEBUG` covers per-batch timings and
match details. Hardware identifiers are never logged.

## Scope

Hardware collection requires Windows. Registry matching can run elsewhere with
an explicitly supplied `HardwareSnapshot`. Separate registry databases produce
separate IDs; installing this package does not create a shared identity service.
For multiple clients, use a trusted central registry through your own
authenticated transport. Client-supplied hardware values are not attestation.

This is heuristic recognition, not a guarantee of uniqueness or resistance to
spoofing. Shared components, OEM duplicates, cloned VMs, and extensive hardware
changes can produce incorrect matches or new IDs. Storing newly seen hardware
on a match also means a wrong match teaches the wrong record; raise
`min_matches` or set `refresh=False` where that matters. Proprietary kernel
anti-cheat internals are not implemented.

## Resource limits

Collection uses at most four concurrent workers and an overall deadline. One
scan runs at a time per process; a caller waits up to `timeout` seconds for an
active scan, then gets the cached result when the same options were scanned,
or runs its own scan with a fresh deadline. Cache reads and `clear_cache()`
never wait behind a scan. The scan cache holds at most eight immutable
snapshots. No workers run between calls. Provider results are limited to 128
rows and Python reads at most 2 MiB of output per collector batch. SQLite
connections and temporary output files close when each operation completes.

The registry uses SQLite WAL mode, so readers never block the single writer.
Opening an existing registry takes no write lock; only enrollment, refresh,
merge, delete, and migration do. Backup has a deadline and removes incomplete
output on failure. Matching uses an index and retrieves at most two qualifying
records to detect ambiguity.

## Tests

```shell
py -m unittest discover -s tests -v
```

The suite covers normalization, snapshot building from recorded provider
output, enrollment and matching, refresh and its cap, ambiguity and merge,
read-only mode, schema migration, integrity failures, backups and rotation,
collector caching and locking, PowerShell failure handling, and the command
line. It runs without hardware access. `ALTIFY_LIVE=1` adds one live
collection on the current Windows PC.
