#!/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)