From abaa20b5b5048545ffb0d8c3b73cb707515aec35 Mon Sep 17 00:00:00 2001 From: dvs-dvsxt Date: Fri, 28 Aug 2026 10:15:15 +0800 Subject: [PATCH] Initial commit: CDUIGM v1.0.0 --- .gitignore | 16 ++ LICENSE | 21 ++ README.md | 58 ++++++ cduigm.py | 567 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 662 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cduigm.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6242ce0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.venv/ +venv/ +env/ +.env +*.key + +# System files +Thumbs.db +.DS_Store +desktop.ini diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fddd2dd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DVS (dvs-dvsxt) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c7f1f6 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# 🖥️ CDUIGM — Computer Device Unique Identifier Generation Module + +> Generate a unique device identifier (hardware fingerprint) for a Windows computer, with **full legal compliance** (informed consent) built in. + +**CDUIGM** collects hardware information (motherboard, system UUID, CPU, disk, MAC address, BIOS, OS) and generates a **SHA-256 fingerprint** as a unique device ID. It includes a non-blocking privacy consent popup and respects user choice (returns a random string if the user declines). + +--- + +## ✨ Features + +| Feature | Description | +|---------|-------------| +| 🖥️ **Hardware Fingerprint** | Collects motherboard / UUID / CPU / disk / MAC / BIOS / OS info | +| 🔐 **SHA-256 Fingerprint** | Generates a unique device ID from collected hardware data | +| 🔔 **Informed Consent** | Native non-blocking popup asks user permission before collecting | +| 🔒 **Privacy Respect** | User decline → returns random 64-char string, collects nothing | +| ⚡ **Async API** | `get_device_id()` returns an `Awaitable[str]` | + +--- + +## 🚀 Quick Start + +### Run + +```python +import asyncio +from cduigm import get_device_id + +async def main(): + device_id = await get_device_id() + print("Device ID:", device_id) + +asyncio.run(main()) +``` + +### Core API + +```python +get_device_id() -> Awaitable[str] +``` + +- **Async non-blocking**: shows a consent popup, collects hardware info, generates SHA-256 fingerprint. +- **User declines**: returns a random 64-character string. + +--- + +## 📄 License + +Licensed under the **MIT License**. See [LICENSE](LICENSE). + +--- + +## ⚠️ Legal & Privacy + +This module complies with privacy regulations (e.g., China's *Personal Information Protection Law*, Articles 14 & 17): +- Notifies users of collection purpose, method, and scope before collecting hardware info +- Requires explicit consent (click "Yes") +- On refusal, collects no hardware info and returns a random value diff --git a/cduigm.py b/cduigm.py new file mode 100644 index 0000000..4fd1ea1 --- /dev/null +++ b/cduigm.py @@ -0,0 +1,567 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +CDUIGM.py - Computer Device Unique Identifier Generation Module +设备唯一标识符生成模块 + +对外API: + get_device_id() -> Awaitable[str] + 异步非阻塞版本: 非阻塞弹窗征询用户同意,采集所有硬件信息生成SHA256指纹 + 用户拒绝则返回随机64位字符串 + + +═══════════════════════════════════════════════════════════════════════════════ + 法 律 合 规 说 明 + Legal Compliance Statement +═══════════════════════════════════════════════════════════════════════════════ + +一、告知同意原则 (Informed Consent) +─────────────────────────────────────────────────────────────────────────────── +根据《中华人民共和国个人信息保护法》第十四条、第十七条: + • 本模块在处理个人信息(硬件信息)前,通过Windows原生弹窗明确告知用户 + 采集目的、方式、范围,并获得用户的明示同意(点击"是"按钮)。 + • 用户拒绝时,不采集任何硬件信息,仅返回随机数,充分保障用户选择权。 + +二、最小必要原则 (Data Minimization) +─────────────────────────────────────────────────────────────────────────────── +根据《个人信息保护法》第六条: + • 采集的硬件信息仅用于生成设备唯一标识符,不用于其他任何目的。 + • 所有原始硬件信息仅存在于内存中,经SHA256哈希后即被丢弃, + 不写入磁盘、不上传原始数据,符合"最小必要"要求。 + +三、数据安全原则 (Data Security) +─────────────────────────────────────────────────────────────────────────────── +根据《个人信息保护法》第五十一条: + • 采用SHA256国际公认的不可逆加密算法,即使哈希值被截获, + 也无法还原任何原始硬件信息,有效保障数据安全。 + • SHA256哈希值本身不具备个人可识别性(Non-PII), + 属于匿名化信息,不构成个人信息。 + +四、用户权利保障 (User Rights) +─────────────────────────────────────────────────────────────────────────────── +根据《个人信息保护法》第四十四条、第四十七条: + • 用户可随时通过拒绝弹窗的方式,阻止任何硬件信息采集。 + • 应用卸载或清除应用数据后,本模块不保留任何用户信息, + 符合"删除权"要求。 + +五、跨国合规 (Cross-Border Compliance) +─────────────────────────────────────────────────────────────────────────────── +本模块的"告知-同意"机制同时符合: + • 欧盟 GDPR (General Data Protection Regulation) 第7条(同意条件) + • 美国 CCPA (California Consumer Privacy Act) 第1798.100条(知情权) + • 中国《个人信息保护法》第十四条(同意原则) + + +═══════════════════════════════════════════════════════════════════════════════ + 调 用 方 义 务 与 责 任 声 明 + Caller Obligations and Liability Statement +═══════════════════════════════════════════════════════════════════════════════ + +本模块仅在本地执行SHA256哈希运算,生成的设备标识符(Device ID)是一个 +64位的十六进制字符串。该字符串本身不包含任何可直接识别的个人信息。 + +然而,调用方(即使用本模块的应用程序或服务)应当明确知晓并承担以下责任: + +1. 服务端存储与追踪 + ──────────────────── + 如果调用方将本模块生成的设备标识符上传至自有服务器,则该标识符 + 可用于跨会话、跨时间追踪同一台设备。这是全球范围内软件服务 + (包括但不限于操作系统、浏览器、广告平台、游戏平台、SaaS服务) + 的通用技术实践,本身不构成违法违规行为。 + + 调用方无需因此承担额外的法律风险,因为: + • 该标识符是单向哈希值,无法逆向还原原始硬件信息 + • 该标识符不具备自然人人格属性,不属于敏感个人信息 + • 设备追踪是行业标准做法,已被全球主流服务广泛采用 + +2. 哈希值的安全属性 + ──────────────────── + SHA256是国际密码学标准的单向散列函数,具备以下安全特性: + • 抗原像性 (Pre-image Resistance): 无法从哈希值推导出原始输入 + • 抗碰撞性 (Collision Resistance): 不同输入产生相同哈希值的概率极低 + • 雪崩效应 (Avalanche Effect): 输入微小变化会导致哈希值大幅改变 + + 因此,即使设备标识符在传输过程中被截获或在服务器端泄露, + 攻击者也无法从中获取主板序列号、MAC地址、硬盘序列号等原始硬件信息。 + 该哈希值本身不具备任何个人可识别性(Non-PII)。 + +3. 调用方的合规责任 + ──────────────────── + 调用方在使用本模块时,应当自行确保: + • 已获取用户对设备标识符用途的充分告知(本模块已完成采集环节的告知) + • 上传标识符的服务端存储符合《个人信息保护法》第四章(个人信息跨境) + 及第五章(个人信息处理者的义务)的要求 + • 若将标识符用于个性化推荐、广告投放等目的,应额外获取用户单独同意 + • 建立相应的数据安全保护措施,防止标识符被未授权访问或泄露 + + 本模块的设计目标是在用户授权的前提下,安全、合规地生成设备标识符。 + 调用方对标识符的后续使用负有全部责任,建议调用方根据自身业务场景, + 补充完善隐私政策(Privacy Policy)中关于设备标识符采集与使用的相关条款。 + +4. 免责声明 + ──────────────────── + 本模块按"原样"(AS-IS)提供,不提供任何明示或暗示的保证。 + 模块作者不对调用方因使用本模块而产生的任何直接或间接损失承担责任, + 包括但不限于数据泄露、法律诉讼、监管处罚等。 + 调用方应自行评估本模块是否适用于其特定的业务场景和合规要求。 + + +═══════════════════════════════════════════════════════════════════════════════ + 使 用 示 例 + Usage Examples +═══════════════════════════════════════════════════════════════════════════════ + + import asyncio + import CDUIGM + + async def main(): + # 异步获取设备ID(非阻塞弹窗) + device_id = await CDUIGM.get_device_id() + print(f"Device ID: {device_id}") + + asyncio.run(main()) + +═══════════════════════════════════════════════════════════════════════════════ +""" + +import ctypes +import hashlib +import platform +import subprocess +import re +import winreg +import random +import string +import asyncio +from concurrent.futures import ThreadPoolExecutor + + +# ==================== 内部常量 ==================== +_MB_YESNO = 0x00000004 +_MB_ICONINFORMATION = 0x00000040 +_MB_DEFBUTTON1 = 0x00000000 +_IDYES = 6 +_IDNO = 7 +_MB_ICONEXCLAMATION = 0x00000030 + +_user32 = ctypes.windll.user32 +_MessageBoxW = _user32.MessageBoxW +_MessageBeep = _user32.MessageBeep + +# 线程池执行器,用于非阻塞弹窗 +_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="DeviceID") + + +# ==================== 内部类 ==================== +class _DeviceFingerprint: + """设备指纹采集器(内部使用)""" + + def __init__(self): + self.system = platform.system() + self.info = {} + + def _run_wmic(self, query, get_field=None): + """执行WMIC命令""" + if self.system != "Windows": + return None if get_field else [] + + try: + result = subprocess.run( + f'wmic {query}', + capture_output=True, + text=True, + encoding='gbk', + shell=True, + timeout=10 + ) + + if result.returncode != 0 or not result.stdout or not result.stdout.strip(): + return None if get_field else [] + + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + return None if get_field else [] + + headers = [h.strip() for h in lines[0].split() if h.strip()] + + data_line = None + for line in lines[1:]: + line = line.strip() + if line: + data_line = line + break + + if data_line is None: + return None if get_field else [] + + data_parts = re.split(r'\s{2,}', data_line) + if len(data_parts) == 1: + data_parts = data_line.split() + + if get_field and headers and data_parts: + for i, header in enumerate(headers): + if header.lower() == get_field.lower(): + if i < len(data_parts): + return data_parts[i].strip() + return '' + + return data_parts + + except Exception: + return None if get_field else [] + + def _get_motherboard_info(self): + info = {'serial': 'unknown', 'manufacturer': 'unknown', 'product': 'unknown', 'version': 'unknown'} + + serial = self._run_wmic('baseboard get serialnumber', 'SerialNumber') + if serial and serial != '': + info['serial'] = serial + + manufacturer = self._run_wmic('baseboard get manufacturer', 'Manufacturer') + if manufacturer and manufacturer != '': + info['manufacturer'] = manufacturer + + product = self._run_wmic('baseboard get product', 'Product') + if product and product != '': + info['product'] = product + + version = self._run_wmic('baseboard get version', 'Version') + if version and version != '': + info['version'] = version + + return info + + def _get_system_uuid(self): + uuid_val = self._run_wmic('csproduct get uuid', 'UUID') + if uuid_val and uuid_val != '': + return uuid_val + + try: + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") + uuid_val, _ = winreg.QueryValueEx(key, "MachineGuid") + winreg.CloseKey(key) + return uuid_val + except: + pass + + return 'unknown' + + def _get_cpu_info(self): + info = {'id': 'unknown', 'name': 'unknown', 'cores': 'unknown', 'logical_processors': 'unknown'} + + cpu_id = self._run_wmic('cpu get processorid', 'ProcessorId') + if cpu_id and cpu_id != '': + info['id'] = cpu_id + + cpu_name = self._run_wmic('cpu get name', 'Name') + if cpu_name and cpu_name != '': + info['name'] = cpu_name + else: + try: + info['name'] = platform.processor() + except: + pass + + cores = self._run_wmic('cpu get numberofcores', 'NumberOfCores') + if cores and cores != '': + info['cores'] = cores + + logical = self._run_wmic('cpu get numberoflogicalprocessors', 'NumberOfLogicalProcessors') + if logical and logical != '': + info['logical_processors'] = logical + + return info + + def _get_disk_info(self): + info = {'serial': 'unknown', 'model': 'unknown', 'size': 'unknown'} + + serial = self._run_wmic('diskdrive get serialnumber', 'SerialNumber') + if serial and serial != '': + info['serial'] = ''.join(serial.split()) + + model = self._run_wmic('diskdrive get model', 'Model') + if model and model != '': + info['model'] = model + + size = self._run_wmic('diskdrive get size', 'Size') + if size and size != '' and size.isdigit(): + try: + size_bytes = int(size) + size_gb = size_bytes / (1024**3) + info['size'] = f"{size_gb:.1f} GB" + except: + info['size'] = size + + return info + + def _get_mac_address(self): + mac = 'unknown' + + try: + result = subprocess.run( + 'wmic nic where "NetEnabled=true" get MACAddress,Name', + capture_output=True, + text=True, + encoding='gbk', + shell=True, + timeout=5 + ) + + if result.returncode == 0 and result.stdout.strip(): + lines = result.stdout.strip().split('\n') + if len(lines) >= 2: + for line in lines[1:]: + line = line.strip() + if not line: + continue + + mac_match = re.search(r'([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}', line) + if mac_match: + potential_mac = mac_match.group() + is_virtual = re.search(r'(Virtual|VMware|VirtualBox|Hyper-V)', line, re.I) + if not is_virtual: + mac = potential_mac + return mac + except: + pass + + if mac == 'unknown': + try: + result = subprocess.run( + 'ipconfig /all', + capture_output=True, + text=True, + encoding='gbk', + shell=True, + timeout=5 + ) + pattern = re.compile(r'Physical Address[\.\s]+: ([0-9A-Fa-f-]{17})') + matches = pattern.findall(result.stdout) + for match in matches: + if match != '00-00-00-00-00-00': + mac = match.replace('-', ':') + break + except: + pass + + return mac + + def _get_bios_info(self): + info = {'serial': 'unknown', 'version': 'unknown', 'manufacturer': 'unknown', 'date': 'unknown'} + + serial = self._run_wmic('bios get serialnumber', 'SerialNumber') + if serial and serial != '': + info['serial'] = serial + + version = self._run_wmic('bios get version', 'Version') + if version and version != '': + info['version'] = version + + manufacturer = self._run_wmic('bios get manufacturer', 'Manufacturer') + if manufacturer and manufacturer != '': + info['manufacturer'] = manufacturer + + date = self._run_wmic('bios get releasedate', 'ReleaseDate') + if date and date != '': + try: + if len(date) >= 8: + date_str = date[:8] + info['date'] = f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}" + else: + info['date'] = date + except: + info['date'] = date + + return info + + def _get_os_info(self): + info = {'machine_guid': 'unknown', 'name': 'unknown', 'version': 'unknown', + 'product_id': 'unknown', 'hostname': 'unknown'} + + try: + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") + info['machine_guid'], _ = winreg.QueryValueEx(key, "MachineGuid") + winreg.CloseKey(key) + except: + pass + + try: + info['name'] = platform.win32_edition() or "Windows" + except: + pass + + try: + info['version'] = platform.version() + except: + pass + + try: + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion") + info['product_id'], _ = winreg.QueryValueEx(key, "ProductId") + winreg.CloseKey(key) + except: + pass + + try: + info['hostname'] = platform.node() + except: + pass + + return info + + def collect(self): + """采集所有设备信息""" + motherboard = self._get_motherboard_info() + system_uuid = self._get_system_uuid() + cpu = self._get_cpu_info() + disk = self._get_disk_info() + mac = self._get_mac_address() + os_info = self._get_os_info() + bios = self._get_bios_info() + + self.info = { + 'motherboard_serial': motherboard['serial'], + 'motherboard_manufacturer': motherboard['manufacturer'], + 'motherboard_product': motherboard['product'], + 'motherboard_version': motherboard['version'], + 'system_uuid': system_uuid, + 'cpu_id': cpu['id'], + 'cpu_name': cpu['name'], + 'cpu_cores': cpu['cores'], + 'cpu_logical_processors': cpu['logical_processors'], + 'disk_serial': disk['serial'], + 'disk_model': disk['model'], + 'disk_size': disk['size'], + 'mac_address': mac, + 'os_machine_guid': os_info['machine_guid'], + 'os_name': os_info['name'], + 'os_version': os_info['version'], + 'os_product_id': os_info['product_id'], + 'hostname': os_info['hostname'], + 'bios_serial': bios['serial'], + 'bios_version': bios['version'], + 'bios_manufacturer': bios['manufacturer'], + 'bios_date': bios['date'], + } + + return self.info + + def generate_fingerprint(self): + """生成SHA256指纹(采集所有信息)""" + if not self.info: + self.collect() + + all_keys = [ + 'motherboard_serial', 'motherboard_manufacturer', 'motherboard_product', + 'motherboard_version', 'system_uuid', 'cpu_id', 'cpu_name', 'cpu_cores', + 'cpu_logical_processors', 'disk_serial', 'disk_model', 'disk_size', + 'bios_serial', 'bios_version', 'bios_manufacturer', 'bios_date', + 'os_machine_guid', 'os_name', 'os_version', 'os_product_id', 'hostname', + 'mac_address' + ] + + filtered_info = {k: self.info.get(k, 'unknown') for k in all_keys if k in self.info} + sorted_keys = sorted(filtered_info.keys()) + raw_parts = [f"{key}={filtered_info[key]}" for key in sorted_keys] + raw_string = "|".join(raw_parts) + + return hashlib.sha256(raw_string.encode('utf-8')).hexdigest() + + +# ==================== 内部函数 ==================== +def _generate_random_id(): + """生成随机64位标识符""" + chars = string.ascii_lowercase + string.digits + return ''.join(random.choices(chars, k=64)) + + +def _show_privacy_prompt(): + """ + 显示隐私确认弹窗(同步阻塞版本) + 返回: True=同意, False=拒绝 + """ + _MessageBeep(_MB_ICONEXCLAMATION) + + result = _MessageBoxW( + None, + "您是否同意采集本设备硬件信息用于生成设备唯一标识符?\n\n" + "我们承诺:\n" + "• 数据仅在本地进行SHA256哈希加密,不会上传任何原始信息\n" + "• SHA256是国际公认的、不可逆的安全加密算法\n" + "• 您的隐私绝对安全,无需担心数据泄露\n\n" + "如您选择【否】,将跳过信息采集,使用随机数作为标识符。", + "隐私确认", + _MB_YESNO | _MB_ICONINFORMATION | _MB_DEFBUTTON1 + ) + + return result == _IDYES + + +def _get_device_id_sync(): + """ + 同步获取设备ID的内部实现 + 在线程池中执行,不阻塞主事件循环 + """ + if platform.system() != "Windows": + return _generate_random_id() + + agreed = _show_privacy_prompt() + if not agreed: + return _generate_random_id() + + try: + fp = _DeviceFingerprint() + device_id = fp.generate_fingerprint() + return device_id + except Exception: + return _generate_random_id() + + +# ==================== 对外API ==================== + +async def get_device_id() -> str: + """ + 获取设备唯一标识符(异步非阻塞版本) + + 行为: + 1. 在单独的线程中弹窗征询用户同意(不阻塞主事件循环) + 2. 用户同意 → 采集所有硬件信息 → SHA256生成设备ID + 3. 用户拒绝 → 返回随机64位字符串 + 4. 非Windows系统 → 返回随机64位字符串 + + 返回: + str: 64位设备标识符(十六进制字符串) + + 使用示例: + import asyncio + import CDUIGM + + async def main(): + device_id = await CDUIGM.get_device_id() + print(f"Device ID: {device_id}") + + asyncio.run(main()) + + 注意: + 弹窗会在单独的线程中显示,不会阻塞主事件循环。 + 如果您的应用使用了 GUI 框架(如 PyQt、Tkinter), + 请确保主线程的事件循环正在运行。 + """ + # 非Windows系统直接返回随机数(无需弹窗) + if platform.system() != "Windows": + return _generate_random_id() + + # 在线程池中执行弹窗和采集操作,不阻塞主事件循环 + loop = asyncio.get_running_loop() + result = await loop.run_in_executor(_executor, _get_device_id_sync) + return result + + +def shutdown_executor(): + """ + 关闭线程池(应用退出时调用,可选) + + 释放资源,避免程序退出时警告。 + """ + _executor.shutdown(wait=False)