feat: 初始化 LLM API 密钥生成器项目

添加完整的 Python 包结构和核心功能:
- 实现高安全性 API 密钥生成功能,支持自定义长度、前缀和字符集
- 集成 cryptography 库提供加密增强的随机数生成和密钥加密存储
- 添加 pydantic 参数验证模型,确保输入参数安全性
- 实现密钥强度检测机制,基于熵值评估密钥质量
- 支持批量生成密钥,提高使用效率
- 提供完整的测试套件,包括基础功能、增强功能和异常处理测试
- 配置项目构建系统,支持 Python 3.8+ 多版本兼容
- 添加 MIT 许可证和项目元数据配置
This commit is contained in:
2025-11-30 20:14:32 +08:00
commit e3cd2d77ac
11 changed files with 785 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
# Python缓存
__pycache__/
.coverage
.coveragerc
# 虚拟环境
.venv
# 测试缓存
.pytest_cache/
# 构建输出
build/
*.py[cod]
*.egg-info
# uv 依赖锁
uv.lock

4
LICENSE Normal file
View File

@@ -0,0 +1,4 @@
Copyright (C) 2025 drd_vic
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.

64
pyproject.toml Normal file
View File

@@ -0,0 +1,64 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "llmapikeygen"
version = "0.0.1"
authors = [{ name="drd_vic", email="2912458403@qq.com" }]
description = "High-security API key generator for large language models (LLMs)"
keywords = ["llm", "api-key", "security", "generator"]
license = "MIT"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Security :: Cryptography",
]
dependencies = []
[project.optional-dependencies]
full = [
"cryptography>=46.0.3",
"pydantic>=2.10.6",
]
dev = [
"black>=24.8.0",
"pytest>=8.3.5",
"pytest-cov>=5.0.0",
]
[project.urls]
"Homepage" = "https://drdfilenest.xyz:8443/root/llm-api-keygen"
"Bug Tracker" = "https://drdfilenest.xyz:8443/root/llm-api-keygen/issues"
[tool.uv]
index-url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-v --cov=src/llmapikeygen --cov-report=term-missing --cov-fail-under=80"
markers = [
"slow: 标记慢测试",
"enhanced: 标记依赖外部库的增强功能测试"
]
[tool.coverage.run]
source = ["src/llmapikeygen"]
omit = [
"src/llmapikeygen/__init__.py"
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.:",
"raise ImportError",
]

View File

@@ -0,0 +1,28 @@
"""
llm-api-keygen: High-security API key generator for large language models
"""
__version__ = "0.0.1"
__author__ = "drd_vic"
__license__ = "MIT"
from .core import (
generate_api_key,
generate_batch,
check_key_strength,
encrypt_key,
decrypt_key
)
# 暴露可选依赖标记
from .core import PYDANTIC_AVAILABLE, CRYPTOGRAPHY_AVAILABLE
__all__ = [
"generate_api_key",
"generate_batch",
"check_key_strength",
"encrypt_key",
"decrypt_key",
"PYDANTIC_AVAILABLE",
"CRYPTOGRAPHY_AVAILABLE"
]

236
src/llmapikeygen/core.py Normal file
View File

@@ -0,0 +1,236 @@
import secrets
import string
from typing import Optional, Union, List, Tuple
try:
from pydantic import BaseModel, field_validator, ValidationError
PYDANTIC_AVAILABLE = True
except ImportError:
PYDANTIC_AVAILABLE = False
try:
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
CRYPTOGRAPHY_AVAILABLE = True
except ImportError:
CRYPTOGRAPHY_AVAILABLE = False
DEFAULT_PREFIX = "sk-"
DEFAULT_LENGTH = 32
DEFAULT_CHARSET = string.ascii_letters + string.digits
SPECIAL_CHARSET = DEFAULT_CHARSET + "!@#$%^&*()_+-=[]{}|;:,.?~`"
MIN_LENGTH = 8
# ------------------------------
# 参数验证模型
# ------------------------------
if PYDANTIC_AVAILABLE:
class KeyGenConfig(BaseModel):
"""
API密钥生成配置
"""
length: int = DEFAULT_LENGTH
prefix: str = DEFAULT_PREFIX
charset: Optional[Union[str, List[str]]] = None
exclude_ambiguous: bool = True
@field_validator("length")
def length_must_be_valid(cls, v):
if v < MIN_LENGTH:
raise ValueError(f"Length must be ≥ {MIN_LENGTH}")
return v
@field_validator("prefix")
def prefix_must_be_non_empty(cls, v):
if not v.strip():
raise ValueError("Prefix cannot be empty")
return v.strip()
def generate_api_key(
length: int = DEFAULT_LENGTH,
prefix: str = DEFAULT_PREFIX,
charset: Optional[Union[str, List[str]]] = None,
exclude_ambiguous: bool = True,
use_crypto: bool = False # 是否使用 cryptography 增强随机性
) -> str:
"""
生成高安全性大模型API密钥密码学级随机
Args:
length: 密钥主体长度不含前缀推荐16/32/64最小值8
prefix: 密钥前缀,默认"sk-"兼容主流LLM平台
charset: 自定义字符集字符串或字符列表None则使用默认字符集
exclude_ambiguous: 是否排除易混淆字符o/O/0、l/I/1默认True
use_crypto: 是否使用 cryptography 库生成更安全的随机字符(需安装 cryptography
Returns:
完整API密钥前缀+主体)
Raises:
ValueError: 输入参数非法或字符集为空
ValidationError: 参数校验失败(需安装 pydantic
"""
# 1. 参数校验(优先使用 pydantic无则用基础校验
if PYDANTIC_AVAILABLE:
try:
config = KeyGenConfig(
length=length,
prefix=prefix,
charset=charset,
exclude_ambiguous=exclude_ambiguous
)
length, prefix, charset, exclude_ambiguous = (
config.length, config.prefix, config.charset, config.exclude_ambiguous
)
except ValidationError as e:
raise ValueError(f"Invalid config: {e}") from e
else:
# 基础参数校验(无 pydantic 时)
if length < MIN_LENGTH:
raise ValueError(f"API key length must be at least {MIN_LENGTH} characters (excluding prefix)")
if not prefix.strip():
raise ValueError("Prefix cannot be empty")
prefix = prefix.strip()
# 2. 处理字符集
if charset is None:
used_charset = DEFAULT_CHARSET
else:
used_charset = "".join(charset) if isinstance(charset, list) else charset
# 排除易混淆字符
if exclude_ambiguous:
ambiguous_chars = {"o", "O", "0", "l", "L", "I", "1"}
used_charset = "".join([c for c in used_charset if c not in ambiguous_chars])
if not used_charset:
raise ValueError("Charset cannot be empty after processing")
# 3. 生成随机字符(优先使用 cryptography 增强安全性)
if use_crypto and CRYPTOGRAPHY_AVAILABLE:
# 使用 cryptography 生成高强度随机字节
num_bytes = (length + 1) // 2 # 每个字节对应2个字符
random_bytes = secrets.token_bytes(num_bytes)
# 将字节映射到字符集(确保均匀分布)
key_body = []
for byte in random_bytes:
key_body.append(used_charset[byte % len(used_charset)])
key_body = "".join(key_body[:length]) # 截断到目标长度
else:
# 标准库 fallback仍安全符合行业规范
key_body = ''.join(secrets.choice(used_charset) for _ in range(length))
return f"{prefix}{key_body}"
def generate_batch(
count: int = 5,
length: int = DEFAULT_LENGTH,
prefix: str = DEFAULT_PREFIX,
charset: Optional[Union[str, List[str]]] = None,
exclude_ambiguous: bool = True,
use_crypto: bool = False
) -> List[str]:
"""
批量生成API密钥
Args:
count: 生成的密钥数量默认5
其他参数同 generate_api_key
Returns:
密钥列表
"""
if count < 1:
raise ValueError("Batch count must be at least 1")
return [
generate_api_key(length, prefix, charset, exclude_ambiguous, use_crypto)
for _ in range(count)
]
# ------------------------------
# 密钥强度检测cryptography
# ------------------------------
def check_key_strength(key: str, min_entropy: float = 4.0) -> Tuple[bool, str]:
"""
检测API密钥的强度基于字符集熵值
Args:
key: 待检测的API密钥
min_entropy: 最小可接受熵值字符集大小的log2默认4.0对应16种字符
Returns:
Tuple[是否安全, 强度说明]
"""
if not key:
return False, "密钥为空"
# 提取密钥主体
if "-" in key:
key_body = key.split("-", 1)[1]
else:
key_body = key
if len(key_body) < MIN_LENGTH:
return False, f"密钥主体长度不足{MIN_LENGTH}"
# 计算字符集大小和熵值
unique_chars = len(set(key_body))
entropy_per_char = (unique_chars).bit_length() - 1 # 近似 log2(unique_chars)
if entropy_per_char < min_entropy:
return False, f"强度不足(字符多样性低,熵值{entropy_per_char:.1f} < 最低{min_entropy:.1f}"
if CRYPTOGRAPHY_AVAILABLE:
# 额外校验:密钥是否含足够随机分布(基于哈希)
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(key_body.encode("utf-8"))
hash_hex = digest.finalize().hex()
# 简单校验哈希结果前4位不重复低概率碰撞仅作为辅助
if len(set(hash_hex[:4])) < 3:
return True, f"强度合格(熵值{entropy_per_char:.1f}),但随机分布略差"
return True, f"强度合格(熵值{entropy_per_char:.1f}"
# ------------------------------
# 密钥加密存储cryptography
# ------------------------------
def encrypt_key(key: str, secret_key: Optional[bytes] = None) -> Tuple[bytes, bytes]:
"""
加密API密钥用于安全存储需安装 cryptography
Args:
key: 待加密的API密钥
secret_key: 加密密钥None则自动生成
Returns:
Tuple[加密后的密钥, 用于解密的secret_key]
"""
if not CRYPTOGRAPHY_AVAILABLE:
raise ImportError("请安装 cryptography 库pip install llm-api-keygen[full]")
if secret_key is None:
secret_key = Fernet.generate_key() # 自动生成加密密钥
fernet = Fernet(secret_key)
encrypted_key = fernet.encrypt(key.encode("utf-8"))
return encrypted_key, secret_key
def decrypt_key(encrypted_key: bytes, secret_key: bytes) -> str:
"""
解密密文
"""
if not CRYPTOGRAPHY_AVAILABLE:
raise ImportError("请安装 cryptography 库pip install llm-api-keygen[full]")
fernet = Fernet(secret_key)
decrypted_key = fernet.decrypt(encrypted_key).decode("utf-8")
return decrypted_key

0
tests/__init__.py Normal file
View File

12
tests/conftest.py Normal file
View File

@@ -0,0 +1,12 @@
import sys
from pathlib import Path
# 获取项目根目录
root_dir = Path(__file__).parent.parent
# 将 src 目录添加到 Python 的 sys.path 中
src_dir = root_dir / "src"
sys.path.insert(0, str(src_dir))
# 验证:打印 sys.path 确认 src 已添加
print(f"Added src directory to sys.path: {src_dir}")
print(f"Current sys.path: {sys.path}")

View File

@@ -0,0 +1,64 @@
import sys
import pytest
from llmapikeygen import generate_api_key, generate_batch
# 测试 Python 版本兼容性(仅标记,实际需多环境运行)
PY38_OR_HIGHER = sys.version_info >= (3, 8)
PY310_OR_HIGHER = sys.version_info >= (3, 10)
def test_python_version_compatibility():
"""测试 Python 版本兼容性(基础功能)"""
# 所有支持的版本都应能生成密钥
key = generate_api_key()
assert key.startswith("sk-")
batch = generate_batch(count=2)
assert len(batch) == 2
def test_charset_unicode_compatibility():
"""测试 Unicode 字符集兼容性"""
# 中文字符集虽然不推荐用于API密钥但需确保不报错
unicode_charset = "一二三四五六七八九十大"
key = generate_api_key(length=16, charset=unicode_charset, exclude_ambiguous=False)
assert len(key) == len("sk-") + 16
assert all(c in unicode_charset for c in key[3:])
# 特殊Unicode符号
emoji_charset = "😀😁😂🤣😃😄😅😆"
key_emoji = generate_api_key(length=8, charset=emoji_charset, exclude_ambiguous=False)
assert all(c in emoji_charset for c in key_emoji[3:])
def test_long_prefix_compatibility():
"""测试超长前缀兼容性"""
# 前缀长度=32接近极限
long_prefix = "a" * 32 + "-"
key = generate_api_key(prefix=long_prefix, length=16)
assert key.startswith(long_prefix)
assert len(key) == len(long_prefix) + 16
def test_extreme_length_compatibility():
"""测试极端长度最小8位最大1024位"""
# 最小长度
key_min = generate_api_key(length=8)
assert len(key_min) == len("sk-") + 8
# 超长长度1024位主体
key_extreme = generate_api_key(length=1024)
assert len(key_extreme) == len("sk-") + 1024
# 批量生成超长密钥
batch_extreme = generate_batch(count=3, length=512)
assert len(batch_extreme) == 3
assert all(len(key) == len("sk-") + 512 for key in batch_extreme)
def test_empty_prefix_compatibility():
"""测试空前缀(虽然不推荐,但需兼容)"""
# 注意:空前缀会被基础校验拒绝,需确认逻辑
with pytest.raises(ValueError):
generate_api_key(prefix="")
# 测试仅含特殊字符的前缀(非空)
special_prefix = "!@#-"
key = generate_api_key(prefix=special_prefix)
assert key.startswith(special_prefix)

148
tests/test_core_basic.py Normal file
View File

@@ -0,0 +1,148 @@
import string
from llmapikeygen import generate_api_key, generate_batch, check_key_strength
# 常量(与核心代码一致,避免硬编码)
DEFAULT_PREFIX = "sk-"
DEFAULT_LENGTH = 32
MIN_LENGTH = 8
AMBIGUOUS_CHARS = {"o", "O", "0", "l", "L", "I", "1"}
def test_generate_api_key_default_params():
"""测试默认参数生成密钥"""
key = generate_api_key()
# 校验前缀
assert key.startswith(DEFAULT_PREFIX)
# 校验长度前缀2位 + 主体32位
assert len(key) == len(DEFAULT_PREFIX) + DEFAULT_LENGTH
# 校验字符集(仅字母+数字,无易混淆字符)
key_body = key[len(DEFAULT_PREFIX):]
assert all(c in string.ascii_letters + string.digits for c in key_body)
assert not any(c in AMBIGUOUS_CHARS for c in key_body)
# 校验随机性(两次生成不重复)
key2 = generate_api_key()
assert key != key2
def test_generate_api_key_custom_length():
"""测试自定义密钥长度"""
# 最小长度8位主体
key_min = generate_api_key(length=MIN_LENGTH)
assert len(key_min) == len(DEFAULT_PREFIX) + MIN_LENGTH
# 常用长度16/64位主体
for length in [16, 64]:
key = generate_api_key(length=length)
assert len(key) == len(DEFAULT_PREFIX) + length
def test_generate_api_key_custom_prefix():
"""测试自定义前缀"""
# 普通前缀
prefix = "pk-"
key = generate_api_key(prefix=prefix)
assert key.startswith(prefix)
# 含特殊字符的前缀
prefix_special = "llm_sk-"
key_special = generate_api_key(prefix=prefix_special)
assert key_special.startswith(prefix_special)
# 大小写混合前缀
prefix_mixed = "SK-"
key_mixed = generate_api_key(prefix=prefix_mixed)
assert key_mixed.startswith(prefix_mixed)
def test_generate_api_key_custom_charset():
"""测试自定义字符集"""
# 仅小写字母
charset_lower = string.ascii_lowercase
key_lower = generate_api_key(charset=charset_lower)
assert all(c in charset_lower for c in key_lower[len(DEFAULT_PREFIX):])
# 仅数字
charset_digit = string.digits
key_digit = generate_api_key(charset=charset_digit, exclude_ambiguous=False)
assert all(c in charset_digit for c in key_digit[len(DEFAULT_PREFIX):])
# 列表格式字符集
charset_list = ["a", "b", "c", "1", "2", "3"]
key_list = generate_api_key(charset=charset_list)
assert all(c in charset_list for c in key_list[len(DEFAULT_PREFIX):])
# 含特殊符号的字符集
charset_special = "abc!@#"
key_special = generate_api_key(charset=charset_special)
assert all(c in charset_special for c in key_special[len(DEFAULT_PREFIX):])
def test_generate_api_key_exclude_ambiguous():
"""测试排除易混淆字符功能"""
# 默认开启(排除易混淆字符)
key_exclude = generate_api_key()
assert not any(c in AMBIGUOUS_CHARS for c in key_exclude)
# 关闭排除(允许易混淆字符)
key_include = generate_api_key(exclude_ambiguous=False, charset=string.ascii_letters + string.digits)
# 断言至少包含一个易混淆字符(概率性,但样本足够大时成立)
assert any(c in AMBIGUOUS_CHARS for c in key_include[len(DEFAULT_PREFIX):])
def test_generate_batch_basic():
"""测试批量生成密钥"""
# 默认数量5个
batch_default = generate_batch()
assert len(batch_default) == 5
# 自定义数量1/10/100个
for count in [1, 10, 100]:
batch = generate_batch(count=count)
assert len(batch) == count
# 断言所有密钥不重复
assert len(set(batch)) == count
# 断言所有密钥格式正确
for key in batch:
assert key.startswith(DEFAULT_PREFIX)
assert len(key) == len(DEFAULT_PREFIX) + DEFAULT_LENGTH
def test_generate_batch_custom_params():
"""测试批量生成的自定义参数"""
batch = generate_batch(
count=3,
length=16,
prefix="test-",
charset="abc123",
exclude_ambiguous=False
)
for key in batch:
assert key.startswith("test-")
assert len(key) == len("test-") + 16
assert all(c in "abc123" for c in key[len("test-"):])
def test_check_key_strength_basic():
"""测试密钥强度检测(基础功能,无外部依赖)"""
# 弱密钥:长度不足
weak_key_short = "sk-1234567" # 主体7位 < 最小8位
is_secure, msg = check_key_strength(weak_key_short)
assert not is_secure
assert "长度不足" in msg
# 弱密钥:字符多样性低(全相同字符)
weak_key_repeat = "sk-" + "a" * 32
is_secure, msg = check_key_strength(weak_key_repeat)
assert not is_secure
assert "熵值" in msg and "不足" in msg
# 弱密钥字符集过小仅2种字符
weak_key_charset = "sk-" + "ab" * 16
is_secure, msg = check_key_strength(weak_key_charset)
assert not is_secure
assert "熵值" in msg and "不足" in msg
# 强密钥:默认生成的密钥
strong_key = generate_api_key()
is_secure, msg = check_key_strength(strong_key)
assert is_secure
assert "合格" in msg
# 自定义最小熵值
key = generate_api_key(charset=string.ascii_letters) # 52种字符熵值≈5.7
is_secure, msg = check_key_strength(key, min_entropy=5.0)
assert is_secure
is_secure, msg = check_key_strength(key, min_entropy=6.0)
assert not is_secure

110
tests/test_core_enhanced.py Normal file
View File

@@ -0,0 +1,110 @@
import pytest
import string
from llmapikeygen import (
generate_api_key, generate_batch, check_key_strength,
encrypt_key, decrypt_key, PYDANTIC_AVAILABLE, CRYPTOGRAPHY_AVAILABLE
)
# 跳过整个文件(如果依赖未安装)
pytestmark = [
pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not installed"),
pytest.mark.skipif(not CRYPTOGRAPHY_AVAILABLE, reason="Cryptography not installed"),
]
def test_generate_api_key_pydantic_validation():
"""测试 pydantic 参数校验"""
# 长度小于最小值8
with pytest.raises(ValueError) as excinfo:
generate_api_key(length=7)
assert "Length must be ≥ 8" in str(excinfo.value) or "至少8个字符" in str(excinfo.value)
# 前缀为空字符串
with pytest.raises(ValueError) as excinfo:
generate_api_key(prefix="")
assert "Prefix cannot be empty" in str(excinfo.value) or "前缀不能为空" in str(excinfo.value)
# 前缀仅含空格
with pytest.raises(ValueError) as excinfo:
generate_api_key(prefix=" ")
assert "Prefix cannot be empty" in str(excinfo.value) or "前缀不能为空" in str(excinfo.value)
def test_generate_api_key_use_crypto():
"""测试使用 cryptography 增强随机性"""
# 单密钥生成
key1 = generate_api_key(length=32, use_crypto=True)
assert len(key1) == len("sk-") + 32
# 批量生成(确保不重复)
batch = generate_batch(count=10, use_crypto=True)
assert len(set(batch)) == 10
# 不同字符集下的兼容性
key_charset = generate_api_key(charset=string.ascii_lowercase, use_crypto=True)
assert all(c in string.ascii_lowercase for c in key_charset[3:])
def test_check_key_strength_crypto_enhanced():
"""测试 cryptography 增强的密钥强度检测"""
# 生成高强度密钥
key = generate_api_key(length=64, use_crypto=True)
is_secure, msg = check_key_strength(key)
assert is_secure
assert "合格" in msg
# 边缘情况:熵值刚好达标
key_marginal = generate_api_key(charset=string.ascii_letters[:16]) # 16种字符熵值=4.0
is_secure, msg = check_key_strength(key_marginal, min_entropy=4.0)
assert is_secure
def test_encrypt_decrypt_key_basic():
"""测试密钥加密和解密"""
# 普通密钥加密解密
key = generate_api_key()
encrypted_key, secret_key = encrypt_key(key)
# 断言加密后的数据非空
assert isinstance(encrypted_key, bytes) and len(encrypted_key) > 0
assert isinstance(secret_key, bytes) and len(secret_key) > 0
# 解密后与原密钥一致
decrypted_key = decrypt_key(encrypted_key, secret_key)
assert decrypted_key == key
def test_encrypt_decrypt_custom_secret_key():
"""测试自定义加密密钥"""
from cryptography.fernet import Fernet
# 自定义 secret_key需符合 Fernet 格式)
custom_secret_key = Fernet.generate_key()
key = "sk-custom-1234567890abcdef"
# 用自定义密钥加密
encrypted_key, secret_key = encrypt_key(key, secret_key=custom_secret_key)
assert secret_key == custom_secret_key # 返回的密钥与自定义一致
# 解密
decrypted_key = decrypt_key(encrypted_key, custom_secret_key)
assert decrypted_key == key
def test_encrypt_decrypt_special_key():
"""测试含特殊字符的密钥加密解密"""
# 含特殊符号的密钥
special_key = "sk-!@#$%^&*()_+-=[]{}|;:,.?~`1234"
encrypted_key, secret_key = encrypt_key(special_key)
decrypted_key = decrypt_key(encrypted_key, secret_key)
assert decrypted_key == special_key
# 长密钥128位主体
long_key = generate_api_key(length=128)
encrypted_long, secret_long = encrypt_key(long_key)
decrypted_long = decrypt_key(encrypted_long, secret_long)
assert decrypted_long == long_key
def test_generate_batch_use_crypto():
"""测试批量生成(启用 cryptography"""
batch = generate_batch(count=5, length=24, use_crypto=True)
assert len(batch) == 5
for key in batch:
assert len(key) == len("sk-") + 24
# 强度检测合格
is_secure, _ = check_key_strength(key)
assert is_secure

101
tests/test_exceptions.py Normal file
View File

@@ -0,0 +1,101 @@
import pytest
from llmapikeygen import generate_api_key, generate_batch, encrypt_key, decrypt_key
def test_generate_api_key_invalid_length():
"""测试无效长度(小于最小值)"""
# 长度为0
with pytest.raises(ValueError):
generate_api_key(length=0)
# 长度为7小于最小8
with pytest.raises(ValueError):
generate_api_key(length=7)
# 负数长度
with pytest.raises(ValueError):
generate_api_key(length=-10)
def test_generate_api_key_empty_charset():
"""测试空字符集"""
# 空字符串字符集
with pytest.raises(ValueError) as excinfo:
generate_api_key(charset="")
assert "Charset cannot be empty" in str(excinfo.value) or "字符集不能为空" in str(excinfo.value)
# 空列表字符集
with pytest.raises(ValueError) as excinfo:
generate_api_key(charset=[])
assert "Charset cannot be empty" in str(excinfo.value) or "字符集不能为空" in str(excinfo.value)
# 字符集被易混淆字符过滤后为空
with pytest.raises(ValueError) as excinfo:
generate_api_key(charset="oO0lL1I", exclude_ambiguous=True)
assert "Charset cannot be empty" in str(excinfo.value) or "字符集不能为空" in str(excinfo.value)
def test_generate_batch_invalid_count():
"""测试批量生成的无效数量"""
# 数量为0
with pytest.raises(ValueError):
generate_batch(count=0)
# 负数数量
with pytest.raises(ValueError):
generate_batch(count=-5)
def test_encrypt_key_without_cryptography():
"""测试未安装 cryptography 时调用加密功能"""
# 先卸载 cryptography模拟无依赖场景
import sys
if "cryptography" in sys.modules:
del sys.modules["cryptography"]
# 重新导入(确保不使用缓存)
from importlib import reload
import llmapikeygen.core
reload(llmapikeygen.core)
with pytest.raises(ImportError) as excinfo:
encrypt_key("sk-test-123")
assert "请安装 cryptography 库" in str(excinfo.value) or "Install cryptography" in str(excinfo.value)
def test_decrypt_key_without_cryptography():
"""测试未安装 cryptography 时调用解密功能"""
import sys
if "cryptography" in sys.modules:
del sys.modules["cryptography"]
from importlib import reload
import llmapikeygen.core
reload(llmapikeygen.core)
with pytest.raises(ImportError) as excinfo:
decrypt_key(b"test", b"test")
assert "请安装 cryptography 库" in str(excinfo.value) or "Install cryptography" in str(excinfo.value)
def test_decrypt_key_invalid_secret_key():
"""测试无效的解密密钥(依赖 cryptography"""
pytest.importorskip("cryptography") # 跳过无依赖的场景
from cryptography.fernet import InvalidToken
# 生成合法加密数据
key = "sk-test-123"
encrypted_key, secret_key = encrypt_key(key)
# 篡改 secret_key
invalid_secret_key = secret_key[:-1] + b"x"
# 解密时抛出 InvalidToken
with pytest.raises(InvalidToken):
decrypt_key(encrypted_key, invalid_secret_key)
def test_decrypt_key_invalid_encrypted_data():
"""测试无效的加密数据(依赖 cryptography"""
pytest.importorskip("cryptography")
from cryptography.fernet import InvalidToken
# 生成合法 secret_key
encrypted_key, secret_key = encrypt_key("sk-test-123")
# 篡改加密数据
invalid_encrypted_key = encrypted_key[:-1] + b"x"
with pytest.raises(InvalidToken):
decrypt_key(invalid_encrypted_key, secret_key)