datamask-core 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datamask_core-1.0.0/PKG-INFO +16 -0
- datamask_core-1.0.0/pyproject.toml +30 -0
- datamask_core-1.0.0/setup.cfg +4 -0
- datamask_core-1.0.0/src/datamask_core/__init__.py +2 -0
- datamask_core-1.0.0/src/datamask_core/blacklist.py +103 -0
- datamask_core-1.0.0/src/datamask_core/config.py +54 -0
- datamask_core-1.0.0/src/datamask_core/entity_crypto.py +164 -0
- datamask_core-1.0.0/src/datamask_core/entity_mapping_store.py +486 -0
- datamask_core-1.0.0/src/datamask_core/entity_types.py +82 -0
- datamask_core-1.0.0/src/datamask_core/inference.py +126 -0
- datamask_core-1.0.0/src/datamask_core/logger.py +28 -0
- datamask_core-1.0.0/src/datamask_core/paths.py +86 -0
- datamask_core-1.0.0/src/datamask_core/patterns.py +741 -0
- datamask_core-1.0.0/src/datamask_core/pipeline.py +798 -0
- datamask_core-1.0.0/src/datamask_core/py.typed +0 -0
- datamask_core-1.0.0/src/datamask_core/regex_masker.py +722 -0
- datamask_core-1.0.0/src/datamask_core/tokenization.py +263 -0
- datamask_core-1.0.0/src/datamask_core.egg-info/PKG-INFO +16 -0
- datamask_core-1.0.0/src/datamask_core.egg-info/SOURCES.txt +19 -0
- datamask_core-1.0.0/src/datamask_core.egg-info/dependency_links.txt +1 -0
- datamask_core-1.0.0/src/datamask_core.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datamask-core
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: DataMask 核心规则引擎 — 正则脱敏、实体识别、NER Pipeline,零外部依赖
|
|
5
|
+
Author-email: TianluAudit <contact@datamask.cn>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://datamask.cn
|
|
8
|
+
Project-URL: Documentation, https://datamask.cn/docs
|
|
9
|
+
Keywords: data-masking,privacy,NER,entity-recognition,FPE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.9
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "datamask-core"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "DataMask 核心规则引擎 — 正则脱敏、实体识别、NER Pipeline,零外部依赖"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = {text = "Proprietary"}
|
|
11
|
+
authors = [{name = "TianluAudit", email = "contact@datamask.cn"}]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3.9",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
]
|
|
20
|
+
keywords = ["data-masking", "privacy", "NER", "entity-recognition", "FPE"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://datamask.cn"
|
|
24
|
+
Documentation = "https://datamask.cn/docs"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.package-dir]
|
|
27
|
+
"" = "src"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DataMask 误识别黑名单
|
|
3
|
+
从格力电器2024年报模型原始输出统计提取(2026-05-16)
|
|
4
|
+
"""
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
# ══ 误识别黑名单(出现频次 >= 3 的高频误识别词)═══
|
|
8
|
+
FP_BLACKLIST = set([
|
|
9
|
+
# 多字通用词/碎片(ORG 高频误识别)
|
|
10
|
+
'有限公司', '展有限公', '公司有', '份有限', '网科技', '力电器',
|
|
11
|
+
'广东省', '年度报', '珠海市', '监事会', '董事会', '有限公',
|
|
12
|
+
'器有限', '投资者', '人民币', '管理层', '有关方', '上市',
|
|
13
|
+
'股东', '上线', '格信', '失控', '科学', '年度', '全国',
|
|
14
|
+
'师事', '全省', '网络', '风险', '中国', '全市', '相关',
|
|
15
|
+
'港元', '所有', '有限', '有关', '缩机', '全区', '市股',
|
|
16
|
+
'并成', '企业', '器建', '市场', '展多', '京海', '线机',
|
|
17
|
+
'技能', '日文', '能力', '集团', '主要', '事会', '中华',
|
|
18
|
+
'能源', '国家', '欧元', '重要', '美元', '中文', '珠海',
|
|
19
|
+
'技术', '广东', '电机', '多个', '公司', '节能', '英文',
|
|
20
|
+
# 2026-05-16 全量测试模型碎片TOP(年报目录/短语碎片)
|
|
21
|
+
'动有', '上市公司股', '上市公司股东', '普通股股东', '普通股股', '普通股',
|
|
22
|
+
'除限', '简介', '及股', '展机', '中有', ':中', '金融机', '联中',
|
|
23
|
+
'母公司所有', '母公', '讨论与分', '和社会责', ' 格力电', '上能',
|
|
24
|
+
# 2026-05-16 人工抽检发现的通用名词碎片
|
|
25
|
+
'再生资源基地', '各基地', '第三方电商平台', '自建线上平台', '助力集团',
|
|
26
|
+
'助力', '会计准则', '会计数据', '境内外会计准则下会计数据',
|
|
27
|
+
# 2026-05-16 第二批人工抽检误识别
|
|
28
|
+
'浮电机系统', '鉴定委员会', '中国电工技术学会鉴定委员会',
|
|
29
|
+
'以用户需求为中心', '卓越级智能', '打造行业领先的智能', '行业领先的智能',
|
|
30
|
+
'加速智能', '股票上市证券交易所', '光照五大系统的智能', '五大系统的智能',
|
|
31
|
+
# 单字碎片(模型 WordPiece 边界导致的高频误识别)
|
|
32
|
+
'线', '外', '份', '现', '场', '有', '罗', '峰', '高', '速',
|
|
33
|
+
'红', '东', '路', '小', '何', '广', '监', '波', '区', '技',
|
|
34
|
+
'讯', '林', '百', '北', '大', '中', '超', '重', '军', '格',
|
|
35
|
+
'曾', '电', '公', '失', '网', '易', '新', '李', '家', '业',
|
|
36
|
+
'创', '局', '所', '险', '督', '立', '安', '行', '器', '师',
|
|
37
|
+
'人', '学', '术', '无', '上', '国', '深', '普', '末', '名',
|
|
38
|
+
'张', '股', '海', '集', '空', '珠', '节', '府', '任', '机',
|
|
39
|
+
'多', '年', '世', '控', '周', '事', '强', '理', '美', '市',
|
|
40
|
+
'能', '敏', '胡', '会', '刘', '为', '司', '省', '平', '限',
|
|
41
|
+
'健', '建', '展', '王', '力', '基', '委', '动', '华', '金',
|
|
42
|
+
'解',
|
|
43
|
+
# 数字/标点碎片
|
|
44
|
+
'00', ',0', ',', '、', ',', '3', '0', '1', '6', '4', '5', '7', '"',
|
|
45
|
+
# ── W1.5 P1-1 新增:ORG FP 高频误识词(基于 455 条评估集分析)──
|
|
46
|
+
# 通用后缀误识
|
|
47
|
+
'投资有限公司', # 4次 - "沃尔玛(中国)投资有限公司" 上下文无中国时被误识
|
|
48
|
+
'高级工程', # 4次 - "高级工程师" 截断(应识别为 PER,不是 ORG)
|
|
49
|
+
'增值税电子', # 3次 - "增值税电子普通发票" 错位
|
|
50
|
+
'人工智能', # 3次 - "人工智能算法" 误识
|
|
51
|
+
'信用等', # 2次 - "信用等级" 截断
|
|
52
|
+
'税务代理', # 1次 - "税务代理" 误识
|
|
53
|
+
'缴金额', # 1次 - "缴金额" 误识
|
|
54
|
+
'例2', # 1次 - 数字+字 误识
|
|
55
|
+
',您', # 1次 - 标点+字 误识
|
|
56
|
+
'581209号\n纳税', # 1次 - 数字+换行+税词
|
|
57
|
+
# ORG 行业词误识("高级工程师"前出现的行业名词被误吞)
|
|
58
|
+
'东莞数据', '佛山数据', '武汉数据', '杭州数据', # "X市数据" 截断
|
|
59
|
+
'苏州市工业', '深圳市南山区数据', '北京市海淀区电力', # 城市+行业截断
|
|
60
|
+
'包括东莞市松山湖科技', # "包括" 引导词 + 城市
|
|
61
|
+
'东莞市松山湖科技', # 城市+科技截断
|
|
62
|
+
'武汉市洪山区光谷软件', # 城市+软件截断
|
|
63
|
+
'深圳市网鹏科技', # 城市+科技公司截断
|
|
64
|
+
# 行业通用词误识(不能单独成 ORG)
|
|
65
|
+
'数据', '科技', '软件', '网络', '智能', '信息', '技术',
|
|
66
|
+
'工程', '建设', '建筑', '机械', '装备', '制造', '加工',
|
|
67
|
+
'文化', '传媒', '出版', '教育', '培训', '咨询', '服务',
|
|
68
|
+
'贸易', '商业', '物流', '运输', '仓储', '物业', '管理',
|
|
69
|
+
'能源', '电力', '热电', '化工', '材料', '冶金', '矿产',
|
|
70
|
+
'装饰', '装修', '景观', '园林', '市政', '建筑装饰',
|
|
71
|
+
'农业', '林业', '渔业', '牧业', '种业', '饲料', '化肥',
|
|
72
|
+
'汽车', '机械', '装备', '工业', '化工', '材料', '冶金',
|
|
73
|
+
'不动产', '房地产', '地产', '置业', '资产管理',
|
|
74
|
+
'金融', '银行', '保险', '证券', '基金', '信托', '期货',
|
|
75
|
+
'酒店', '宾馆', '饭店', '旅馆', '招待所', '度假村',
|
|
76
|
+
'医院', '门诊部', '卫生所', '卫生院', '疾控中心',
|
|
77
|
+
'学校', '中学', '小学', '幼儿园', '大学', '学院',
|
|
78
|
+
'政府', '机关', '机构', '组织', '单位', '部门', '科室',
|
|
79
|
+
'团队', '小组', '工作', '工作地',
|
|
80
|
+
# "X国/中国" 单独被误识
|
|
81
|
+
'沃尔玛', '京东', '阿里', '百度', '腾讯', '网易', '美团', '字节跳动',
|
|
82
|
+
# 行政区+行业后缀误识
|
|
83
|
+
'海淀区', '南山区', '朝阳区', '天河区', '浦东新区',
|
|
84
|
+
'高新区', '开发区', '保税区', '自贸区', '产业园区',
|
|
85
|
+
])
|
|
86
|
+
|
|
87
|
+
# 常见姓氏(用于单字PER判断)
|
|
88
|
+
COMMON_SURNAMES = '张王李赵刘陈杨黄周吴徐孙马朱胡郭何罗林郑梁谢宋唐韩冯董萧程曹袁邓许傅沈曾彭吕苏卢蒋蔡贾丁魏薛叶阎余潘杜戴夏钟汪田任姜范方石姚谭廖邹熊金陆郝孔白崔康毛邱秦江史顾侯邵孟龙万段雷钱汤尹黎易常武乔贺赖龚文'
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_blacklisted(text: str) -> bool:
|
|
92
|
+
"""检查文本是否在误识别黑名单中"""
|
|
93
|
+
if not text or len(text.strip()) == 0:
|
|
94
|
+
return True
|
|
95
|
+
if text.strip() in FP_BLACKLIST:
|
|
96
|
+
return True
|
|
97
|
+
if re.match(r'^\d+$', text):
|
|
98
|
+
return True
|
|
99
|
+
if len(text) == 1 and text not in COMMON_SURNAMES:
|
|
100
|
+
return True
|
|
101
|
+
if re.match(r'^[\s,,、.。!?;;::\-_()()\[\]【\]]+$', text):
|
|
102
|
+
return True
|
|
103
|
+
return False
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DataMask 集中配置(环境变量 + 默认值)
|
|
3
|
+
所有硬编码配置迁移至此
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import hashlib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# ── 路径推导 ────────────────────────────────────────────
|
|
11
|
+
_POC_DIR = Path(__file__).parent.absolute()
|
|
12
|
+
_PROJECT_DIR = _POC_DIR.parent
|
|
13
|
+
_NER_DIR = _POC_DIR / "ner"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _env_path(var: str, default: str) -> str:
|
|
17
|
+
val = os.environ.get(var, "")
|
|
18
|
+
return val if val else str(Path(default))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── FPE 加密密钥 ────────────────────────────────────────
|
|
22
|
+
DATAMASK_KEY = os.environ.get("DATAMASK_KEY", "").encode()
|
|
23
|
+
if not DATAMASK_KEY:
|
|
24
|
+
# POC 默认开发密钥 — 生产环境必须通过环境变量覆盖
|
|
25
|
+
DATAMASK_KEY = hashlib.sha256(b"datamask-poc-dev-key").digest()
|
|
26
|
+
|
|
27
|
+
# ── 模型路径 ────────────────────────────────────────────
|
|
28
|
+
ONNX_MODEL_PATH = _env_path("ONNX_MODEL_PATH", _NER_DIR / "onnx_infer" / "bert_ner_v7.onnx")
|
|
29
|
+
TOKENIZER_DIR = _env_path("TOKENIZER_DIR", _NER_DIR / "onnx_infer")
|
|
30
|
+
|
|
31
|
+
# ── 推理参数 ────────────────────────────────────────────
|
|
32
|
+
CONFIDENCE_THRESHOLD = float(os.environ.get("CONFIDENCE_THRESHOLD", "0.6"))
|
|
33
|
+
MAX_SEQUENCE_LENGTH = int(os.environ.get("MAX_SEQUENCE_LENGTH", "512"))
|
|
34
|
+
|
|
35
|
+
# ── 日志 ────────────────────────────────────────────────
|
|
36
|
+
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
|
|
37
|
+
|
|
38
|
+
# ── API ─────────────────────────────────────────────────
|
|
39
|
+
API_HOST = os.environ.get("API_HOST", "0.0.0.0")
|
|
40
|
+
API_PORT = int(os.environ.get("API_PORT", "8000"))
|
|
41
|
+
|
|
42
|
+
# ── 实体类型枚举 ────────────────────────────────────────
|
|
43
|
+
# 正则引擎实体类型
|
|
44
|
+
REGEX_ENTITY_TYPES = {
|
|
45
|
+
"PHONE", "PHONE_400", "EMAIL", "URL", "QQ", "WECHAT",
|
|
46
|
+
"OFFICIAL_ACCOUNT", "SERVICE_ACCOUNT", "IDCARD", "BANKCARD",
|
|
47
|
+
"STOCK", "ZIPCODE",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# 模型实体类型(需脱敏)
|
|
51
|
+
MODEL_ENTITY_TYPES = {"ORG", "PER", "LOC"}
|
|
52
|
+
|
|
53
|
+
# 保留实体类型(不脱敏,原样保留)
|
|
54
|
+
PRESERVED_ENTITY_TYPES = {"DATE", "MONEY"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DataMask 实体加密层 — Design v1.1 LLM-Friendly 模式核心
|
|
3
|
+
|
|
4
|
+
设计要点:
|
|
5
|
+
- 同一原文(type 内)跨任务保持 token 一致(HMAC-SHA256 派生)
|
|
6
|
+
- 原文 AES-256-GCM 加密后存库,用于 reverse 还原
|
|
7
|
+
- 每次加密使用随机 nonce(96-bit),保证语义安全
|
|
8
|
+
- 派生密钥与加密密钥分离(HKDF 或双独立密钥),降低密钥泄露影响
|
|
9
|
+
|
|
10
|
+
密钥架构:
|
|
11
|
+
- ENCRYPTION_KEY (32B): 用于 AES-256-GCM 加解密原文
|
|
12
|
+
- HMAC_KEY (32B): 用于 HMAC-SHA256 派生 token_id
|
|
13
|
+
- 两把密钥独立,POC 默认值由 dev key 派生,生产必须从环境变量覆盖
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import hmac
|
|
17
|
+
import hashlib
|
|
18
|
+
import base64
|
|
19
|
+
import secrets
|
|
20
|
+
from typing import Optional, Tuple
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── 默认开发密钥(POC)──────────────────────────────────
|
|
24
|
+
# 生产环境必须通过环境变量 ENCRYPTION_KEY / HMAC_KEY 提供 32 字节密钥
|
|
25
|
+
# base64 编码后填入,避免源码出现裸字节
|
|
26
|
+
_DEV_ENCRYPTION_KEY_B64 = hashlib.sha256(b"datamask-encryption-dev-key").digest()
|
|
27
|
+
_DEV_HMAC_KEY_B64 = hashlib.sha256(b"datamask-hmac-dev-key").digest()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_key(env_name: str, dev_default: bytes) -> bytes:
|
|
31
|
+
"""
|
|
32
|
+
从环境变量加载 base64 编码的 32 字节密钥
|
|
33
|
+
缺失时回退到开发默认密钥(仅 POC 用途)
|
|
34
|
+
"""
|
|
35
|
+
val = os.environ.get(env_name, "").strip()
|
|
36
|
+
if not val:
|
|
37
|
+
return dev_default
|
|
38
|
+
try:
|
|
39
|
+
decoded = base64.b64decode(val)
|
|
40
|
+
if len(decoded) < 32:
|
|
41
|
+
# 太短则 sha256 扩展到 32B
|
|
42
|
+
return hashlib.sha256(decoded).digest()
|
|
43
|
+
return decoded[:32]
|
|
44
|
+
except Exception:
|
|
45
|
+
return dev_default
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# 全局默认密钥(POC 友好)
|
|
49
|
+
ENCRYPTION_KEY = _load_key("ENCRYPTION_KEY", _DEV_ENCRYPTION_KEY_B64)
|
|
50
|
+
HMAC_KEY = _load_key("HMAC_KEY", _DEV_HMAC_KEY_B64)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EntityCrypto:
|
|
54
|
+
"""
|
|
55
|
+
实体加密器 — 负责 token_id 派生与原文加解密
|
|
56
|
+
|
|
57
|
+
用法:
|
|
58
|
+
crypto = EntityCrypto(encryption_key, hmac_key)
|
|
59
|
+
token_id = crypto.derive_token_id("深圳南山区科技公司", "ORG")
|
|
60
|
+
ciphertext, nonce = crypto.encrypt("深圳南山区科技公司")
|
|
61
|
+
plain = crypto.decrypt(ciphertext, nonce)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
NONCE_SIZE = 12 # AES-256-GCM 推荐 96-bit nonce
|
|
65
|
+
|
|
66
|
+
def __init__(self, encryption_key: bytes = None, hmac_key: bytes = None):
|
|
67
|
+
"""
|
|
68
|
+
:param encryption_key: 32 字节 AES-256 密钥(None 则使用全局默认)
|
|
69
|
+
:param hmac_key: 32 字节 HMAC 密钥(None 则使用全局默认)
|
|
70
|
+
"""
|
|
71
|
+
self.encryption_key = encryption_key or ENCRYPTION_KEY
|
|
72
|
+
self.hmac_key = hmac_key or HMAC_KEY
|
|
73
|
+
|
|
74
|
+
if len(self.encryption_key) != 32:
|
|
75
|
+
raise ValueError(f"encryption_key 必须是 32 字节,当前 {len(self.encryption_key)}")
|
|
76
|
+
if len(self.hmac_key) != 32:
|
|
77
|
+
raise ValueError(f"hmac_key 必须是 32 字节,当前 {len(self.hmac_key)}")
|
|
78
|
+
|
|
79
|
+
def derive_token_id(self, original: str, entity_type: str) -> str:
|
|
80
|
+
"""
|
|
81
|
+
派生稳定的 token_id(16 字节 hex = 32 字符)
|
|
82
|
+
|
|
83
|
+
特性:
|
|
84
|
+
- 同一 (type, original) 永远得到同一 token_id(确定性)
|
|
85
|
+
- 不同 type 下相同 original 派生不同 token_id(隔离)
|
|
86
|
+
- HMAC-SHA256 抗碰撞,无法逆推原文
|
|
87
|
+
- 16 字节 = 128-bit 强度远超 2^64 生日攻击阈值
|
|
88
|
+
"""
|
|
89
|
+
if not original:
|
|
90
|
+
raise ValueError("original 不能为空")
|
|
91
|
+
if not entity_type:
|
|
92
|
+
raise ValueError("entity_type 不能为空")
|
|
93
|
+
msg = f"{entity_type.upper()}\x00{original}".encode("utf-8")
|
|
94
|
+
digest = hmac.new(self.hmac_key, msg, hashlib.sha256).digest()
|
|
95
|
+
return digest[:16].hex()
|
|
96
|
+
|
|
97
|
+
def encrypt(self, plaintext: str) -> Tuple[bytes, bytes]:
|
|
98
|
+
"""
|
|
99
|
+
AES-256-GCM 加密原文
|
|
100
|
+
|
|
101
|
+
:return: (ciphertext, nonce) — nonce 12 字节随机生成
|
|
102
|
+
:raises ImportError: cryptography 库未安装
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
106
|
+
except ImportError as e:
|
|
107
|
+
raise ImportError(
|
|
108
|
+
"需要安装 cryptography 库: pip install cryptography"
|
|
109
|
+
) from e
|
|
110
|
+
|
|
111
|
+
if not isinstance(plaintext, str):
|
|
112
|
+
plaintext = str(plaintext)
|
|
113
|
+
nonce = secrets.token_bytes(self.NONCE_SIZE)
|
|
114
|
+
aesgcm = AESGCM(self.encryption_key)
|
|
115
|
+
ct = aesgcm.encrypt(nonce, plaintext.encode("utf-8"), associated_data=None)
|
|
116
|
+
return ct, nonce
|
|
117
|
+
|
|
118
|
+
def decrypt(self, ciphertext: bytes, nonce: bytes) -> str:
|
|
119
|
+
"""
|
|
120
|
+
AES-256-GCM 解密
|
|
121
|
+
|
|
122
|
+
:param ciphertext: 密文(包含 GCM tag)
|
|
123
|
+
:param nonce: 12 字节 nonce
|
|
124
|
+
:return: 原文
|
|
125
|
+
:raises ValueError: 解密失败(tag 校验失败 / 密钥错误)
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
129
|
+
except ImportError as e:
|
|
130
|
+
raise ImportError(
|
|
131
|
+
"需要安装 cryptography 库: pip install cryptography"
|
|
132
|
+
) from e
|
|
133
|
+
|
|
134
|
+
if len(nonce) != self.NONCE_SIZE:
|
|
135
|
+
raise ValueError(f"nonce 必须是 {self.NONCE_SIZE} 字节")
|
|
136
|
+
aesgcm = AESGCM(self.encryption_key)
|
|
137
|
+
try:
|
|
138
|
+
pt = aesgcm.decrypt(nonce, ciphertext, associated_data=None)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
raise ValueError(f"解密失败(tag 校验失败): {e}") from e
|
|
141
|
+
return pt.decode("utf-8")
|
|
142
|
+
|
|
143
|
+
def encrypt_to_b64(self, plaintext: str) -> Tuple[str, str]:
|
|
144
|
+
"""便捷接口:加密并返回 base64 编码"""
|
|
145
|
+
ct, nonce = self.encrypt(plaintext)
|
|
146
|
+
return base64.b64encode(ct).decode("ascii"), base64.b64encode(nonce).decode("ascii")
|
|
147
|
+
|
|
148
|
+
def decrypt_from_b64(self, ct_b64: str, nonce_b64: str) -> str:
|
|
149
|
+
"""便捷接口:从 base64 解密"""
|
|
150
|
+
ct = base64.b64decode(ct_b64)
|
|
151
|
+
nonce = base64.b64decode(nonce_b64)
|
|
152
|
+
return self.decrypt(ct, nonce)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── 工厂函数 ────────────────────────────────────────────
|
|
156
|
+
_default_crypto: Optional[EntityCrypto] = None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_default_crypto() -> EntityCrypto:
|
|
160
|
+
"""获取全局默认 EntityCrypto 实例(延迟初始化)"""
|
|
161
|
+
global _default_crypto
|
|
162
|
+
if _default_crypto is None:
|
|
163
|
+
_default_crypto = EntityCrypto()
|
|
164
|
+
return _default_crypto
|