Enhance AI extraction and document generation features. Added support for output field extraction by field codes, updated API endpoints for compatibility, and introduced a new document generation endpoint. Updated requirements and prompt configuration for improved functionality.

This commit is contained in:
python 2025-12-05 07:46:35 +08:00
parent 2b6b76f694
commit d0ad41fddd
1593 changed files with 238099 additions and 61 deletions

Binary file not shown.

288
app.py
View File

@ -10,6 +10,7 @@ from dotenv import load_dotenv
from services.ai_service import AIService
from services.field_service import FieldService
from services.document_service import DocumentService
from utils.response import success_response, error_response
# 加载环境变量
@ -63,6 +64,7 @@ swagger = Swagger(app, config=swagger_config, template=swagger_template)
# 初始化服务
ai_service = AIService()
field_service = FieldService()
document_service = DocumentService()
@app.route('/')
@ -71,7 +73,8 @@ def index():
return send_from_directory('static', 'index.html')
@app.route('/api/ai/extract', methods=['POST'])
@app.route('/ai/extract', methods=['POST'])
@app.route('/api/ai/extract', methods=['POST']) # 保留旧路径以兼容
def extract():
"""
AI字段提取接口
@ -81,7 +84,7 @@ def extract():
tags:
- AI解析
summary: 从输入数据中提取结构化字段
description: 使用AI大模型从输入文本中提取结构化字段支持多种业务类型
description: 使用AI大模型从输入文本中提取结构化字段根据fieldCode从数据库查询字段配置
consumes:
- application/json
produces:
@ -94,13 +97,9 @@ def extract():
schema:
type: object
required:
- businessType
- inputData
- outputData
properties:
businessType:
type: string
description: 业务类型
example: INVESTIGATION
inputData:
type: array
description: 输入数据列表
@ -115,6 +114,16 @@ def extract():
type: string
description: 字段值原始文本
example: 被举报用户名称是张三年龄30岁某公司总经理
outputData:
type: array
description: 需要提取的输出字段列表
items:
type: object
properties:
fieldCode:
type: string
description: 字段编码
example: userName
responses:
200:
description: 解析成功
@ -137,7 +146,7 @@ def extract():
fieldCode:
type: string
description: 字段编码
example: target_name
example: userName
fieldValue:
type: string
description: 提取的字段值
@ -170,20 +179,6 @@ def extract():
isSuccess:
type: boolean
example: false
1001:
description: 业务类型不存在
schema:
type: object
properties:
code:
type: integer
example: 1001
errorMsg:
type: string
example: 未找到业务类型 INVESTIGATION 对应的字段配置
isSuccess:
type: boolean
example: false
2001:
description: AI解析超时或发生错误
schema:
@ -220,23 +215,34 @@ def extract():
if not data:
return error_response(400, "请求参数不能为空")
business_type = data.get('businessType')
input_data = data.get('inputData', [])
if not business_type:
return error_response(400, "businessType参数不能为空")
output_data = data.get('outputData', [])
if not input_data or not isinstance(input_data, list):
return error_response(400, "inputData参数必须是非空数组")
# 获取业务类型对应的输出字段
output_fields = field_service.get_output_fields_by_business_type(business_type)
if not output_data or not isinstance(output_data, list):
return error_response(400, "outputData参数必须是非空数组")
# 提取outputData中的fieldCode列表
output_field_codes = []
for item in output_data:
if isinstance(item, dict) and 'fieldCode' in item:
output_field_codes.append(item['fieldCode'])
elif isinstance(item, str):
output_field_codes.append(item)
if not output_field_codes:
return error_response(400, "outputData中必须包含至少一个fieldCode")
# 根据fieldCode从数据库查询输出字段配置
output_fields = field_service.get_output_fields_by_field_codes(output_field_codes)
if not output_fields:
return error_response(1001, f"未找到业务类型 {business_type} 对应的字段配置")
return error_response(2002, f"未找到字段编码 {output_field_codes} 对应的字段配置")
# 构建AI提示词
prompt = field_service.build_extract_prompt(input_data, output_fields, business_type)
# 构建AI提示词不再需要business_type
prompt = field_service.build_extract_prompt(input_data, output_fields)
# 调用AI服务进行解析
ai_result = ai_service.extract_fields(prompt, output_fields)
@ -244,10 +250,13 @@ def extract():
if not ai_result:
return error_response(2002, "AI解析失败请检查输入文本质量")
# 构建返回数据
# 构建返回数据按照outputData中的字段顺序返回
out_data = []
for field in output_fields:
field_code = field['field_code']
# 创建一个字段编码到字段信息的映射
field_map = {field['field_code']: field for field in output_fields}
# 按照outputData的顺序构建返回数据
for field_code in output_field_codes:
field_value = ai_result.get(field_code, '')
out_data.append({
'fieldCode': field_code,
@ -370,6 +379,217 @@ def get_fields():
return error_response(500, f"获取字段配置失败: {str(e)}")
@app.route('/ai/generate-document', methods=['POST'])
@app.route('/api/ai/generate-document', methods=['POST']) # 保留旧路径以兼容
def generate_document():
"""
文档生成接口
根据输入数据填充Word模板并生成文档
---
tags:
- 文档生成
summary: 生成填充后的文档
description: 根据输入数据填充Word模板上传到MinIO并返回文件路径
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: body
description: 请求参数
required: true
schema:
type: object
required:
- inputData
- fpolicFieldParamFileList
properties:
inputData:
type: array
description: 输入数据列表
items:
type: object
properties:
fieldCode:
type: string
description: 字段编码
example: userName
fieldValue:
type: string
description: 字段值
example: 张三
fpolicFieldParamFileList:
type: array
description: 文件列表
items:
type: object
properties:
fileId:
type: integer
description: 文件ID
example: 1
fileName:
type: string
description: 文件名称
example: 请示报告卡.doc
templateCode:
type: string
description: 模板编码
example: REPORT_CARD
responses:
200:
description: 生成成功
schema:
type: object
properties:
code:
type: integer
description: 响应码0表示成功
example: 0
data:
type: object
properties:
documentId:
type: string
description: 文档ID
example: DOC202411260001
documentName:
type: string
description: 文档名称
example: 请示报告卡_张三.docx
inputData:
type: array
description: 输入数据
fpolicFieldParamFileList:
type: array
description: 文件列表包含filePath
msg:
type: string
example: ok
isSuccess:
type: boolean
example: true
1001:
description: 模板不存在
schema:
type: object
properties:
code:
type: integer
example: 1001
errorMsg:
type: string
example: 模板不存在
isSuccess:
type: boolean
example: false
3001:
description: 文件生成失败
schema:
type: object
properties:
code:
type: integer
example: 3001
errorMsg:
type: string
example: 文件生成失败
isSuccess:
type: boolean
example: false
3002:
description: 文件保存失败
schema:
type: object
properties:
code:
type: integer
example: 3002
errorMsg:
type: string
example: 文件保存失败
isSuccess:
type: boolean
example: false
"""
try:
data = request.get_json()
# 验证请求参数
if not data:
return error_response(400, "请求参数不能为空")
input_data = data.get('inputData', [])
file_list = data.get('fpolicFieldParamFileList', [])
if not input_data or not isinstance(input_data, list):
return error_response(400, "inputData参数必须是非空数组")
if not file_list or not isinstance(file_list, list):
return error_response(400, "fpolicFieldParamFileList参数必须是非空数组")
# 将input_data转换为字典格式用于生成文档名称
field_data = {}
for item in input_data:
field_code = item.get('fieldCode', '')
field_value = item.get('fieldValue', '')
if field_code:
field_data[field_code] = field_value or ''
# 生成文档ID和文档名称使用第一个文件
document_id = document_service.generate_document_id()
first_file_name = file_list[0].get('fileName', 'document.docx') if file_list else 'document.docx'
document_name = document_service.generate_document_name(first_file_name, field_data)
# 处理每个文件
result_file_list = []
for file_info in file_list:
file_id = file_info.get('fileId')
file_name = file_info.get('fileName', '')
template_code = file_info.get('templateCode', '')
if not template_code:
return error_response(1001, f"文件 {file_name} 缺少templateCode参数")
try:
# 生成文档
result = document_service.generate_document(
template_code=template_code,
input_data=input_data,
file_info=file_info
)
result_file_list.append({
'fileId': file_id,
'fileName': file_name,
'filePath': result['filePath']
})
except Exception as e:
error_msg = str(e)
if '不存在' in error_msg or '模板' in error_msg:
return error_response(1001, error_msg)
elif '生成' in error_msg or '填充' in error_msg:
return error_response(3001, error_msg)
elif '上传' in error_msg or '保存' in error_msg:
return error_response(3002, error_msg)
else:
return error_response(3001, f"文件生成失败: {error_msg}")
# 构建返回数据
return success_response({
'documentId': document_id,
'documentName': document_name,
'inputData': input_data,
'fpolicFieldParamFileList': result_file_list
})
except Exception as e:
return error_response(3001, f"文档生成失败: {str(e)}")
if __name__ == '__main__':
# 确保static目录存在
os.makedirs('static', exist_ok=True)

108
check_database_structure.py Normal file
View File

@ -0,0 +1,108 @@
"""
检查数据库表结构确认template_code字段是否存在
"""
import pymysql
import os
DB_CONFIG = {
'host': os.getenv('DB_HOST', '152.136.177.240'),
'port': int(os.getenv('DB_PORT', 5012)),
'user': os.getenv('DB_USER', 'finyx'),
'password': os.getenv('DB_PASSWORD', '6QsGK6MpePZDE57Z'),
'database': os.getenv('DB_NAME', 'finyx'),
'charset': 'utf8mb4'
}
TENANT_ID = 615873064429507639
def check_table_structure():
"""检查表结构"""
print("="*60)
print("检查数据库表结构")
print("="*60)
try:
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor()
# 检查f_polic_file_config表结构
print("\n1. 检查 f_polic_file_config 表结构:")
cursor.execute("DESCRIBE f_polic_file_config")
columns = cursor.fetchall()
has_template_code = False
has_input_data = False
for col in columns:
col_name = col[0]
col_type = col[1]
print(f" - {col_name}: {col_type}")
if col_name == 'template_code':
has_template_code = True
if col_name == 'input_data':
has_input_data = True
print(f"\n template_code 字段: {'✓ 存在' if has_template_code else '✗ 不存在'}")
print(f" input_data 字段: {'✓ 存在' if has_input_data else '✗ 不存在'}")
if not has_template_code:
print("\n ⚠ 警告template_code字段不存在可能需要添加该字段")
print(" 请联系数据库管理员添加该字段")
# 检查是否有数据
print("\n2. 检查文件配置数据:")
cursor.execute("""
SELECT COUNT(*)
FROM f_polic_file_config
WHERE tenant_id = %s
""", (TENANT_ID,))
count = cursor.fetchone()[0]
print(f" 找到 {count} 个文件配置记录")
if count > 0:
# 检查template_code字段是否有值
if has_template_code:
cursor.execute("""
SELECT COUNT(*)
FROM f_polic_file_config
WHERE tenant_id = %s AND template_code IS NOT NULL AND template_code != ''
""", (TENANT_ID,))
code_count = cursor.fetchone()[0]
print(f" 其中 {code_count} 个记录有template_code值")
if code_count < count:
print(f" ⚠ 建议运行: python update_template_code_field.py")
# 检查字段数据
print("\n3. 检查字段数据:")
cursor.execute("""
SELECT COUNT(*)
FROM f_polic_field
WHERE tenant_id = %s
""", (TENANT_ID,))
field_count = cursor.fetchone()[0]
print(f" 找到 {field_count} 个字段记录")
if field_count == 0:
print(" ⚠ 建议运行: python init_all_fields_from_excel.py")
cursor.close()
conn.close()
print("\n" + "="*60)
print("检查完成")
print("="*60)
return True
except Exception as e:
print(f"\n✗ 检查失败: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == '__main__':
check_table_structure()

57
check_dependencies.py Normal file
View File

@ -0,0 +1,57 @@
"""
检查所有依赖是否已安装
"""
import sys
def check_dependency(module_name, import_statement):
"""检查单个依赖"""
try:
exec(import_statement)
print(f"{module_name}: 已安装")
return True
except ImportError as e:
print(f"{module_name}: 未安装 - {str(e)}")
return False
def main():
"""主函数"""
print("="*60)
print("检查依赖安装情况")
print("="*60)
print()
dependencies = [
("Flask", "from flask import Flask"),
("Flask-CORS", "from flask_cors import CORS"),
("Flasgger", "from flasgger import Swagger"),
("PyMySQL", "import pymysql"),
("python-dotenv", "from dotenv import load_dotenv"),
("Requests", "import requests"),
("python-docx", "from docx import Document"),
("MinIO", "from minio import Minio"),
("openpyxl", "import openpyxl"),
]
results = []
for name, import_stmt in dependencies:
results.append(check_dependency(name, import_stmt))
print()
print("="*60)
installed = sum(results)
total = len(results)
print(f"已安装: {installed}/{total}")
if installed < total:
print("\n缺少的依赖,请运行:")
print(" pip install -r requirements.txt")
return False
else:
print("\n✓ 所有依赖已安装!")
return True
if __name__ == '__main__':
success = main()
sys.exit(0 if success else 1)

View File

@ -8,9 +8,13 @@
"requirements": [
"仔细分析输入文本,准确提取每个字段的值",
"如果某个字段在输入文本中找不到对应信息,该字段值设为空字符串\"\"",
"日期格式统一为YYYYMM198005表示1980年5月",
"性别统一为\"男\"或\"女\"",
"政治面貌使用标准表述(如:中共党员、群众等)",
"日期格式统一为YYYYMM198005表示1980年5月如果包含日期信息则格式为YYYYMMDD",
"性别统一为\"男\"或\"女\",不要使用\"男性\"或\"女性\"",
"政治面貌使用标准表述(如:中共党员、中共预备党员、共青团员、群众等)",
"职级使用标准表述(如:正处级、副处级、正科级、副科级等)",
"身份证号码只提取数字,不包含其他字符",
"联系方式提取电话号码,格式化为纯数字",
"地址信息保持完整,包含省市区街道等详细信息",
"只返回JSON对象不要包含markdown代码块标记"
]
},
@ -18,10 +22,67 @@
"input_field_format": "{field_code}: {field_value}",
"output_field_format": "- {field_name} (字段编码: {field_code})"
},
"business_type_rules": {
"INVESTIGATION": {
"description": "调查核实业务类型的特殊规则",
"additional_requirements": []
"field_specific_rules": {
"target_name": {
"description": "被核查人姓名",
"rules": [
"提取完整姓名,包括姓氏和名字",
"如果文本中包含多个姓名,提取最相关的那个"
]
},
"target_gender": {
"description": "被核查人员性别",
"rules": [
"只能返回\"男\"或\"女\"",
"如果文本中提到\"男性\"、\"男性公民\"等,统一转换为\"男\"",
"如果文本中提到\"女性\"、\"女性公民\"等,统一转换为\"女\""
]
},
"target_date_of_birth": {
"description": "被核查人员出生年月",
"rules": [
"格式YYYYMM如198005表示1980年5月",
"如果只有年份月份设为01",
"如果文本中提到\"X年X月X日出生\",只提取年月,忽略日期"
]
},
"target_date_of_birth_full": {
"description": "被核查人员出生年月日",
"rules": [
"格式YYYYMMDD如19800515表示1980年5月15日",
"如果只有年月日期设为01"
]
},
"target_political_status": {
"description": "被核查人员政治面貌",
"rules": [
"使用标准表述:中共党员、中共预备党员、共青团员、群众、无党派人士等",
"如果文本中提到\"党员\",统一转换为\"中共党员\"",
"如果文本中提到\"非党员\",统一转换为\"群众\""
]
},
"target_professional_rank": {
"description": "被核查人员职级",
"rules": [
"使用标准表述:正处级、副处级、正科级、副科级、正厅级、副厅级等",
"保持原文中的职级表述"
]
},
"target_id_number": {
"description": "被核查人员身份证号",
"rules": [
"提取18位身份证号码只包含数字和可能的最后一位X",
"如果文本中只有部分号码,保持原样",
"不包含其他字符如空格、横线等"
]
},
"target_contact": {
"description": "被核查人员联系方式",
"rules": [
"提取电话号码,格式化为纯数字",
"如果是手机号提取11位数字",
"如果是座机,包含区号和号码"
]
}
}
}

View File

@ -0,0 +1,380 @@
"""
从Excel解析结果初始化所有字段到数据库
基于parsed_fields.json文件初始化所有模板和字段
"""
import pymysql
import json
from datetime import datetime
from pathlib import Path
# 数据库连接配置
DB_CONFIG = {
'host': '152.136.177.240',
'port': 5012,
'user': 'finyx',
'password': '6QsGK6MpePZDE57Z',
'database': 'finyx',
'charset': 'utf8mb4'
}
# 固定值
TENANT_ID = 615873064429507639
CREATED_BY = 655162080928945152
CURRENT_TIME = datetime.now()
# 字段名称到字段编码的映射(用于将中文名称转换为标准编码)
FIELD_NAME_TO_CODE_MAP = {
# 输入字段
'线索信息': 'clue_info',
'被核查人员工作基本情况线索': 'target_basic_info_clue',
# 输出字段 - 基本信息
'被核查人姓名': 'target_name',
'被核查人员单位及职务': 'target_organization_and_position',
'被核查人员性别': 'target_gender',
'被核查人员出生年月': 'target_date_of_birth',
'被核查人员出生年月日': 'target_date_of_birth_full',
'被核查人员政治面貌': 'target_political_status',
'被核查人员职级': 'target_professional_rank',
# 输出字段 - 其他信息
'线索来源': 'clue_source',
'主要问题线索': 'target_issue_description',
'初步核实审批表承办部门意见': 'department_opinion',
'初步核实审批表填表人': 'filler_name',
# 其他常用字段根据Excel数据补充
'请示报告卡请示时间': 'report_card_request_time',
'被核查人员身份证件及号码': 'target_id_number',
'被核查人员身份证号': 'target_id_number',
'应到时间': 'appointment_time',
'应到地点': 'appointment_location',
'批准时间': 'approval_time',
'承办部门': 'handling_department',
'承办人': 'handler_name',
'谈话通知时间': 'notification_time',
'谈话通知地点': 'notification_location',
'被核查人员住址': 'target_address',
'被核查人员户籍住址': 'target_registered_address',
'被核查人员联系方式': 'target_contact',
'被核查人员籍贯': 'target_place_of_origin',
'被核查人员民族': 'target_ethnicity',
}
def generate_id():
"""生成ID使用时间戳+随机数的方式,模拟雪花算法)"""
import time
import random
timestamp = int(time.time() * 1000)
random_part = random.randint(100000, 999999)
return timestamp * 1000 + random_part
def get_field_code_from_name(field_name: str, default_prefix: str = 'field') -> str:
"""
根据字段名称获取字段编码
Args:
field_name: 字段名称中文
default_prefix: 默认前缀如果找不到映射使用此前缀
Returns:
字段编码
"""
# 先查映射表
if field_name in FIELD_NAME_TO_CODE_MAP:
return FIELD_NAME_TO_CODE_MAP[field_name]
# 如果没有映射,尝试使用字段名称本身(如果已经是英文编码)
if field_name and not any('\u4e00' <= char <= '\u9fff' for char in field_name):
# 如果字段名称中不包含中文字符,直接使用
return field_name.lower().replace(' ', '_')
# 如果包含中文,转换为拼音或者使用默认格式
# 这里简化处理,直接使用字段名称(实际应该转换为拼音)
return field_name
def load_parsed_fields():
"""加载解析的字段数据"""
json_file = Path(__file__).parent / 'parsed_fields.json'
if not json_file.exists():
raise FileNotFoundError(f"解析结果文件不存在: {json_file}")
with open(json_file, 'r', encoding='utf-8') as f:
return json.load(f)
def init_all_fields(conn, parsed_data):
"""
初始化所有字段输入字段和输出字段
Returns:
字段编码到字段ID的映射字典
"""
cursor = conn.cursor()
field_map = {} # field_code -> field_id
print("="*60)
print("开始初始化所有字段...")
print("="*60)
# 收集所有唯一的字段
all_fields = {}
# 收集输入字段
for template_name, template_info in parsed_data.items():
for input_field in template_info.get('input_fields', []):
field_name = input_field['name']
field_code = input_field.get('field_code') or get_field_code_from_name(field_name)
if field_code not in all_fields:
all_fields[field_code] = {
'name': field_name,
'field_code': field_code,
'field_type': 1 # 输入字段
}
# 收集输出字段
for template_name, template_info in parsed_data.items():
for output_field in template_info.get('output_fields', []):
field_name = output_field['name']
field_code = output_field.get('field_code') or get_field_code_from_name(field_name)
if field_code not in all_fields:
all_fields[field_code] = {
'name': field_name,
'field_code': field_code,
'field_type': 2 # 输出字段
}
elif all_fields[field_code]['field_type'] == 1:
# 如果同一个字段编码既出现在输入字段也出现在输出字段,优先作为输出字段
all_fields[field_code]['field_type'] = 2
print(f"\n共找到 {len(all_fields)} 个唯一字段")
# 按字段类型分组
input_fields = [f for f in all_fields.values() if f['field_type'] == 1]
output_fields = [f for f in all_fields.values() if f['field_type'] == 2]
print(f" 输入字段: {len(input_fields)}")
print(f" 输出字段: {len(output_fields)}")
# 初始化字段
for field_code, field_info in all_fields.items():
# 检查字段是否已存在
check_sql = """
SELECT id FROM f_polic_field
WHERE tenant_id = %s AND filed_code = %s
"""
cursor.execute(check_sql, (TENANT_ID, field_code))
existing = cursor.fetchone()
if existing:
field_id = existing[0]
print(f" 字段 '{field_info['name']}' (code: {field_code}) 已存在ID: {field_id}")
else:
field_id = generate_id()
insert_sql = """
INSERT INTO f_polic_field
(id, tenant_id, name, filed_code, field_type, created_time, created_by, updated_time, updated_by, state)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_sql, (
field_id,
TENANT_ID,
field_info['name'],
field_code,
field_info['field_type'],
CURRENT_TIME,
CREATED_BY,
CURRENT_TIME,
CREATED_BY,
1 # state: 1表示启用
))
field_type_str = "输入字段" if field_info['field_type'] == 1 else "输出字段"
print(f" ✓ 创建{field_type_str}: {field_info['name']} (code: {field_code}), ID: {field_id}")
field_map[field_code] = field_id
conn.commit()
return field_map
def init_file_configs(conn, parsed_data):
"""
初始化所有文件配置
Returns:
文件配置名称到文件配置ID的映射字典
"""
cursor = conn.cursor()
config_map = {} # template_name -> file_config_id
print("\n" + "="*60)
print("开始初始化文件配置...")
print("="*60)
for template_name, template_info in parsed_data.items():
template_code = template_info.get('template_code', template_name)
# 检查文件配置是否已存在
check_sql = """
SELECT id, template_code FROM f_polic_file_config
WHERE tenant_id = %s AND name = %s
"""
cursor.execute(check_sql, (TENANT_ID, template_name))
existing = cursor.fetchone()
if existing:
file_config_id = existing[0]
existing_template_code = existing[1]
# 如果已存在但template_code不同更新它
if existing_template_code != template_code:
update_sql = """
UPDATE f_polic_file_config
SET template_code = %s, updated_time = %s, updated_by = %s
WHERE id = %s
"""
cursor.execute(update_sql, (template_code, CURRENT_TIME, CREATED_BY, file_config_id))
conn.commit()
print(f" ✓ 更新文件配置 '{template_name}' 的template_code: {existing_template_code} -> {template_code}")
print(f" 文件配置 '{template_name}' 已存在ID: {file_config_id}, template_code: {template_code}")
else:
file_config_id = generate_id()
# 注意不再写入input_data字段只写入template_code
insert_sql = """
INSERT INTO f_polic_file_config
(id, tenant_id, parent_id, name, template_code, file_path, created_time, created_by, updated_time, updated_by, state)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
# 默认文件路径(可以根据实际情况调整)
default_file_path = f"/templates/{template_name}.docx"
cursor.execute(insert_sql, (
file_config_id,
TENANT_ID,
None, # parent_id
template_name,
template_code,
default_file_path,
CURRENT_TIME,
CREATED_BY,
CURRENT_TIME,
CREATED_BY,
1 # state: 1表示启用
))
print(f" ✓ 创建文件配置: {template_name} (template_code: {template_code}), ID: {file_config_id}")
config_map[template_name] = file_config_id
conn.commit()
return config_map
def init_file_field_relations(conn, parsed_data, field_map, config_map):
"""初始化文件和字段的关联关系(只关联输出字段)"""
cursor = conn.cursor()
print("\n" + "="*60)
print("开始初始化文件和字段的关联关系...")
print("="*60)
for template_name, template_info in parsed_data.items():
file_config_id = config_map.get(template_name)
if not file_config_id:
print(f" 警告: 模板 '{template_name}' 的文件配置不存在,跳过")
continue
output_fields = template_info.get('output_fields', [])
print(f"\n 处理模板: {template_name} ({len(output_fields)} 个输出字段)")
for output_field in output_fields:
field_name = output_field['name']
field_code = output_field.get('field_code') or get_field_code_from_name(field_name)
field_id = field_map.get(field_code)
if not field_id:
print(f" 警告: 字段 '{field_name}' (code: {field_code}) 不存在,跳过")
continue
# 检查关联关系是否已存在
check_sql = """
SELECT id FROM f_polic_file_field
WHERE tenant_id = %s AND file_id = %s AND filed_id = %s
"""
cursor.execute(check_sql, (TENANT_ID, file_config_id, field_id))
existing = cursor.fetchone()
if existing:
print(f" 关联关系已存在: {field_name}")
else:
relation_id = generate_id()
insert_sql = """
INSERT INTO f_polic_file_field
(id, tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_sql, (
relation_id,
TENANT_ID,
file_config_id,
field_id,
CURRENT_TIME,
CREATED_BY,
CURRENT_TIME,
CREATED_BY,
1 # state: 1表示启用
))
print(f" ✓ 创建关联: {field_name} (code: {field_code})")
conn.commit()
def main():
"""主函数"""
try:
# 加载解析的字段数据
print("="*60)
print("加载Excel解析结果...")
print("="*60)
parsed_data = load_parsed_fields()
print(f"✓ 成功加载 {len(parsed_data)} 个模板的数据")
# 连接数据库
print("\n" + "="*60)
print("连接数据库...")
print("="*60)
conn = pymysql.connect(**DB_CONFIG)
print("✓ 数据库连接成功")
try:
# 1. 初始化所有字段
field_map = init_all_fields(conn, parsed_data)
# 2. 初始化文件配置
config_map = init_file_configs(conn, parsed_data)
# 3. 初始化关联关系
init_file_field_relations(conn, parsed_data, field_map, config_map)
print("\n" + "="*60)
print("✓ 所有数据初始化完成!")
print("="*60)
finally:
conn.close()
except Exception as e:
print(f"\n错误: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()

169
parse_excel_fields.py Normal file
View File

@ -0,0 +1,169 @@
"""
解析Excel文件提取所有字段定义
"""
import openpyxl
from pathlib import Path
import json
from collections import defaultdict
# Excel文件路径
EXCEL_FILE = '技术文档/智慧监督项目模板数据结构设计表-20251125-一凡标注.xlsx'
def parse_excel():
"""解析Excel文件提取字段信息"""
print("="*60)
print("开始解析Excel文件...")
print("="*60)
try:
# 检查文件是否存在
if not Path(EXCEL_FILE).exists():
print(f"错误: Excel文件不存在: {EXCEL_FILE}")
return None
# 打开Excel文件
workbook = openpyxl.load_workbook(EXCEL_FILE, data_only=True)
print(f"✓ 成功打开Excel文件: {EXCEL_FILE}")
# 获取工作表
sheet = workbook.active
# 读取所有数据
all_rows = []
for row in sheet.iter_rows(values_only=True):
all_rows.append(row)
print(f"总行数: {len(all_rows)}")
if len(all_rows) < 2:
print("错误: Excel文件数据不足")
return None
# 解析表头
headers = all_rows[0]
print(f"\n表头: {headers}")
# 字段映射关系
field_map = {
'一级分类': 0,
'二级分类': 1,
'三级分类': 2,
'输入数据字段': 3,
'输出数据字段': 4,
'输出示例数据': 5,
'备注说明': 6
}
# 存储字段信息
templates = defaultdict(lambda: {
'template_name': '',
'template_code': '',
'input_fields': [],
'output_fields': []
})
current_template = None
current_input_field = None
# 处理数据行从第2行开始
for i, row in enumerate(all_rows[1:], 2):
# 跳过空行
if not any(row):
continue
level1 = row[0] # 一级分类
level2 = row[1] # 二级分类
level3 = row[2] # 三级分类
input_field = row[3] # 输入数据字段
output_field = row[4] # 输出数据字段
example = row[5] # 输出示例数据
remark = row[6] # 备注说明
# 如果有一级分类,说明是新的模板组
if level1:
print(f"\n处理一级分类: {level1}")
# 如果有二级分类,说明是新的模板
if level2:
current_template = level2
# 生成模板编码(将中文转换为大写英文,去掉空格)
template_code = level2.upper().replace(' ', '_').replace('初步核实审批表', 'PRELIMINARY_VERIFICATION_APPROVAL').replace('请示报告卡', 'REPORT_CARD')
templates[current_template]['template_name'] = current_template
templates[current_template]['template_code'] = template_code
print(f" 处理二级分类(模板): {current_template} -> {template_code}")
# 处理输入字段
if input_field and input_field != current_input_field:
current_input_field = input_field
if current_template:
templates[current_template]['input_fields'].append({
'name': input_field,
'field_code': input_field.replace('线索信息', 'clue_info').replace('被核查人员工作基本情况线索', 'target_basic_info_clue')
})
print(f" 输入字段: {input_field}")
# 处理输出字段
if output_field:
if current_template:
# 生成字段编码(简化版,实际需要更精确的映射)
field_code = output_field.lower().replace(' ', '_').replace('被核查人姓名', 'target_name').replace('被核查人员单位及职务', 'target_organization_and_position')
templates[current_template]['output_fields'].append({
'name': output_field,
'field_code': field_code,
'example': example,
'remark': remark
})
print(f" 输出字段: {output_field} -> {field_code}")
workbook.close()
# 转换为字典格式
result = {}
for template_name, template_info in templates.items():
result[template_name] = template_info
return result
except Exception as e:
print(f"解析Excel文件时发生错误: {e}")
import traceback
traceback.print_exc()
return None
def generate_field_mapping():
"""生成字段映射关系(基于已知字段)"""
# 根据初步核实审批表的字段定义,创建字段名称到字段编码的映射
field_mapping = {
'被核查人姓名': 'target_name',
'被核查人员单位及职务': 'target_organization_and_position',
'被核查人员性别': 'target_gender',
'被核查人员出生年月': 'target_date_of_birth',
'被核查人员政治面貌': 'target_political_status',
'被核查人员职级': 'target_professional_rank',
'线索来源': 'clue_source',
'主要问题线索': 'target_issue_description',
'初步核实审批表承办部门意见': 'department_opinion',
'初步核实审批表填表人': 'filler_name',
'线索信息': 'clue_info',
'被核查人员工作基本情况线索': 'target_basic_info_clue',
}
return field_mapping
if __name__ == '__main__':
result = parse_excel()
if result:
print("\n" + "="*60)
print("解析结果汇总:")
print("="*60)
for template_name, template_info in result.items():
print(f"\n模板: {template_name}")
print(f" 模板编码: {template_info['template_code']}")
print(f" 输入字段数: {len(template_info['input_fields'])}")
print(f" 输出字段数: {len(template_info['output_fields'])}")
# 保存为JSON文件
output_file = 'parsed_fields.json'
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"\n✓ 结果已保存到: {output_file}")

896
parsed_fields.json Normal file
View File

@ -0,0 +1,896 @@
{
"请示报告卡": {
"template_name": "请示报告卡",
"template_code": "REPORT_CARD",
"input_fields": [
{
"name": "线索信息",
"field_code": "clue_info"
}
],
"output_fields": [
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员单位及职务",
"field_code": "target_organization_and_position",
"example": "某公司总经理",
"remark": null
},
{
"name": "请示报告卡请示时间",
"field_code": "请示报告卡请示时间",
"example": 45972,
"remark": null
}
]
},
"初步核实审批表": {
"template_name": "初步核实审批表",
"template_code": "PRELIMINARY_VERIFICATION_APPROVAL",
"input_fields": [
{
"name": "被核查人员工作基本情况线索",
"field_code": "target_basic_info_clue"
},
{
"name": "线索信息",
"field_code": "clue_info"
}
],
"output_fields": [
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员单位及职务",
"field_code": "target_organization_and_position",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人员性别",
"field_code": "被核查人员性别",
"example": "男",
"remark": "男/女,不用男性和女性"
},
{
"name": "被核查人员出生年月",
"field_code": "被核查人员出生年月",
"example": 32448,
"remark": "YYYYMM不需要日"
},
{
"name": "被核查人员政治面貌",
"field_code": "被核查人员政治面貌",
"example": "中共党员",
"remark": "中共党员、群众"
},
{
"name": "被核查人员职级",
"field_code": "被核查人员职级",
"example": "正处级",
"remark": null
},
{
"name": "线索来源",
"field_code": "线索来源",
"example": "空下",
"remark": null
},
{
"name": "主要问题线索",
"field_code": "主要问题线索",
"example": "违反国家计划生育有关政策规定于2010年10月生育二胎。",
"remark": null
},
{
"name": "初步核实审批表承办部门意见",
"field_code": "初步核实审批表承办部门意见",
"example": "空下",
"remark": null
},
{
"name": "初步核实审批表填表人",
"field_code": "初步核实审批表填表人",
"example": "空下",
"remark": null
}
]
},
"初核方案": {
"template_name": "初核方案",
"template_code": "初核方案",
"input_fields": [
{
"name": "被核查人员工作基本情况线索",
"field_code": "target_basic_info_clue"
},
{
"name": "线索信息",
"field_code": "clue_info"
}
],
"output_fields": [
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员单位及职务",
"field_code": "target_organization_and_position",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人员工作基本情况",
"field_code": "被核查人员工作基本情况",
"example": "XXX汉族19XX年X月出生山西XX人XX学历19XX年X月参加工作20XX年X月加入中国共产党。19XX年X月至20XX年X月先后在XXXX工作20XX年X月至20XX年X月任XXXXX20XX年X月至20XX年X月任XXXX20XX年X月至今任XXXXX。",
"remark": null
},
{
"name": "主要问题线索",
"field_code": "主要问题线索",
"example": "违反国家计划生育有关政策规定于2010年10月生育二胎。",
"remark": null
},
{
"name": "核查单位名称",
"field_code": "核查单位名称",
"example": "XXX纪委",
"remark": null
},
{
"name": "核查组组长姓名",
"field_code": "核查组组长姓名",
"example": "张三",
"remark": null
},
{
"name": "核查组成员姓名",
"field_code": "核查组成员姓名",
"example": "张三、李四",
"remark": null
},
{
"name": "核查地点",
"field_code": "核查地点",
"example": "空下",
"remark": null
}
]
},
"谈话通知书": {
"template_name": "谈话通知书",
"template_code": "谈话通知书",
"input_fields": [
{
"name": "被核查人员工作基本情况线索",
"field_code": "target_basic_info_clue"
}
],
"output_fields": [
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员单位及职务",
"field_code": "target_organization_and_position",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人员身份证件及号码",
"field_code": "被核查人员身份证件及号码",
"example": "110111111111111111",
"remark": null
},
{
"name": "应到时间",
"field_code": "应到时间",
"example": "空下",
"remark": null
},
{
"name": "应到地点",
"field_code": "应到地点",
"example": "空下",
"remark": null
},
{
"name": "批准时间",
"field_code": "批准时间",
"example": "空下",
"remark": null
},
{
"name": "承办部门",
"field_code": "承办部门",
"example": "空下",
"remark": null
},
{
"name": "承办人",
"field_code": "承办人",
"example": "空下",
"remark": null
},
{
"name": "谈话通知时间",
"field_code": "谈话通知时间",
"example": "空下",
"remark": null
},
{
"name": "谈话通知地点",
"field_code": "谈话通知地点",
"example": "空下",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
}
]
},
"走读式谈话流程": {
"template_name": "走读式谈话流程",
"template_code": "走读式谈话流程",
"input_fields": [],
"output_fields": [
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员单位及职务",
"field_code": "target_organization_and_position",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人员性别",
"field_code": "被核查人员性别",
"example": "男",
"remark": null
},
{
"name": "被核查人员出生年月日",
"field_code": "被核查人员出生年月日",
"example": 45972,
"remark": null
},
{
"name": "被核查人员政治面貌",
"field_code": "被核查人员政治面貌",
"example": "党员",
"remark": null
},
{
"name": "被核查人员住址",
"field_code": "被核查人员住址",
"example": "省市县区街道楼门牌号",
"remark": null
},
{
"name": "被核查人员联系方式",
"field_code": "被核查人员联系方式",
"example": 13111111111,
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员单位及职务",
"field_code": "target_organization_and_position",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人员性别",
"field_code": "被核查人员性别",
"example": "男",
"remark": null
},
{
"name": "被核查人员出生年月日",
"field_code": "被核查人员出生年月日",
"example": 45972,
"remark": null
},
{
"name": "被核查人员政治面貌",
"field_code": "被核查人员政治面貌",
"example": "党员",
"remark": null
},
{
"name": "被核查人员户籍住址",
"field_code": "被核查人员户籍住址",
"example": "省市县区街道楼门牌号",
"remark": null
},
{
"name": "被核查人员联系方式",
"field_code": "被核查人员联系方式",
"example": 13111111111,
"remark": null
},
{
"name": "被核查人员籍贯",
"field_code": "被核查人员籍贯",
"example": "太原",
"remark": null
},
{
"name": "被核查人员民族",
"field_code": "被核查人员民族",
"example": "汉族",
"remark": null
},
{
"name": "被核查人员身份证号",
"field_code": "被核查人员身份证号",
"example": "110111111111111111",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员身份证号",
"field_code": "被核查人员身份证号",
"example": "110111111111111111",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员性别",
"field_code": "被核查人员性别",
"example": "男",
"remark": null
},
{
"name": "被核查人员身份证号",
"field_code": "被核查人员身份证号",
"example": "110111111111111111",
"remark": null
},
{
"name": "被核查人单位及职务",
"field_code": "被核查人单位及职务",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员性别",
"field_code": "被核查人员性别",
"example": "男",
"remark": null
},
{
"name": "被核查人员身份证号",
"field_code": "被核查人员身份证号",
"example": "110111111111111111",
"remark": null
},
{
"name": "被核查人单位及职务",
"field_code": "被核查人单位及职务",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人员联系方式",
"field_code": "被核查人员联系方式",
"example": 13111111111,
"remark": null
},
{
"name": "被核查人员政治面貌",
"field_code": "被核查人员政治面貌",
"example": "党员",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员性别",
"field_code": "被核查人员性别",
"example": "男",
"remark": null
},
{
"name": "被核查人员身份证号",
"field_code": "被核查人员身份证号",
"example": "110111111111111111",
"remark": null
},
{
"name": "被核查人单位及职务",
"field_code": "被核查人单位及职务",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人员联系方式",
"field_code": "被核查人员联系方式",
"example": 13111111111,
"remark": null
},
{
"name": "核查组代号",
"field_code": "核查组代号",
"example": "空下",
"remark": null
}
]
},
"走读式谈话审批": {
"template_name": "走读式谈话审批",
"template_code": "走读式谈话审批",
"input_fields": [
{
"name": "线索信息",
"field_code": "clue_info"
},
{
"name": "被核查人员工作基本情况线索",
"field_code": "target_basic_info_clue"
}
],
"output_fields": [
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员单位及职务",
"field_code": "target_organization_and_position",
"example": "男",
"remark": null
},
{
"name": "请示报告卡请示时间",
"field_code": "请示报告卡请示时间",
"example": "空下",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员性别",
"field_code": "被核查人员性别",
"example": "男",
"remark": null
},
{
"name": "谈话前安全风险评估结果",
"field_code": "谈话前安全风险评估结果",
"example": "空下",
"remark": null
},
{
"name": "谈话次数",
"field_code": "谈话次数",
"example": "空下",
"remark": null
},
{
"name": "拟谈话地点",
"field_code": "拟谈话地点",
"example": "空下",
"remark": null
},
{
"name": "拟谈话时间",
"field_code": "拟谈话时间",
"example": "空下",
"remark": null
},
{
"name": "谈话人员-组长",
"field_code": "谈话人员-组长",
"example": "空下",
"remark": null
},
{
"name": "谈话人员-谈话人员",
"field_code": "谈话人员-谈话人员",
"example": "空下",
"remark": null
},
{
"name": "谈话人员-安全员",
"field_code": "谈话人员-安全员",
"example": "空下",
"remark": null
},
{
"name": "补空人员",
"field_code": "补空人员",
"example": "空下",
"remark": null
},
{
"name": "谈话事由",
"field_code": "谈话事由",
"example": "空下",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员性别",
"field_code": "被核查人员性别",
"example": "男",
"remark": null
},
{
"name": "被核查人员年龄",
"field_code": "被核查人员年龄",
"example": "110111111111111111",
"remark": null
},
{
"name": "被核查人员职业",
"field_code": "被核查人员职业",
"example": "国有企业领导人员",
"remark": null
},
{
"name": "被核查人员学历",
"field_code": "被核查人员学历",
"example": "本科",
"remark": null
},
{
"name": "被核查人员工作履历",
"field_code": "被核查人员工作履历",
"example": "19XX年X月参加工作20XX年X月加入中国共产党。19XX年X月至20XX年X月先后在XXXX工作20XX年X月至20XX年X月任XXXXX20XX年X月至20XX年X月任XXXX20XX年X月至今任XXXXX。",
"remark": null
},
{
"name": "被核查人员家庭情况",
"field_code": "被核查人员家庭情况",
"example": "家庭关系和谐稳定",
"remark": null
},
{
"name": "被核查人员社会关系",
"field_code": "被核查人员社会关系",
"example": "社会交往较多,人机关系基本正常",
"remark": null
},
{
"name": "被核查人员健康状况",
"field_code": "被核查人员健康状况",
"example": "良好",
"remark": null
},
{
"name": "被核查人员性格特征",
"field_code": "被核查人员性格特征",
"example": "开朗",
"remark": null
},
{
"name": "被核查人员承受能力",
"field_code": "被核查人员承受能力",
"example": "较强",
"remark": null
},
{
"name": "被核查人员涉及问题严重程度",
"field_code": "被核查人员涉及问题严重程度",
"example": "较轻",
"remark": null
},
{
"name": "被核查人员涉及其他问题的可能性",
"field_code": "被核查人员涉及其他问题的可能性",
"example": "较小",
"remark": null
},
{
"name": "被核查人员此前被审查情况",
"field_code": "被核查人员此前被审查情况",
"example": "无",
"remark": "几乎都是“无”"
},
{
"name": "被核查人员社会负面事件",
"field_code": "被核查人员社会负面事件",
"example": "无",
"remark": "几乎都是“无”"
},
{
"name": "被核查人员其他情况",
"field_code": "被核查人员其他情况",
"example": "无",
"remark": "几乎都是“无”"
},
{
"name": "风险等级",
"field_code": "风险等级",
"example": "低/中/高",
"remark": "三选一"
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人单位及职务",
"field_code": "被核查人单位及职务",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人基本情况",
"field_code": "被核查人基本情况",
"example": "XXX汉族19XX年X月出生山西XX人XX学历19XX年X月参加工作20XX年X月加入中国共产党。19XX年X月至20XX年X月先后在XXXX工作20XX年X月至20XX年X月任XXXXX20XX年X月至20XX年X月任XXXX20XX年X月至今任XXXXX。",
"remark": null
},
{
"name": "谈话人",
"field_code": "谈话人",
"example": "空下",
"remark": null
},
{
"name": "记录人",
"field_code": "记录人",
"example": "空下",
"remark": null
},
{
"name": "谈话地点",
"field_code": "谈话地点",
"example": "空下",
"remark": null
},
{
"name": "核查组代号",
"field_code": "核查组代号",
"example": "空下",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人员性别",
"field_code": "被核查人员性别",
"example": "男",
"remark": null
},
{
"name": "被核查人员年龄",
"field_code": "被核查人员年龄",
"example": 45,
"remark": null
},
{
"name": "被核查人单位及职务",
"field_code": "被核查人单位及职务",
"example": "某公司总经理",
"remark": null
},
{
"name": "被核查人员学历",
"field_code": "被核查人员学历",
"example": "本科",
"remark": null
},
{
"name": "被核查人员工作基本情况",
"field_code": "被核查人员工作基本情况",
"example": "XXX汉族19XX年X月出生山西XX人XX学历19XX年X月参加工作20XX年X月加入中国共产党。19XX年X月至20XX年X月先后在XXXX工作20XX年X月至20XX年X月任XXXXX20XX年X月至20XX年X月任XXXX20XX年X月至今任XXXXX。",
"remark": null
},
{
"name": "被核查人员谈话中的表现",
"field_code": "被核查人员谈话中的表现",
"example": "空下",
"remark": null
},
{
"name": "被核查人员交代问题程度",
"field_code": "被核查人员交代问题程度",
"example": "交代充分",
"remark": null
},
{
"name": "被核查人员本人认识和态度",
"field_code": "被核查人员本人认识和态度",
"example": "积极配合谈话,如实回答问题",
"remark": null
},
{
"name": "被核查人员思想负担程度",
"field_code": "被核查人员思想负担程度",
"example": "轻微",
"remark": null
},
{
"name": "被核查人员问题严重程度",
"field_code": "被核查人员问题严重程度",
"example": "轻微",
"remark": null
},
{
"name": "被核查人员涉及其他问题的可能性",
"field_code": "被核查人员涉及其他问题的可能性",
"example": "无",
"remark": "几乎都是“无”"
},
{
"name": "被核查人员减压后的表现",
"field_code": "被核查人员减压后的表现",
"example": "积极改正,表现良好",
"remark": null
},
{
"name": "被核查人员其他情况",
"field_code": "被核查人员其他情况",
"example": "空下",
"remark": null
},
{
"name": "被核查人员风险等级",
"field_code": "被核查人员风险等级",
"example": "低/中/高",
"remark": "三选一"
},
{
"name": "评估意见",
"field_code": "评估意见",
"example": "空下",
"remark": null
}
]
},
"请示报告卡(初核报告结论)": {
"template_name": "请示报告卡(初核报告结论)",
"template_code": "REPORT_CARD初核报告结论",
"input_fields": [],
"output_fields": [
{
"name": "核查组代号",
"field_code": "核查组代号",
"example": "空下",
"remark": null
},
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "被核查人问题描述",
"field_code": "被核查人问题描述",
"example": "违反国家计划生育有关政策规定于2010年10月生育二胎。",
"remark": null
},
{
"name": "被核查人员本人认识和态度",
"field_code": "被核查人员本人认识和态度",
"example": "积极配合谈话,如实回答问题",
"remark": null
}
]
},
"XXX初核情况报告": {
"template_name": "XXX初核情况报告",
"template_code": "XXX初核情况报告",
"input_fields": [
{
"name": "线索信息",
"field_code": "clue_info"
},
{
"name": "被核查人员工作基本情况线索",
"field_code": "target_basic_info_clue"
}
],
"output_fields": [
{
"name": "被核查人姓名",
"field_code": "target_name",
"example": "张三",
"remark": null
},
{
"name": "纪委名称",
"field_code": "纪委名称",
"example": "空下",
"remark": null
},
{
"name": "被核查人员工作基本情况",
"field_code": "被核查人员工作基本情况",
"example": "19XX年X月参加工作20XX年X月加入中国共产党。19XX年X月至20XX年X月先后在XXXX工作20XX年X月至20XX年X月任XXXXX20XX年X月至20XX年X月任XXXX20XX年X月至今任XXXXX。",
"remark": null
},
{
"name": "主要问题线索",
"field_code": "主要问题线索",
"example": "违反国家计划生育有关政策规定于2010年10月生育二胎。",
"remark": null
},
{
"name": "被核查人问题描述",
"field_code": "被核查人问题描述",
"example": "违反国家计划生育有关政策规定于2010年10月生育二胎。",
"remark": null
},
{
"name": "被核查人单位及职务",
"field_code": "被核查人单位及职务",
"example": "某公司总经理",
"remark": null
}
]
}
}

View File

@ -4,4 +4,7 @@ pymysql==1.1.2
python-dotenv==1.0.0
requests==2.31.0
flasgger==0.9.7.1
python-docx==1.1.0
minio==7.2.3
openpyxl==3.1.2

Binary file not shown.

View File

@ -0,0 +1,282 @@
"""
文档生成服务 - 处理Word模板填充和MinIO文件上传
"""
import os
import re
import tempfile
from typing import Dict, List, Optional
from datetime import datetime
from pathlib import Path
from docx import Document
from minio import Minio
from minio.error import S3Error
import pymysql
class DocumentService:
"""文档生成服务类"""
def __init__(self):
# MinIO配置
self.minio_config = {
'endpoint': os.getenv('MINIO_ENDPOINT', 'minio.datacubeworld.com:9000'),
'access_key': os.getenv('MINIO_ACCESS_KEY', 'JOLXFXny3avFSzB0uRA5'),
'secret_key': os.getenv('MINIO_SECRET_KEY', 'G1BR8jStNfovkfH5ou39EmPl34E4l7dGrnd3Cz0I'),
'secure': os.getenv('MINIO_SECURE', 'true').lower() == 'true'
}
self.bucket_name = os.getenv('MINIO_BUCKET', 'finyx')
# 数据库配置
self.db_config = {
'host': os.getenv('DB_HOST', '152.136.177.240'),
'port': int(os.getenv('DB_PORT', 5012)),
'user': os.getenv('DB_USER', 'finyx'),
'password': os.getenv('DB_PASSWORD', '6QsGK6MpePZDE57Z'),
'database': os.getenv('DB_NAME', 'finyx'),
'charset': 'utf8mb4'
}
self.tenant_id = 615873064429507639
def get_connection(self):
"""获取数据库连接"""
return pymysql.connect(**self.db_config)
def get_minio_client(self):
"""获取MinIO客户端"""
return Minio(
self.minio_config['endpoint'],
access_key=self.minio_config['access_key'],
secret_key=self.minio_config['secret_key'],
secure=self.minio_config['secure']
)
def get_file_config_by_template_code(self, template_code: str) -> Optional[Dict]:
"""
根据模板编码获取文件配置
Args:
template_code: 模板编码 'PRELIMINARY_VERIFICATION_APPROVAL'
Returns:
文件配置信息包含: id, name, file_path, template_code
"""
conn = self.get_connection()
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
# 查询文件配置使用template_code字段
sql = """
SELECT id, name, file_path, template_code
FROM f_polic_file_config
WHERE tenant_id = %s
AND template_code = %s
AND state = 1
LIMIT 1
"""
cursor.execute(sql, (self.tenant_id, template_code))
config = cursor.fetchone()
if config:
return {
'id': config['id'],
'name': config['name'],
'file_path': config['file_path'],
'template_code': config.get('template_code', template_code)
}
return None
finally:
cursor.close()
conn.close()
def download_template_from_minio(self, file_path: str) -> str:
"""
从MinIO下载模板文件到临时目录
Args:
file_path: MinIO中的相对路径 '/615873064429507639/TEMPLATE/2024/11/初步核实审批表模板.docx'
Returns:
本地临时文件路径
"""
client = self.get_minio_client()
# 创建临时文件
temp_dir = tempfile.gettempdir()
temp_file = os.path.join(temp_dir, f"template_{datetime.now().strftime('%Y%m%d%H%M%S')}.docx")
try:
# 从相对路径中提取对象名称(去掉开头的/
object_name = file_path.lstrip('/')
# 下载文件
client.fget_object(self.bucket_name, object_name, temp_file)
return temp_file
except S3Error as e:
raise Exception(f"从MinIO下载模板文件失败: {str(e)}")
def fill_template(self, template_path: str, field_data: Dict[str, str]) -> str:
"""
填充Word模板中的占位符
Args:
template_path: 模板文件路径
field_data: 字段数据字典格式: {'field_code': 'field_value'}
Returns:
填充后的文档路径
"""
try:
# 打开模板文档
doc = Document(template_path)
# 替换占位符 {{field_code}} 为实际值
for paragraph in doc.paragraphs:
# 替换段落文本中的占位符
for field_code, field_value in field_data.items():
placeholder = f"{{{{{field_code}}}}}"
if placeholder in paragraph.text:
# 替换占位符
for run in paragraph.runs:
if placeholder in run.text:
run.text = run.text.replace(placeholder, field_value or '')
# 替换表格中的占位符
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
for field_code, field_value in field_data.items():
placeholder = f"{{{{{field_code}}}}}"
if placeholder in paragraph.text:
for run in paragraph.runs:
if placeholder in run.text:
run.text = run.text.replace(placeholder, field_value or '')
# 保存到临时文件
temp_dir = tempfile.gettempdir()
output_file = os.path.join(temp_dir, f"filled_{datetime.now().strftime('%Y%m%d%H%M%S')}.docx")
doc.save(output_file)
return output_file
except Exception as e:
raise Exception(f"填充模板失败: {str(e)}")
def upload_to_minio(self, file_path: str, file_name: str) -> str:
"""
上传文件到MinIO
Args:
file_path: 本地文件路径
file_name: 文件名称
Returns:
MinIO中的相对路径
"""
client = self.get_minio_client()
try:
# 生成MinIO对象路径相对路径
now = datetime.now()
# 使用日期路径组织文件
object_name = f"{self.tenant_id}/{now.strftime('%Y%m%d%H%M%S')}/{file_name}"
# 上传文件
client.fput_object(
self.bucket_name,
object_name,
file_path,
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
# 返回相对路径(以/开头)
return f"/{object_name}"
except S3Error as e:
raise Exception(f"上传文件到MinIO失败: {str(e)}")
def generate_document(self, template_code: str, input_data: List[Dict], file_info: Dict) -> Dict:
"""
生成文档
Args:
template_code: 模板编码
input_data: 输入数据列表格式: [{'fieldCode': 'xxx', 'fieldValue': 'xxx'}]
file_info: 文件信息格式: {'fileId': 1, 'fileName': 'xxx.doc', 'templateCode': 'xxx'}
Returns:
生成结果包含: filePath
"""
# 获取文件配置
file_config = self.get_file_config_by_template_code(template_code)
if not file_config:
raise Exception(f"模板编码 {template_code} 不存在")
# 将input_data转换为字典格式
field_data = {}
for item in input_data:
field_code = item.get('fieldCode', '')
field_value = item.get('fieldValue', '')
if field_code:
field_data[field_code] = field_value or ''
# 下载模板
template_path = None
filled_doc_path = None
try:
template_path = self.download_template_from_minio(file_config['file_path'])
# 填充模板
filled_doc_path = self.fill_template(template_path, field_data)
# 上传到MinIO
file_name = file_info.get('fileName', 'generated.docx')
file_path = self.upload_to_minio(filled_doc_path, file_name)
return {
'filePath': file_path
}
finally:
# 清理临时文件
if template_path and os.path.exists(template_path):
try:
os.remove(template_path)
except:
pass
if filled_doc_path and os.path.exists(filled_doc_path):
try:
os.remove(filled_doc_path)
except:
pass
def generate_document_id(self) -> str:
"""生成文档ID"""
now = datetime.now()
return f"DOC{now.strftime('%Y%m%d%H%M%S')}{str(now.microsecond)[:3]}"
def generate_document_name(self, original_file_name: str, field_data: Dict[str, str]) -> str:
"""
生成文档名称
Args:
original_file_name: 原始文件名称
field_data: 字段数据
Returns:
生成的文档名称 "初步核实审批表_张三.docx"
"""
# 提取文件基础名称(不含扩展名)
base_name = Path(original_file_name).stem
# 尝试从字段数据中提取被核查人姓名作为后缀
suffix = ''
if 'target_name' in field_data and field_data['target_name']:
suffix = f"_{field_data['target_name']}"
# 生成新文件名
return f"{base_name}{suffix}.docx"

View File

@ -83,33 +83,34 @@ class FieldService:
"""获取数据库连接"""
return pymysql.connect(**self.db_config)
def get_output_fields_by_business_type(self, business_type: str) -> List[Dict]:
def get_output_fields_by_field_codes(self, field_codes: List[str]) -> List[Dict]:
"""
根据业务类型获取输出字段列表
根据字段编码列表获取输出字段列表
Args:
business_type: 业务类型 'INVESTIGATION'
field_codes: 字段编码列表 ['userName', 'userAge']
Returns:
字段列表每个字段包含: id, name, field_code, field_type
"""
if not field_codes:
return []
conn = self.get_connection()
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
# 查询"初步核实审批表"相关的输出字段field_type=2
# 目前只支持初步核实审批表后续可以根据business_type扩展
sql = """
# 根据字段编码查询字段信息
placeholders = ','.join(['%s'] * len(field_codes))
sql = f"""
SELECT f.id, f.name, f.filed_code as field_code, f.field_type
FROM f_polic_field f
INNER JOIN f_polic_file_field ff ON f.id = ff.filed_id
INNER JOIN f_polic_file_config fc ON ff.file_id = fc.id
WHERE f.tenant_id = %s
AND f.filed_code IN ({placeholders})
AND f.field_type = 2
AND fc.name = '初步核实审批表'
ORDER BY f.id
"""
cursor.execute(sql, (self.tenant_id,))
cursor.execute(sql, [self.tenant_id] + field_codes)
fields = cursor.fetchall()
# 转换为字典列表
@ -128,6 +129,44 @@ class FieldService:
cursor.close()
conn.close()
def get_input_field_by_field_code(self, field_code: str) -> Optional[Dict]:
"""
根据字段编码获取输入字段信息
Args:
field_code: 字段编码
Returns:
字段信息字典如果不存在返回None
"""
conn = self.get_connection()
cursor = conn.cursor(pymysql.cursors.DictCursor)
try:
sql = """
SELECT f.id, f.name, f.filed_code as field_code, f.field_type
FROM f_polic_field f
WHERE f.tenant_id = %s
AND f.filed_code = %s
AND f.field_type = 1
LIMIT 1
"""
cursor.execute(sql, (self.tenant_id, field_code))
field = cursor.fetchone()
if field:
return {
'id': field['id'],
'name': field['name'],
'field_code': field['field_code'],
'field_type': field['field_type']
}
return None
finally:
cursor.close()
conn.close()
def get_fields_by_business_type(self, business_type: str) -> Dict:
"""
获取业务类型的所有字段包括输入和输出字段
@ -188,14 +227,13 @@ class FieldService:
cursor.close()
conn.close()
def build_extract_prompt(self, input_data: List[Dict], output_fields: List[Dict], business_type: str = 'INVESTIGATION') -> str:
def build_extract_prompt(self, input_data: List[Dict], output_fields: List[Dict]) -> str:
"""
构建AI提取提示词
Args:
input_data: 输入数据列表格式: [{'fieldCode': 'xxx', 'fieldValue': 'xxx'}]
output_fields: 输出字段列表
business_type: 业务类型用于获取特定规则
Returns:
构建好的提示词
@ -203,7 +241,6 @@ class FieldService:
# 获取配置
template = self.prompt_config.get('prompt_template', {})
formatting = self.prompt_config.get('field_formatting', {})
business_rules = self.prompt_config.get('business_type_rules', {}).get(business_type, {})
# 构建输入文本
input_field_format = formatting.get('input_field_format', '{field_code}: {field_value}')
@ -234,13 +271,10 @@ class FieldService:
# 获取要求列表
requirements = template.get('requirements', [])
# 添加业务类型特定的要求
additional_requirements = business_rules.get('additional_requirements', [])
all_requirements = requirements + additional_requirements
# 构建要求文本
requirements_text = ""
for i, req in enumerate(all_requirements, 1):
for i, req in enumerate(requirements, 1):
requirements_text += f"{i}. {req}\n"
# 构建完整提示词

148
test_api.py Normal file
View File

@ -0,0 +1,148 @@
"""
API接口测试脚本
用于测试解析接口和文档生成接口
"""
import requests
import json
BASE_URL = "http://localhost:7500"
def test_extract_api():
"""测试解析接口"""
print("="*60)
print("测试解析接口 (/ai/extract)")
print("="*60)
url = f"{BASE_URL}/ai/extract"
# 测试数据
test_data = {
"inputData": [
{
"fieldCode": "clue_info",
"fieldValue": "被举报用户名称是张三年龄30岁某公司总经理男性1980年5月出生中共党员"
}
],
"outputData": [
{"fieldCode": "target_name"},
{"fieldCode": "target_gender"},
{"fieldCode": "target_organization_and_position"}
]
}
print(f"\n请求URL: {url}")
print(f"请求数据:\n{json.dumps(test_data, ensure_ascii=False, indent=2)}")
try:
response = requests.post(url, json=test_data, timeout=30)
print(f"\n响应状态码: {response.status_code}")
result = response.json()
print(f"响应数据:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
if result.get('isSuccess'):
print("\n✓ 解析接口测试成功!")
if result.get('data', {}).get('outData'):
print("\n提取的字段:")
for item in result['data']['outData']:
print(f" - {item.get('fieldCode')}: {item.get('fieldValue', '(空)')}")
else:
print(f"\n✗ 解析接口测试失败: {result.get('errorMsg', '未知错误')}")
except requests.exceptions.ConnectionError:
print("\n✗ 连接失败:请确保服务已启动")
except requests.exceptions.Timeout:
print("\n✗ 请求超时")
except Exception as e:
print(f"\n✗ 测试失败: {str(e)}")
print()
def test_generate_document_api():
"""测试文档生成接口"""
print("="*60)
print("测试文档生成接口 (/ai/generate-document)")
print("="*60)
url = f"{BASE_URL}/ai/generate-document"
# 测试数据
test_data = {
"inputData": [
{
"fieldCode": "target_name",
"fieldValue": "张三"
},
{
"fieldCode": "target_gender",
"fieldValue": ""
},
{
"fieldCode": "target_organization_and_position",
"fieldValue": "某公司总经理"
}
],
"fpolicFieldParamFileList": [
{
"fileId": 1,
"fileName": "测试文档.doc",
"templateCode": "PRELIMINARY_VERIFICATION_APPROVAL"
}
]
}
print(f"\n请求URL: {url}")
print(f"请求数据:\n{json.dumps(test_data, ensure_ascii=False, indent=2)}")
try:
response = requests.post(url, json=test_data, timeout=60)
print(f"\n响应状态码: {response.status_code}")
result = response.json()
print(f"响应数据:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
if result.get('isSuccess'):
print("\n✓ 文档生成接口测试成功!")
if result.get('data', {}).get('fpolicFieldParamFileList'):
print("\n生成的文件:")
for file_info in result['data']['fpolicFieldParamFileList']:
print(f" - {file_info.get('fileName')}: {file_info.get('filePath', '(无路径)')}")
else:
print(f"\n✗ 文档生成接口测试失败: {result.get('errorMsg', '未知错误')}")
print(f"错误码: {result.get('code', 'N/A')}")
except requests.exceptions.ConnectionError:
print("\n✗ 连接失败:请确保服务已启动")
except requests.exceptions.Timeout:
print("\n✗ 请求超时(文档生成可能需要较长时间)")
except Exception as e:
print(f"\n✗ 测试失败: {str(e)}")
import traceback
traceback.print_exc()
print()
def main():
"""主函数"""
print("\n" + "="*60)
print("API接口测试")
print("="*60)
print(f"\n服务地址: {BASE_URL}")
print("请确保服务已启动(运行 python app.py\n")
# 测试解析接口
test_extract_api()
# 测试文档生成接口注释掉因为需要MinIO配置和模板文件
# test_generate_document_api()
print("="*60)
print("测试完成")
print("="*60)
if __name__ == '__main__':
main()

302
test_complete.py Normal file
View File

@ -0,0 +1,302 @@
"""
完整测试脚本 - 测试所有功能
"""
import requests
import json
import sys
import time
BASE_URL = "http://localhost:7500"
def print_section(title):
"""打印章节标题"""
print("\n" + "="*60)
print(f" {title}")
print("="*60)
def print_result(success, message):
"""打印测试结果"""
status = "" if success else ""
print(f"{status} {message}")
def test_server_connection():
"""测试服务连接"""
print_section("1. 测试服务连接")
try:
response = requests.get(f"{BASE_URL}/", timeout=5)
if response.status_code == 200:
print_result(True, f"服务连接成功 (状态码: {response.status_code})")
return True
else:
print_result(False, f"服务连接异常 (状态码: {response.status_code})")
return False
except requests.exceptions.ConnectionError:
print_result(False, "无法连接到服务,请确保服务已启动 (运行 python app.py)")
return False
except Exception as e:
print_result(False, f"连接失败: {str(e)}")
return False
def test_extract_api():
"""测试解析接口"""
print_section("2. 测试解析接口 (/ai/extract)")
url = f"{BASE_URL}/ai/extract"
# 测试数据
test_data = {
"inputData": [
{
"fieldCode": "clue_info",
"fieldValue": "被举报用户名称是张三年龄30岁某公司总经理男性1980年5月出生中共党员正处级"
}
],
"outputData": [
{"fieldCode": "target_name"},
{"fieldCode": "target_gender"},
{"fieldCode": "target_organization_and_position"}
]
}
print(f"\n请求URL: {url}")
print(f"请求数据:")
print(json.dumps(test_data, ensure_ascii=False, indent=2))
try:
print("\n正在发送请求...")
response = requests.post(url, json=test_data, timeout=30)
print(f"响应状态码: {response.status_code}")
result = response.json()
print(f"\n响应数据:")
print(json.dumps(result, ensure_ascii=False, indent=2))
if result.get('isSuccess'):
print_result(True, "解析接口调用成功")
out_data = result.get('data', {}).get('outData', [])
if out_data:
print("\n提取的字段:")
for item in out_data:
field_code = item.get('fieldCode', '')
field_value = item.get('fieldValue', '')
if field_value:
print(f" - {field_code}: {field_value}")
else:
print(f" - {field_code}: (空)")
return True
else:
error_msg = result.get('errorMsg', '未知错误')
error_code = result.get('code', 'N/A')
print_result(False, f"解析失败 - 错误码: {error_code}, 错误信息: {error_msg}")
return False
except requests.exceptions.Timeout:
print_result(False, "请求超时超过30秒")
return False
except requests.exceptions.ConnectionError:
print_result(False, "无法连接到服务")
return False
except json.JSONDecodeError as e:
print_result(False, f"响应JSON解析失败: {str(e)}")
return False
except Exception as e:
print_result(False, f"测试失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def test_generate_document_api():
"""测试文档生成接口需要MinIO配置"""
print_section("3. 测试文档生成接口 (/ai/generate-document)")
url = f"{BASE_URL}/ai/generate-document"
# 测试数据
test_data = {
"inputData": [
{
"fieldCode": "target_name",
"fieldValue": "张三"
},
{
"fieldCode": "target_gender",
"fieldValue": ""
},
{
"fieldCode": "target_organization_and_position",
"fieldValue": "某公司总经理"
}
],
"fpolicFieldParamFileList": [
{
"fileId": 1,
"fileName": "测试文档.doc",
"templateCode": "PRELIMINARY_VERIFICATION_APPROVAL"
}
]
}
print(f"\n请求URL: {url}")
print(f"请求数据:")
print(json.dumps(test_data, ensure_ascii=False, indent=2))
print("\n⚠ 注意此接口需要MinIO配置和模板文件可能会失败")
print("按 Enter 继续,或 Ctrl+C 跳过此测试...")
try:
input()
except KeyboardInterrupt:
print("\n已跳过文档生成接口测试")
return None
try:
print("\n正在发送请求...")
response = requests.post(url, json=test_data, timeout=60)
print(f"响应状态码: {response.status_code}")
result = response.json()
print(f"\n响应数据:")
print(json.dumps(result, ensure_ascii=False, indent=2))
if result.get('isSuccess'):
print_result(True, "文档生成接口调用成功")
file_list = result.get('data', {}).get('fpolicFieldParamFileList', [])
if file_list:
print("\n生成的文件:")
for file_info in file_list:
print(f" - {file_info.get('fileName')}: {file_info.get('filePath', '(无路径)')}")
return True
else:
error_msg = result.get('errorMsg', '未知错误')
error_code = result.get('code', 'N/A')
print_result(False, f"文档生成失败 - 错误码: {error_code}, 错误信息: {error_msg}")
# 提供错误诊断
if error_code == 1001:
print("\n 诊断:模板不存在,请检查:")
print(" 1. templateCode 是否正确")
print(" 2. 数据库中是否有对应的文件配置")
print(" 3. 运行 python update_template_code_field.py 更新配置")
elif error_code == 3001:
print("\n 诊断:文件生成失败,请检查:")
print(" 1. MinIO配置是否正确")
print(" 2. 模板文件是否存在于MinIO")
elif error_code == 3002:
print("\n 诊断:文件保存失败,请检查:")
print(" 1. MinIO服务是否可访问")
print(" 2. 存储桶是否存在")
return False
except requests.exceptions.Timeout:
print_result(False, "请求超时超过60秒")
return False
except requests.exceptions.ConnectionError:
print_result(False, "无法连接到服务")
return False
except Exception as e:
print_result(False, f"测试失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def test_api_docs():
"""测试API文档页面"""
print_section("4. 测试API文档页面")
try:
response = requests.get(f"{BASE_URL}/api-docs", timeout=5)
if response.status_code == 200:
print_result(True, f"API文档页面可访问 (状态码: {response.status_code})")
print(f" URL: {BASE_URL}/api-docs")
return True
else:
print_result(False, f"API文档页面访问异常 (状态码: {response.status_code})")
return False
except Exception as e:
print_result(False, f"访问失败: {str(e)}")
return False
def main():
"""主函数"""
print("\n" + "="*60)
print(" 智慧监督AI文书写作服务 - 完整功能测试")
print("="*60)
print(f"\n测试目标: {BASE_URL}")
print("请确保服务已启动(运行 python app.py\n")
results = {
'server': False,
'extract': False,
'generate_document': None,
'api_docs': False
}
# 1. 测试服务连接
results['server'] = test_server_connection()
if not results['server']:
print("\n" + "="*60)
print(" 服务未启动,无法继续测试")
print("="*60)
print("\n请先启动服务:")
print(" python app.py")
return
# 2. 测试解析接口
results['extract'] = test_extract_api()
# 3. 测试文档生成接口(可选)
results['generate_document'] = test_generate_document_api()
# 4. 测试API文档
results['api_docs'] = test_api_docs()
# 总结
print_section("测试总结")
print("\n测试结果:")
print(f" 服务连接: {'✓ 通过' if results['server'] else '✗ 失败'}")
print(f" 解析接口: {'✓ 通过' if results['extract'] else '✗ 失败'}")
if results['generate_document'] is not None:
print(f" 文档生成: {'✓ 通过' if results['generate_document'] else '✗ 失败'}")
else:
print(f" 文档生成: ⊘ 跳过")
print(f" API文档: {'✓ 通过' if results['api_docs'] else '✗ 失败'}")
# 计算成功率
test_count = sum(1 for v in results.values() if v is not None)
pass_count = sum(1 for v in results.values() if v is True)
print(f"\n通过率: {pass_count}/{test_count} ({pass_count*100//test_count if test_count > 0 else 0}%)")
print("\n" + "="*60)
print(" 测试完成")
print("="*60)
print("\n提示:")
if not results['extract']:
print(" - 如果解析接口失败,请检查:")
print(" 1. 数据库连接是否正常")
print(" 2. 字段是否已初始化(运行 python init_all_fields_from_excel.py")
print(" 3. AI服务配置是否正确检查 .env 文件中的 SILICONFLOW_API_KEY")
if results['generate_document'] is False:
print(" - 文档生成接口需要:")
print(" 1. MinIO服务配置")
print(" 2. 模板文件存在于MinIO")
print(" 3. 数据库中有正确的文件配置")
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\n\n测试已中断")
sys.exit(1)

View File

@ -53,9 +53,10 @@ def test_field_service():
from services.field_service import FieldService
field_service = FieldService()
# 测试获取字段
fields = field_service.get_output_fields_by_business_type('INVESTIGATION')
print(f"✓ 成功获取 {len(fields)} 个输出字段")
# 测试获取字段(使用新的方法)
field_codes = ['target_name', 'target_gender']
fields = field_service.get_output_fields_by_field_codes(field_codes)
print(f"✓ 成功获取 {len(fields)} 个输出字段通过fieldCode查询")
if fields:
print(f" 示例字段: {fields[0].get('name', 'N/A')} ({fields[0].get('field_code', 'N/A')})")
@ -128,3 +129,4 @@ if __name__ == '__main__':
success = main()
sys.exit(0 if success else 1)

View File

@ -0,0 +1,165 @@
"""
更新f_polic_file_config表确保使用template_code字段
从input_data字段中提取template_code并更新到template_code字段
"""
import pymysql
import json
from datetime import datetime
# 数据库连接配置
DB_CONFIG = {
'host': '152.136.177.240',
'port': 5012,
'user': 'finyx',
'password': '6QsGK6MpePZDE57Z',
'database': 'finyx',
'charset': 'utf8mb4'
}
# 固定值
TENANT_ID = 615873064429507639
UPDATED_BY = 655162080928945152
CURRENT_TIME = datetime.now()
# 模板名称到template_code的映射
TEMPLATE_NAME_TO_CODE = {
'初步核实审批表': 'PRELIMINARY_VERIFICATION_APPROVAL',
'请示报告卡': 'REPORT_CARD',
# 可以根据实际情况添加其他映射
}
def update_template_code_from_input_data():
"""
从input_data字段中提取template_code并更新到template_code字段
如果template_code字段为空则从input_data中提取
"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("="*60)
print("开始更新template_code字段...")
print("="*60)
try:
# 查询所有文件配置
sql = """
SELECT id, name, input_data, template_code
FROM f_polic_file_config
WHERE tenant_id = %s
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
updated_count = 0
for config in configs:
config_id = config['id']
config_name = config['name']
input_data = config.get('input_data')
existing_template_code = config.get('template_code')
new_template_code = None
# 如果已有template_code跳过
if existing_template_code:
print(f"{config_name} 已有template_code: {existing_template_code}")
continue
# 方法1: 从input_data中提取
if input_data:
try:
input_data_dict = json.loads(input_data) if isinstance(input_data, str) else input_data
if isinstance(input_data_dict, dict):
new_template_code = input_data_dict.get('template_code')
if new_template_code:
print(f" ✓ 从input_data中提取到template_code: {new_template_code}")
except:
pass
# 方法2: 从映射表中获取
if not new_template_code and config_name in TEMPLATE_NAME_TO_CODE:
new_template_code = TEMPLATE_NAME_TO_CODE[config_name]
print(f" ✓ 从映射表获取template_code: {new_template_code}")
# 如果找到了template_code更新数据库
if new_template_code:
update_sql = """
UPDATE f_polic_file_config
SET template_code = %s, updated_time = %s, updated_by = %s
WHERE id = %s
"""
cursor.execute(update_sql, (new_template_code, CURRENT_TIME, UPDATED_BY, config_id))
updated_count += 1
print(f" ✓ 更新 {config_name} 的template_code: {new_template_code}")
else:
print(f" ⚠ 无法确定 {config_name} 的template_code请手动设置")
conn.commit()
print("\n" + "="*60)
print(f"更新完成!共更新 {updated_count} 条记录")
print("="*60)
finally:
cursor.close()
conn.close()
def verify_template_code():
"""验证template_code字段"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
print("\n" + "="*60)
print("验证template_code字段...")
print("="*60)
try:
sql = """
SELECT id, name, template_code, state
FROM f_polic_file_config
WHERE tenant_id = %s
ORDER BY name
"""
cursor.execute(sql, (TENANT_ID,))
configs = cursor.fetchall()
print(f"\n共找到 {len(configs)} 个文件配置:\n")
for config in configs:
template_code = config.get('template_code')
state_str = "启用" if config.get('state') == 1 else "未启用"
if template_code:
print(f"{config['name']}")
print(f" template_code: {template_code}")
print(f" 状态: {state_str}")
else:
print(f"{config['name']} (缺少template_code)")
print(f" 状态: {state_str}")
print()
finally:
cursor.close()
conn.close()
def main():
"""主函数"""
try:
# 更新template_code字段
update_template_code_from_input_data()
# 验证结果
verify_template_code()
except Exception as e:
print(f"\n错误: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
#
# Cipher/AES.py : AES
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
import sys
from Crypto.Cipher import _create_cipher
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
VoidPointer, SmartPointer,
c_size_t, c_uint8_ptr)
from Crypto.Util import _cpu_features
from Crypto.Random import get_random_bytes
MODE_ECB = 1 #: Electronic Code Book (:ref:`ecb_mode`)
MODE_CBC = 2 #: Cipher-Block Chaining (:ref:`cbc_mode`)
MODE_CFB = 3 #: Cipher Feedback (:ref:`cfb_mode`)
MODE_OFB = 5 #: Output Feedback (:ref:`ofb_mode`)
MODE_CTR = 6 #: Counter mode (:ref:`ctr_mode`)
MODE_OPENPGP = 7 #: OpenPGP mode (:ref:`openpgp_mode`)
MODE_CCM = 8 #: Counter with CBC-MAC (:ref:`ccm_mode`)
MODE_EAX = 9 #: :ref:`eax_mode`
MODE_SIV = 10 #: Synthetic Initialization Vector (:ref:`siv_mode`)
MODE_GCM = 11 #: Galois Counter Mode (:ref:`gcm_mode`)
MODE_OCB = 12 #: Offset Code Book (:ref:`ocb_mode`)
MODE_KW = 13 #: Key Wrap (:ref:`kw_mode`)
MODE_KWP = 14 #: Key Wrap with Padding (:ref:`kwp_mode`)
_cproto = """
int AES_start_operation(const uint8_t key[],
size_t key_len,
void **pResult);
int AES_encrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int AES_decrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int AES_stop_operation(void *state);
"""
# Load portable AES
_raw_aes_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_aes",
_cproto)
# Try to load AES with AES NI instructions
try:
_raw_aesni_lib = None
if _cpu_features.have_aes_ni():
_raw_aesni_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_aesni",
_cproto.replace("AES",
"AESNI"))
# _raw_aesni may not have been compiled in
except OSError:
pass
def _create_base_cipher(dict_parameters):
"""This method instantiates and returns a handle to a low-level
base cipher. It will absorb named parameters in the process."""
use_aesni = dict_parameters.pop("use_aesni", True)
try:
key = dict_parameters.pop("key")
except KeyError:
raise TypeError("Missing 'key' parameter")
if len(key) not in key_size:
raise ValueError("Incorrect AES key length (%d bytes)" % len(key))
if use_aesni and _raw_aesni_lib:
start_operation = _raw_aesni_lib.AESNI_start_operation
stop_operation = _raw_aesni_lib.AESNI_stop_operation
else:
start_operation = _raw_aes_lib.AES_start_operation
stop_operation = _raw_aes_lib.AES_stop_operation
cipher = VoidPointer()
result = start_operation(c_uint8_ptr(key),
c_size_t(len(key)),
cipher.address_of())
if result:
raise ValueError("Error %X while instantiating the AES cipher"
% result)
return SmartPointer(cipher.get(), stop_operation)
def _derive_Poly1305_key_pair(key, nonce):
"""Derive a tuple (r, s, nonce) for a Poly1305 MAC.
If nonce is ``None``, a new 16-byte nonce is generated.
"""
if len(key) != 32:
raise ValueError("Poly1305 with AES requires a 32-byte key")
if nonce is None:
nonce = get_random_bytes(16)
elif len(nonce) != 16:
raise ValueError("Poly1305 with AES requires a 16-byte nonce")
s = new(key[:16], MODE_ECB).encrypt(nonce)
return key[16:], s, nonce
def new(key, mode, *args, **kwargs):
"""Create a new AES cipher.
Args:
key(bytes/bytearray/memoryview):
The secret key to use in the symmetric cipher.
It must be 16 (*AES-128)*, 24 (*AES-192*) or 32 (*AES-256*) bytes long.
For ``MODE_SIV`` only, it doubles to 32, 48, or 64 bytes.
mode (a ``MODE_*`` constant):
The chaining mode to use for encryption or decryption.
If in doubt, use ``MODE_EAX``.
Keyword Args:
iv (bytes/bytearray/memoryview):
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
and ``MODE_OPENPGP`` modes).
The initialization vector to use for encryption or decryption.
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 16 bytes long.
For ``MODE_OPENPGP`` mode only,
it must be 16 bytes long for encryption
and 18 bytes for decryption (in the latter case, it is
actually the *encrypted* IV which was prefixed to the ciphertext).
If not provided, a random byte string is generated (you must then
read its value with the :attr:`iv` attribute).
nonce (bytes/bytearray/memoryview):
(Only applicable for ``MODE_CCM``, ``MODE_EAX``, ``MODE_GCM``,
``MODE_SIV``, ``MODE_OCB``, and ``MODE_CTR``).
A value that must never be reused for any other encryption done
with this key (except possibly for ``MODE_SIV``, see below).
For ``MODE_EAX``, ``MODE_GCM`` and ``MODE_SIV`` there are no
restrictions on its length (recommended: **16** bytes).
For ``MODE_CCM``, its length must be in the range **[7..13]**.
Bear in mind that with CCM there is a trade-off between nonce
length and maximum message size. Recommendation: **11** bytes.
For ``MODE_OCB``, its length must be in the range **[1..15]**
(recommended: **15**).
For ``MODE_CTR``, its length must be in the range **[0..15]**
(recommended: **8**).
For ``MODE_SIV``, the nonce is optional, if it is not specified,
then no nonce is being used, which renders the encryption
deterministic.
If not provided, for modes other than ``MODE_SIV``, a random
byte string of the recommended length is used (you must then
read its value with the :attr:`nonce` attribute).
segment_size (integer):
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
are segmented in. It must be a multiple of 8.
If not specified, it will be assumed to be 8.
mac_len (integer):
(Only ``MODE_EAX``, ``MODE_GCM``, ``MODE_OCB``, ``MODE_CCM``)
Length of the authentication tag, in bytes.
It must be even and in the range **[4..16]**.
The recommended value (and the default, if not specified) is **16**.
msg_len (integer):
(Only ``MODE_CCM``). Length of the message to (de)cipher.
If not specified, ``encrypt`` must be called with the entire message.
Similarly, ``decrypt`` can only be called once.
assoc_len (integer):
(Only ``MODE_CCM``). Length of the associated data.
If not specified, all associated data is buffered internally,
which may represent a problem for very large messages.
initial_value (integer or bytes/bytearray/memoryview):
(Only ``MODE_CTR``).
The initial value for the counter. If not present, the cipher will
start counting from 0. The value is incremented by one for each block.
The counter number is encoded in big endian mode.
counter (object):
(Only ``MODE_CTR``).
Instance of ``Crypto.Util.Counter``, which allows full customization
of the counter block. This parameter is incompatible to both ``nonce``
and ``initial_value``.
use_aesni: (boolean):
Use Intel AES-NI hardware extensions (default: use if available).
Returns:
an AES object, of the applicable mode.
"""
kwargs["add_aes_modes"] = True
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
# Size of a data block (in bytes)
block_size = 16
# Size of a key (in bytes)
key_size = (16, 24, 32)

View File

@ -0,0 +1,156 @@
from typing import Dict, Optional, Tuple, Union, overload
from typing_extensions import Literal
Buffer=bytes|bytearray|memoryview
from Crypto.Cipher._mode_ecb import EcbMode
from Crypto.Cipher._mode_cbc import CbcMode
from Crypto.Cipher._mode_cfb import CfbMode
from Crypto.Cipher._mode_ofb import OfbMode
from Crypto.Cipher._mode_ctr import CtrMode
from Crypto.Cipher._mode_openpgp import OpenPgpMode
from Crypto.Cipher._mode_ccm import CcmMode
from Crypto.Cipher._mode_eax import EaxMode
from Crypto.Cipher._mode_gcm import GcmMode
from Crypto.Cipher._mode_siv import SivMode
from Crypto.Cipher._mode_ocb import OcbMode
MODE_ECB: Literal[1]
MODE_CBC: Literal[2]
MODE_CFB: Literal[3]
MODE_OFB: Literal[5]
MODE_CTR: Literal[6]
MODE_OPENPGP: Literal[7]
MODE_CCM: Literal[8]
MODE_EAX: Literal[9]
MODE_SIV: Literal[10]
MODE_GCM: Literal[11]
MODE_OCB: Literal[12]
# MODE_ECB
@overload
def new(key: Buffer,
mode: Literal[1],
use_aesni : bool = ...) -> \
EcbMode: ...
# MODE_CBC
@overload
def new(key: Buffer,
mode: Literal[2],
iv : Optional[Buffer] = ...,
use_aesni : bool = ...) -> \
CbcMode: ...
@overload
def new(key: Buffer,
mode: Literal[2],
IV : Optional[Buffer] = ...,
use_aesni : bool = ...) -> \
CbcMode: ...
# MODE_CFB
@overload
def new(key: Buffer,
mode: Literal[3],
iv : Optional[Buffer] = ...,
segment_size : int = ...,
use_aesni : bool = ...) -> \
CfbMode: ...
@overload
def new(key: Buffer,
mode: Literal[3],
IV : Optional[Buffer] = ...,
segment_size : int = ...,
use_aesni : bool = ...) -> \
CfbMode: ...
# MODE_OFB
@overload
def new(key: Buffer,
mode: Literal[5],
iv : Optional[Buffer] = ...,
use_aesni : bool = ...) -> \
OfbMode: ...
@overload
def new(key: Buffer,
mode: Literal[5],
IV : Optional[Buffer] = ...,
use_aesni : bool = ...) -> \
OfbMode: ...
# MODE_CTR
@overload
def new(key: Buffer,
mode: Literal[6],
nonce : Optional[Buffer] = ...,
initial_value : Union[int, Buffer] = ...,
counter : Dict = ...,
use_aesni : bool = ...) -> \
CtrMode: ...
# MODE_OPENPGP
@overload
def new(key: Buffer,
mode: Literal[7],
iv : Optional[Buffer] = ...,
use_aesni : bool = ...) -> \
OpenPgpMode: ...
@overload
def new(key: Buffer,
mode: Literal[7],
IV : Optional[Buffer] = ...,
use_aesni : bool = ...) -> \
OpenPgpMode: ...
# MODE_CCM
@overload
def new(key: Buffer,
mode: Literal[8],
nonce : Optional[Buffer] = ...,
mac_len : int = ...,
assoc_len : int = ...,
use_aesni : bool = ...) -> \
CcmMode: ...
# MODE_EAX
@overload
def new(key: Buffer,
mode: Literal[9],
nonce : Optional[Buffer] = ...,
mac_len : int = ...,
use_aesni : bool = ...) -> \
EaxMode: ...
# MODE_GCM
@overload
def new(key: Buffer,
mode: Literal[10],
nonce : Optional[Buffer] = ...,
use_aesni : bool = ...) -> \
SivMode: ...
# MODE_SIV
@overload
def new(key: Buffer,
mode: Literal[11],
nonce : Optional[Buffer] = ...,
mac_len : int = ...,
use_aesni : bool = ...) -> \
GcmMode: ...
# MODE_OCB
@overload
def new(key: Buffer,
mode: Literal[12],
nonce : Optional[Buffer] = ...,
mac_len : int = ...,
use_aesni : bool = ...) -> \
OcbMode: ...
block_size: int
key_size: Tuple[int, int, int]

View File

@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
#
# Cipher/ARC2.py : ARC2.py
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
"""
Module's constants for the modes of operation supported with ARC2:
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
"""
import sys
from Crypto.Cipher import _create_cipher
from Crypto.Util.py3compat import byte_string
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
VoidPointer, SmartPointer,
c_size_t, c_uint8_ptr)
_raw_arc2_lib = load_pycryptodome_raw_lib(
"Crypto.Cipher._raw_arc2",
"""
int ARC2_start_operation(const uint8_t key[],
size_t key_len,
size_t effective_key_len,
void **pResult);
int ARC2_encrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int ARC2_decrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int ARC2_stop_operation(void *state);
"""
)
def _create_base_cipher(dict_parameters):
"""This method instantiates and returns a handle to a low-level
base cipher. It will absorb named parameters in the process."""
try:
key = dict_parameters.pop("key")
except KeyError:
raise TypeError("Missing 'key' parameter")
effective_keylen = dict_parameters.pop("effective_keylen", 1024)
if len(key) not in key_size:
raise ValueError("Incorrect ARC2 key length (%d bytes)" % len(key))
if not (40 <= effective_keylen <= 1024):
raise ValueError("'effective_key_len' must be at least 40 and no larger than 1024 "
"(not %d)" % effective_keylen)
start_operation = _raw_arc2_lib.ARC2_start_operation
stop_operation = _raw_arc2_lib.ARC2_stop_operation
cipher = VoidPointer()
result = start_operation(c_uint8_ptr(key),
c_size_t(len(key)),
c_size_t(effective_keylen),
cipher.address_of())
if result:
raise ValueError("Error %X while instantiating the ARC2 cipher"
% result)
return SmartPointer(cipher.get(), stop_operation)
def new(key, mode, *args, **kwargs):
"""Create a new RC2 cipher.
:param key:
The secret key to use in the symmetric cipher.
Its length can vary from 5 to 128 bytes; the actual search space
(and the cipher strength) can be reduced with the ``effective_keylen`` parameter.
:type key: bytes, bytearray, memoryview
:param mode:
The chaining mode to use for encryption or decryption.
:type mode: One of the supported ``MODE_*`` constants
:Keyword Arguments:
* **iv** (*bytes*, *bytearray*, *memoryview*) --
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
and ``MODE_OPENPGP`` modes).
The initialization vector to use for encryption or decryption.
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
For ``MODE_OPENPGP`` mode only,
it must be 8 bytes long for encryption
and 10 bytes for decryption (in the latter case, it is
actually the *encrypted* IV which was prefixed to the ciphertext).
If not provided, a random byte string is generated (you must then
read its value with the :attr:`iv` attribute).
* **nonce** (*bytes*, *bytearray*, *memoryview*) --
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
A value that must never be reused for any other encryption done
with this key.
For ``MODE_EAX`` there are no
restrictions on its length (recommended: **16** bytes).
For ``MODE_CTR``, its length must be in the range **[0..7]**.
If not provided for ``MODE_EAX``, a random byte string is generated (you
can read it back via the ``nonce`` attribute).
* **effective_keylen** (*integer*) --
Optional. Maximum strength in bits of the actual key used by the ARC2 algorithm.
If the supplied ``key`` parameter is longer (in bits) of the value specified
here, it will be weakened to match it.
If not specified, no limitation is applied.
* **segment_size** (*integer*) --
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
are segmented in. It must be a multiple of 8.
If not specified, it will be assumed to be 8.
* **mac_len** : (*integer*) --
(Only ``MODE_EAX``)
Length of the authentication tag, in bytes.
It must be no longer than 8 (default).
* **initial_value** : (*integer*) --
(Only ``MODE_CTR``). The initial value for the counter within
the counter block. By default it is **0**.
:Return: an ARC2 object, of the applicable mode.
"""
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
MODE_ECB = 1
MODE_CBC = 2
MODE_CFB = 3
MODE_OFB = 5
MODE_CTR = 6
MODE_OPENPGP = 7
MODE_EAX = 9
# Size of a data block (in bytes)
block_size = 8
# Size of a key (in bytes)
key_size = range(5, 128 + 1)

View File

@ -0,0 +1,35 @@
from typing import Union, Dict, Iterable, Optional
Buffer = bytes|bytearray|memoryview
from Crypto.Cipher._mode_ecb import EcbMode
from Crypto.Cipher._mode_cbc import CbcMode
from Crypto.Cipher._mode_cfb import CfbMode
from Crypto.Cipher._mode_ofb import OfbMode
from Crypto.Cipher._mode_ctr import CtrMode
from Crypto.Cipher._mode_openpgp import OpenPgpMode
from Crypto.Cipher._mode_eax import EaxMode
ARC2Mode = int
MODE_ECB: ARC2Mode
MODE_CBC: ARC2Mode
MODE_CFB: ARC2Mode
MODE_OFB: ARC2Mode
MODE_CTR: ARC2Mode
MODE_OPENPGP: ARC2Mode
MODE_EAX: ARC2Mode
def new(key: Buffer,
mode: ARC2Mode,
iv : Optional[Buffer] = ...,
IV : Optional[Buffer] = ...,
nonce : Optional[Buffer] = ...,
segment_size : int = ...,
mac_len : int = ...,
initial_value : Union[int, Buffer] = ...,
counter : Dict = ...) -> \
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
block_size: int
key_size: Iterable[int]

View File

@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
#
# Cipher/ARC4.py : ARC4
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
create_string_buffer, get_raw_buffer,
SmartPointer, c_size_t, c_uint8_ptr)
_raw_arc4_lib = load_pycryptodome_raw_lib("Crypto.Cipher._ARC4", """
int ARC4_stream_encrypt(void *rc4State, const uint8_t in[],
uint8_t out[], size_t len);
int ARC4_stream_init(uint8_t *key, size_t keylen,
void **pRc4State);
int ARC4_stream_destroy(void *rc4State);
""")
class ARC4Cipher:
"""ARC4 cipher object. Do not create it directly. Use
:func:`Crypto.Cipher.ARC4.new` instead.
"""
def __init__(self, key, *args, **kwargs):
"""Initialize an ARC4 cipher object
See also `new()` at the module level."""
if len(args) > 0:
ndrop = args[0]
args = args[1:]
else:
ndrop = kwargs.pop('drop', 0)
if len(key) not in key_size:
raise ValueError("Incorrect ARC4 key length (%d bytes)" %
len(key))
self._state = VoidPointer()
result = _raw_arc4_lib.ARC4_stream_init(c_uint8_ptr(key),
c_size_t(len(key)),
self._state.address_of())
if result != 0:
raise ValueError("Error %d while creating the ARC4 cipher"
% result)
self._state = SmartPointer(self._state.get(),
_raw_arc4_lib.ARC4_stream_destroy)
if ndrop > 0:
# This is OK even if the cipher is used for decryption,
# since encrypt and decrypt are actually the same thing
# with ARC4.
self.encrypt(b'\x00' * ndrop)
self.block_size = 1
self.key_size = len(key)
def encrypt(self, plaintext):
"""Encrypt a piece of data.
:param plaintext: The data to encrypt, of any size.
:type plaintext: bytes, bytearray, memoryview
:returns: the encrypted byte string, of equal length as the
plaintext.
"""
ciphertext = create_string_buffer(len(plaintext))
result = _raw_arc4_lib.ARC4_stream_encrypt(self._state.get(),
c_uint8_ptr(plaintext),
ciphertext,
c_size_t(len(plaintext)))
if result:
raise ValueError("Error %d while encrypting with RC4" % result)
return get_raw_buffer(ciphertext)
def decrypt(self, ciphertext):
"""Decrypt a piece of data.
:param ciphertext: The data to decrypt, of any size.
:type ciphertext: bytes, bytearray, memoryview
:returns: the decrypted byte string, of equal length as the
ciphertext.
"""
try:
return self.encrypt(ciphertext)
except ValueError as e:
raise ValueError(str(e).replace("enc", "dec"))
def new(key, *args, **kwargs):
"""Create a new ARC4 cipher.
:param key:
The secret key to use in the symmetric cipher.
Its length must be in the range ``[1..256]``.
The recommended length is 16 bytes.
:type key: bytes, bytearray, memoryview
:Keyword Arguments:
* *drop* (``integer``) --
The amount of bytes to discard from the initial part of the keystream.
In fact, such part has been found to be distinguishable from random
data (while it shouldn't) and also correlated to key.
The recommended value is 3072_ bytes. The default value is 0.
:Return: an `ARC4Cipher` object
.. _3072: http://eprint.iacr.org/2002/067.pdf
"""
return ARC4Cipher(key, *args, **kwargs)
# Size of a data block (in bytes)
block_size = 1
# Size of a key (in bytes)
key_size = range(1, 256+1)

View File

@ -0,0 +1,16 @@
from typing import Any, Union, Iterable
Buffer = bytes|bytearray|memoryview
class ARC4Cipher:
block_size: int
key_size: int
def __init__(self, key: Buffer, *args: Any, **kwargs: Any) -> None: ...
def encrypt(self, plaintext: Buffer) -> bytes: ...
def decrypt(self, ciphertext: Buffer) -> bytes: ...
def new(key: Buffer, drop : int = ...) -> ARC4Cipher: ...
block_size: int
key_size: Iterable[int]

View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
#
# Cipher/Blowfish.py : Blowfish
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
"""
Module's constants for the modes of operation supported with Blowfish:
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
"""
import sys
from Crypto.Cipher import _create_cipher
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
VoidPointer, SmartPointer, c_size_t,
c_uint8_ptr)
_raw_blowfish_lib = load_pycryptodome_raw_lib(
"Crypto.Cipher._raw_blowfish",
"""
int Blowfish_start_operation(const uint8_t key[],
size_t key_len,
void **pResult);
int Blowfish_encrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int Blowfish_decrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int Blowfish_stop_operation(void *state);
"""
)
def _create_base_cipher(dict_parameters):
"""This method instantiates and returns a smart pointer to
a low-level base cipher. It will absorb named parameters in
the process."""
try:
key = dict_parameters.pop("key")
except KeyError:
raise TypeError("Missing 'key' parameter")
if len(key) not in key_size:
raise ValueError("Incorrect Blowfish key length (%d bytes)" % len(key))
start_operation = _raw_blowfish_lib.Blowfish_start_operation
stop_operation = _raw_blowfish_lib.Blowfish_stop_operation
void_p = VoidPointer()
result = start_operation(c_uint8_ptr(key),
c_size_t(len(key)),
void_p.address_of())
if result:
raise ValueError("Error %X while instantiating the Blowfish cipher"
% result)
return SmartPointer(void_p.get(), stop_operation)
def new(key, mode, *args, **kwargs):
"""Create a new Blowfish cipher
:param key:
The secret key to use in the symmetric cipher.
Its length can vary from 5 to 56 bytes.
:type key: bytes, bytearray, memoryview
:param mode:
The chaining mode to use for encryption or decryption.
:type mode: One of the supported ``MODE_*`` constants
:Keyword Arguments:
* **iv** (*bytes*, *bytearray*, *memoryview*) --
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
and ``MODE_OPENPGP`` modes).
The initialization vector to use for encryption or decryption.
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
For ``MODE_OPENPGP`` mode only,
it must be 8 bytes long for encryption
and 10 bytes for decryption (in the latter case, it is
actually the *encrypted* IV which was prefixed to the ciphertext).
If not provided, a random byte string is generated (you must then
read its value with the :attr:`iv` attribute).
* **nonce** (*bytes*, *bytearray*, *memoryview*) --
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
A value that must never be reused for any other encryption done
with this key.
For ``MODE_EAX`` there are no
restrictions on its length (recommended: **16** bytes).
For ``MODE_CTR``, its length must be in the range **[0..7]**.
If not provided for ``MODE_EAX``, a random byte string is generated (you
can read it back via the ``nonce`` attribute).
* **segment_size** (*integer*) --
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
are segmented in. It must be a multiple of 8.
If not specified, it will be assumed to be 8.
* **mac_len** : (*integer*) --
(Only ``MODE_EAX``)
Length of the authentication tag, in bytes.
It must be no longer than 8 (default).
* **initial_value** : (*integer*) --
(Only ``MODE_CTR``). The initial value for the counter within
the counter block. By default it is **0**.
:Return: a Blowfish object, of the applicable mode.
"""
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
MODE_ECB = 1
MODE_CBC = 2
MODE_CFB = 3
MODE_OFB = 5
MODE_CTR = 6
MODE_OPENPGP = 7
MODE_EAX = 9
# Size of a data block (in bytes)
block_size = 8
# Size of a key (in bytes)
key_size = range(4, 56 + 1)

View File

@ -0,0 +1,35 @@
from typing import Union, Dict, Iterable, Optional
Buffer = bytes|bytearray|memoryview
from Crypto.Cipher._mode_ecb import EcbMode
from Crypto.Cipher._mode_cbc import CbcMode
from Crypto.Cipher._mode_cfb import CfbMode
from Crypto.Cipher._mode_ofb import OfbMode
from Crypto.Cipher._mode_ctr import CtrMode
from Crypto.Cipher._mode_openpgp import OpenPgpMode
from Crypto.Cipher._mode_eax import EaxMode
BlowfishMode = int
MODE_ECB: BlowfishMode
MODE_CBC: BlowfishMode
MODE_CFB: BlowfishMode
MODE_OFB: BlowfishMode
MODE_CTR: BlowfishMode
MODE_OPENPGP: BlowfishMode
MODE_EAX: BlowfishMode
def new(key: Buffer,
mode: BlowfishMode,
iv : Optional[Buffer] = ...,
IV : Optional[Buffer] = ...,
nonce : Optional[Buffer] = ...,
segment_size : int = ...,
mac_len : int = ...,
initial_value : Union[int, Buffer] = ...,
counter : Dict = ...) -> \
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
block_size: int
key_size: Iterable[int]

View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
#
# Cipher/CAST.py : CAST
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
"""
Module's constants for the modes of operation supported with CAST:
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
"""
import sys
from Crypto.Cipher import _create_cipher
from Crypto.Util.py3compat import byte_string
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
VoidPointer, SmartPointer,
c_size_t, c_uint8_ptr)
_raw_cast_lib = load_pycryptodome_raw_lib(
"Crypto.Cipher._raw_cast",
"""
int CAST_start_operation(const uint8_t key[],
size_t key_len,
void **pResult);
int CAST_encrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int CAST_decrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int CAST_stop_operation(void *state);
""")
def _create_base_cipher(dict_parameters):
"""This method instantiates and returns a handle to a low-level
base cipher. It will absorb named parameters in the process."""
try:
key = dict_parameters.pop("key")
except KeyError:
raise TypeError("Missing 'key' parameter")
if len(key) not in key_size:
raise ValueError("Incorrect CAST key length (%d bytes)" % len(key))
start_operation = _raw_cast_lib.CAST_start_operation
stop_operation = _raw_cast_lib.CAST_stop_operation
cipher = VoidPointer()
result = start_operation(c_uint8_ptr(key),
c_size_t(len(key)),
cipher.address_of())
if result:
raise ValueError("Error %X while instantiating the CAST cipher"
% result)
return SmartPointer(cipher.get(), stop_operation)
def new(key, mode, *args, **kwargs):
"""Create a new CAST cipher
:param key:
The secret key to use in the symmetric cipher.
Its length can vary from 5 to 16 bytes.
:type key: bytes, bytearray, memoryview
:param mode:
The chaining mode to use for encryption or decryption.
:type mode: One of the supported ``MODE_*`` constants
:Keyword Arguments:
* **iv** (*bytes*, *bytearray*, *memoryview*) --
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
and ``MODE_OPENPGP`` modes).
The initialization vector to use for encryption or decryption.
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
For ``MODE_OPENPGP`` mode only,
it must be 8 bytes long for encryption
and 10 bytes for decryption (in the latter case, it is
actually the *encrypted* IV which was prefixed to the ciphertext).
If not provided, a random byte string is generated (you must then
read its value with the :attr:`iv` attribute).
* **nonce** (*bytes*, *bytearray*, *memoryview*) --
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
A value that must never be reused for any other encryption done
with this key.
For ``MODE_EAX`` there are no
restrictions on its length (recommended: **16** bytes).
For ``MODE_CTR``, its length must be in the range **[0..7]**.
If not provided for ``MODE_EAX``, a random byte string is generated (you
can read it back via the ``nonce`` attribute).
* **segment_size** (*integer*) --
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
are segmented in. It must be a multiple of 8.
If not specified, it will be assumed to be 8.
* **mac_len** : (*integer*) --
(Only ``MODE_EAX``)
Length of the authentication tag, in bytes.
It must be no longer than 8 (default).
* **initial_value** : (*integer*) --
(Only ``MODE_CTR``). The initial value for the counter within
the counter block. By default it is **0**.
:Return: a CAST object, of the applicable mode.
"""
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
MODE_ECB = 1
MODE_CBC = 2
MODE_CFB = 3
MODE_OFB = 5
MODE_CTR = 6
MODE_OPENPGP = 7
MODE_EAX = 9
# Size of a data block (in bytes)
block_size = 8
# Size of a key (in bytes)
key_size = range(5, 16 + 1)

View File

@ -0,0 +1,35 @@
from typing import Union, Dict, Iterable, Optional
Buffer = bytes|bytearray|memoryview
from Crypto.Cipher._mode_ecb import EcbMode
from Crypto.Cipher._mode_cbc import CbcMode
from Crypto.Cipher._mode_cfb import CfbMode
from Crypto.Cipher._mode_ofb import OfbMode
from Crypto.Cipher._mode_ctr import CtrMode
from Crypto.Cipher._mode_openpgp import OpenPgpMode
from Crypto.Cipher._mode_eax import EaxMode
CASTMode = int
MODE_ECB: CASTMode
MODE_CBC: CASTMode
MODE_CFB: CASTMode
MODE_OFB: CASTMode
MODE_CTR: CASTMode
MODE_OPENPGP: CASTMode
MODE_EAX: CASTMode
def new(key: Buffer,
mode: CASTMode,
iv : Optional[Buffer] = ...,
IV : Optional[Buffer] = ...,
nonce : Optional[Buffer] = ...,
segment_size : int = ...,
mac_len : int = ...,
initial_value : Union[int, Buffer] = ...,
counter : Dict = ...) -> \
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
block_size: int
key_size : Iterable[int]

View File

@ -0,0 +1,291 @@
# ===================================================================
#
# Copyright (c) 2014, Legrandin <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
from Crypto.Random import get_random_bytes
from Crypto.Util.py3compat import _copy_bytes
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
create_string_buffer,
get_raw_buffer, VoidPointer,
SmartPointer, c_size_t,
c_uint8_ptr, c_ulong,
is_writeable_buffer)
_raw_chacha20_lib = load_pycryptodome_raw_lib("Crypto.Cipher._chacha20",
"""
int chacha20_init(void **pState,
const uint8_t *key,
size_t keySize,
const uint8_t *nonce,
size_t nonceSize);
int chacha20_destroy(void *state);
int chacha20_encrypt(void *state,
const uint8_t in[],
uint8_t out[],
size_t len);
int chacha20_seek(void *state,
unsigned long block_high,
unsigned long block_low,
unsigned offset);
int hchacha20( const uint8_t key[32],
const uint8_t nonce16[16],
uint8_t subkey[32]);
""")
def _HChaCha20(key, nonce):
assert(len(key) == 32)
assert(len(nonce) == 16)
subkey = bytearray(32)
result = _raw_chacha20_lib.hchacha20(
c_uint8_ptr(key),
c_uint8_ptr(nonce),
c_uint8_ptr(subkey))
if result:
raise ValueError("Error %d when deriving subkey with HChaCha20" % result)
return subkey
class ChaCha20Cipher(object):
"""ChaCha20 (or XChaCha20) cipher object.
Do not create it directly. Use :py:func:`new` instead.
:var nonce: The nonce with length 8, 12 or 24 bytes
:vartype nonce: bytes
"""
block_size = 1
def __init__(self, key, nonce):
"""Initialize a ChaCha20/XChaCha20 cipher object
See also `new()` at the module level."""
self.nonce = _copy_bytes(None, None, nonce)
# XChaCha20 requires a key derivation with HChaCha20
# See 2.3 in https://tools.ietf.org/html/draft-arciszewski-xchacha-03
if len(nonce) == 24:
key = _HChaCha20(key, nonce[:16])
nonce = b'\x00' * 4 + nonce[16:]
self._name = "XChaCha20"
else:
self._name = "ChaCha20"
nonce = self.nonce
self._next = ("encrypt", "decrypt")
self._state = VoidPointer()
result = _raw_chacha20_lib.chacha20_init(
self._state.address_of(),
c_uint8_ptr(key),
c_size_t(len(key)),
nonce,
c_size_t(len(nonce)))
if result:
raise ValueError("Error %d instantiating a %s cipher" % (result,
self._name))
self._state = SmartPointer(self._state.get(),
_raw_chacha20_lib.chacha20_destroy)
def encrypt(self, plaintext, output=None):
"""Encrypt a piece of data.
Args:
plaintext(bytes/bytearray/memoryview): The data to encrypt, of any size.
Keyword Args:
output(bytes/bytearray/memoryview): The location where the ciphertext
is written to. If ``None``, the ciphertext is returned.
Returns:
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "encrypt" not in self._next:
raise TypeError("Cipher object can only be used for decryption")
self._next = ("encrypt",)
return self._encrypt(plaintext, output)
def _encrypt(self, plaintext, output):
"""Encrypt without FSM checks"""
if output is None:
ciphertext = create_string_buffer(len(plaintext))
else:
ciphertext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(plaintext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = _raw_chacha20_lib.chacha20_encrypt(
self._state.get(),
c_uint8_ptr(plaintext),
c_uint8_ptr(ciphertext),
c_size_t(len(plaintext)))
if result:
raise ValueError("Error %d while encrypting with %s" % (result, self._name))
if output is None:
return get_raw_buffer(ciphertext)
else:
return None
def decrypt(self, ciphertext, output=None):
"""Decrypt a piece of data.
Args:
ciphertext(bytes/bytearray/memoryview): The data to decrypt, of any size.
Keyword Args:
output(bytes/bytearray/memoryview): The location where the plaintext
is written to. If ``None``, the plaintext is returned.
Returns:
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "decrypt" not in self._next:
raise TypeError("Cipher object can only be used for encryption")
self._next = ("decrypt",)
try:
return self._encrypt(ciphertext, output)
except ValueError as e:
raise ValueError(str(e).replace("enc", "dec"))
def seek(self, position):
"""Seek to a certain position in the key stream.
If you want to seek to a certain block,
use ``seek(block_number * 64)``.
Args:
position (integer):
The absolute position within the key stream, in bytes.
"""
block_number, offset = divmod(position, 64)
block_low = block_number & 0xFFFFFFFF
block_high = block_number >> 32
result = _raw_chacha20_lib.chacha20_seek(
self._state.get(),
c_ulong(block_high),
c_ulong(block_low),
offset
)
if result:
raise ValueError("Error %d while seeking with %s" % (result, self._name))
def _derive_Poly1305_key_pair(key, nonce):
"""Derive a tuple (r, s, nonce) for a Poly1305 MAC.
If nonce is ``None``, a new 12-byte nonce is generated.
"""
if len(key) != 32:
raise ValueError("Poly1305 with ChaCha20 requires a 32-byte key")
if nonce is None:
padded_nonce = nonce = get_random_bytes(12)
elif len(nonce) == 8:
# See RFC7538, 2.6: [...] ChaCha20 as specified here requires a 96-bit
# nonce. So if the provided nonce is only 64-bit, then the first 32
# bits of the nonce will be set to a constant number.
# This will usually be zero, but for protocols with multiple senders it may be
# different for each sender, but should be the same for all
# invocations of the function with the same key by a particular
# sender.
padded_nonce = b'\x00\x00\x00\x00' + nonce
elif len(nonce) == 12:
padded_nonce = nonce
else:
raise ValueError("Poly1305 with ChaCha20 requires an 8- or 12-byte nonce")
rs = new(key=key, nonce=padded_nonce).encrypt(b'\x00' * 32)
return rs[:16], rs[16:], nonce
def new(**kwargs):
"""Create a new ChaCha20 or XChaCha20 cipher
Keyword Args:
key (bytes/bytearray/memoryview): The secret key to use.
It must be 32 bytes long.
nonce (bytes/bytearray/memoryview): A mandatory value that
must never be reused for any other encryption
done with this key.
For ChaCha20, it must be 8 or 12 bytes long.
For XChaCha20, it must be 24 bytes long.
If not provided, 8 bytes will be randomly generated
(you can find them back in the ``nonce`` attribute).
:Return: a :class:`Crypto.Cipher.ChaCha20.ChaCha20Cipher` object
"""
try:
key = kwargs.pop("key")
except KeyError as e:
raise TypeError("Missing parameter %s" % e)
nonce = kwargs.pop("nonce", None)
if nonce is None:
nonce = get_random_bytes(8)
if len(key) != 32:
raise ValueError("ChaCha20/XChaCha20 key must be 32 bytes long")
if len(nonce) not in (8, 12, 24):
raise ValueError("Nonce must be 8/12 bytes(ChaCha20) or 24 bytes (XChaCha20)")
if kwargs:
raise TypeError("Unknown parameters: " + str(kwargs))
return ChaCha20Cipher(key, nonce)
# Size of a data block (in bytes)
block_size = 1
# Size of a key (in bytes)
key_size = 32

View File

@ -0,0 +1,25 @@
from typing import Union, overload, Optional
Buffer = bytes|bytearray|memoryview
def _HChaCha20(key: Buffer, nonce: Buffer) -> bytearray: ...
class ChaCha20Cipher:
block_size: int
nonce: bytes
def __init__(self, key: Buffer, nonce: Buffer) -> None: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
def seek(self, position: int) -> None: ...
def new(key: Buffer, nonce: Optional[Buffer] = ...) -> ChaCha20Cipher: ...
block_size: int
key_size: int

View File

@ -0,0 +1,334 @@
# ===================================================================
#
# Copyright (c) 2018, Helder Eijs <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
from binascii import unhexlify
from Crypto.Cipher import ChaCha20
from Crypto.Cipher.ChaCha20 import _HChaCha20
from Crypto.Hash import Poly1305, BLAKE2s
from Crypto.Random import get_random_bytes
from Crypto.Util.number import long_to_bytes
from Crypto.Util.py3compat import _copy_bytes, bord
from Crypto.Util._raw_api import is_buffer
def _enum(**enums):
return type('Enum', (), enums)
_CipherStatus = _enum(PROCESSING_AUTH_DATA=1,
PROCESSING_CIPHERTEXT=2,
PROCESSING_DONE=3)
class ChaCha20Poly1305Cipher(object):
"""ChaCha20-Poly1305 and XChaCha20-Poly1305 cipher object.
Do not create it directly. Use :py:func:`new` instead.
:var nonce: The nonce with length 8, 12 or 24 bytes
:vartype nonce: byte string
"""
def __init__(self, key, nonce):
"""Initialize a ChaCha20-Poly1305 AEAD cipher object
See also `new()` at the module level."""
self._next = ("update", "encrypt", "decrypt", "digest",
"verify")
self._authenticator = Poly1305.new(key=key, nonce=nonce, cipher=ChaCha20)
self._cipher = ChaCha20.new(key=key, nonce=nonce)
self._cipher.seek(64) # Block counter starts at 1
self._len_aad = 0
self._len_ct = 0
self._mac_tag = None
self._status = _CipherStatus.PROCESSING_AUTH_DATA
def update(self, data):
"""Protect the associated data.
Associated data (also known as *additional authenticated data* - AAD)
is the piece of the message that must stay in the clear, while
still allowing the receiver to verify its integrity.
An example is packet headers.
The associated data (possibly split into multiple segments) is
fed into :meth:`update` before any call to :meth:`decrypt` or :meth:`encrypt`.
If there is no associated data, :meth:`update` is not called.
:param bytes/bytearray/memoryview assoc_data:
A piece of associated data. There are no restrictions on its size.
"""
if "update" not in self._next:
raise TypeError("update() method cannot be called")
self._len_aad += len(data)
self._authenticator.update(data)
def _pad_aad(self):
assert(self._status == _CipherStatus.PROCESSING_AUTH_DATA)
if self._len_aad & 0x0F:
self._authenticator.update(b'\x00' * (16 - (self._len_aad & 0x0F)))
self._status = _CipherStatus.PROCESSING_CIPHERTEXT
def encrypt(self, plaintext, output=None):
"""Encrypt a piece of data.
Args:
plaintext(bytes/bytearray/memoryview): The data to encrypt, of any size.
Keyword Args:
output(bytes/bytearray/memoryview): The location where the ciphertext
is written to. If ``None``, the ciphertext is returned.
Returns:
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() method cannot be called")
if self._status == _CipherStatus.PROCESSING_AUTH_DATA:
self._pad_aad()
self._next = ("encrypt", "digest")
result = self._cipher.encrypt(plaintext, output=output)
self._len_ct += len(plaintext)
if output is None:
self._authenticator.update(result)
else:
self._authenticator.update(output)
return result
def decrypt(self, ciphertext, output=None):
"""Decrypt a piece of data.
Args:
ciphertext(bytes/bytearray/memoryview): The data to decrypt, of any size.
Keyword Args:
output(bytes/bytearray/memoryview): The location where the plaintext
is written to. If ``None``, the plaintext is returned.
Returns:
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() method cannot be called")
if self._status == _CipherStatus.PROCESSING_AUTH_DATA:
self._pad_aad()
self._next = ("decrypt", "verify")
self._len_ct += len(ciphertext)
self._authenticator.update(ciphertext)
return self._cipher.decrypt(ciphertext, output=output)
def _compute_mac(self):
"""Finalize the cipher (if not done already) and return the MAC."""
if self._mac_tag:
assert(self._status == _CipherStatus.PROCESSING_DONE)
return self._mac_tag
assert(self._status != _CipherStatus.PROCESSING_DONE)
if self._status == _CipherStatus.PROCESSING_AUTH_DATA:
self._pad_aad()
if self._len_ct & 0x0F:
self._authenticator.update(b'\x00' * (16 - (self._len_ct & 0x0F)))
self._status = _CipherStatus.PROCESSING_DONE
self._authenticator.update(long_to_bytes(self._len_aad, 8)[::-1])
self._authenticator.update(long_to_bytes(self._len_ct, 8)[::-1])
self._mac_tag = self._authenticator.digest()
return self._mac_tag
def digest(self):
"""Compute the *binary* authentication tag (MAC).
:Return: the MAC tag, as 16 ``bytes``.
"""
if "digest" not in self._next:
raise TypeError("digest() method cannot be called")
self._next = ("digest",)
return self._compute_mac()
def hexdigest(self):
"""Compute the *printable* authentication tag (MAC).
This method is like :meth:`digest`.
:Return: the MAC tag, as a hexadecimal string.
"""
return "".join(["%02x" % bord(x) for x in self.digest()])
def verify(self, received_mac_tag):
"""Validate the *binary* authentication tag (MAC).
The receiver invokes this method at the very end, to
check if the associated data (if any) and the decrypted
messages are valid.
:param bytes/bytearray/memoryview received_mac_tag:
This is the 16-byte *binary* MAC, as received from the sender.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
if "verify" not in self._next:
raise TypeError("verify() cannot be called"
" when encrypting a message")
self._next = ("verify",)
secret = get_random_bytes(16)
self._compute_mac()
mac1 = BLAKE2s.new(digest_bits=160, key=secret,
data=self._mac_tag)
mac2 = BLAKE2s.new(digest_bits=160, key=secret,
data=received_mac_tag)
if mac1.digest() != mac2.digest():
raise ValueError("MAC check failed")
def hexverify(self, hex_mac_tag):
"""Validate the *printable* authentication tag (MAC).
This method is like :meth:`verify`.
:param string hex_mac_tag:
This is the *printable* MAC.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
self.verify(unhexlify(hex_mac_tag))
def encrypt_and_digest(self, plaintext):
"""Perform :meth:`encrypt` and :meth:`digest` in one step.
:param plaintext: The data to encrypt, of any size.
:type plaintext: bytes/bytearray/memoryview
:return: a tuple with two ``bytes`` objects:
- the ciphertext, of equal length as the plaintext
- the 16-byte MAC tag
"""
return self.encrypt(plaintext), self.digest()
def decrypt_and_verify(self, ciphertext, received_mac_tag):
"""Perform :meth:`decrypt` and :meth:`verify` in one step.
:param ciphertext: The piece of data to decrypt.
:type ciphertext: bytes/bytearray/memoryview
:param bytes received_mac_tag:
This is the 16-byte *binary* MAC, as received from the sender.
:return: the decrypted data (as ``bytes``)
:raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
plaintext = self.decrypt(ciphertext)
self.verify(received_mac_tag)
return plaintext
def new(**kwargs):
"""Create a new ChaCha20-Poly1305 or XChaCha20-Poly1305 AEAD cipher.
:keyword key: The secret key to use. It must be 32 bytes long.
:type key: byte string
:keyword nonce:
A value that must never be reused for any other encryption
done with this key.
For ChaCha20-Poly1305, it must be 8 or 12 bytes long.
For XChaCha20-Poly1305, it must be 24 bytes long.
If not provided, 12 ``bytes`` will be generated randomly
(you can find them back in the ``nonce`` attribute).
:type nonce: bytes, bytearray, memoryview
:Return: a :class:`Crypto.Cipher.ChaCha20.ChaCha20Poly1305Cipher` object
"""
try:
key = kwargs.pop("key")
except KeyError as e:
raise TypeError("Missing parameter %s" % e)
if len(key) != 32:
raise ValueError("Key must be 32 bytes long")
nonce = kwargs.pop("nonce", None)
if nonce is None:
nonce = get_random_bytes(12)
if len(nonce) in (8, 12):
chacha20_poly1305_nonce = nonce
elif len(nonce) == 24:
key = _HChaCha20(key, nonce[:16])
chacha20_poly1305_nonce = b'\x00\x00\x00\x00' + nonce[16:]
else:
raise ValueError("Nonce must be 8, 12 or 24 bytes long")
if not is_buffer(nonce):
raise TypeError("nonce must be bytes, bytearray or memoryview")
if kwargs:
raise TypeError("Unknown parameters: " + str(kwargs))
cipher = ChaCha20Poly1305Cipher(key, chacha20_poly1305_nonce)
cipher.nonce = _copy_bytes(None, None, nonce)
return cipher
# Size of a key (in bytes)
key_size = 32

View File

@ -0,0 +1,28 @@
from typing import Union, Tuple, overload, Optional
Buffer = bytes|bytearray|memoryview
class ChaCha20Poly1305Cipher:
nonce: bytes
def __init__(self, key: Buffer, nonce: Buffer) -> None: ...
def update(self, data: Buffer) -> None: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def verify(self, received_mac_tag: Buffer) -> None: ...
def hexverify(self, received_mac_tag: str) -> None: ...
def encrypt_and_digest(self, plaintext: Buffer) -> Tuple[bytes, bytes]: ...
def decrypt_and_verify(self, ciphertext: Buffer, received_mac_tag: Buffer) -> bytes: ...
def new(key: Buffer, nonce: Optional[Buffer] = ...) -> ChaCha20Poly1305Cipher: ...
block_size: int
key_size: int

View File

@ -0,0 +1,158 @@
# -*- coding: utf-8 -*-
#
# Cipher/DES.py : DES
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
"""
Module's constants for the modes of operation supported with Single DES:
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
"""
import sys
from Crypto.Cipher import _create_cipher
from Crypto.Util.py3compat import byte_string
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
VoidPointer, SmartPointer,
c_size_t, c_uint8_ptr)
_raw_des_lib = load_pycryptodome_raw_lib(
"Crypto.Cipher._raw_des",
"""
int DES_start_operation(const uint8_t key[],
size_t key_len,
void **pResult);
int DES_encrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int DES_decrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int DES_stop_operation(void *state);
""")
def _create_base_cipher(dict_parameters):
"""This method instantiates and returns a handle to a low-level
base cipher. It will absorb named parameters in the process."""
try:
key = dict_parameters.pop("key")
except KeyError:
raise TypeError("Missing 'key' parameter")
if len(key) != key_size:
raise ValueError("Incorrect DES key length (%d bytes)" % len(key))
start_operation = _raw_des_lib.DES_start_operation
stop_operation = _raw_des_lib.DES_stop_operation
cipher = VoidPointer()
result = start_operation(c_uint8_ptr(key),
c_size_t(len(key)),
cipher.address_of())
if result:
raise ValueError("Error %X while instantiating the DES cipher"
% result)
return SmartPointer(cipher.get(), stop_operation)
def new(key, mode, *args, **kwargs):
"""Create a new DES cipher.
:param key:
The secret key to use in the symmetric cipher.
It must be 8 byte long. The parity bits will be ignored.
:type key: bytes/bytearray/memoryview
:param mode:
The chaining mode to use for encryption or decryption.
:type mode: One of the supported ``MODE_*`` constants
:Keyword Arguments:
* **iv** (*byte string*) --
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
and ``MODE_OPENPGP`` modes).
The initialization vector to use for encryption or decryption.
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
For ``MODE_OPENPGP`` mode only,
it must be 8 bytes long for encryption
and 10 bytes for decryption (in the latter case, it is
actually the *encrypted* IV which was prefixed to the ciphertext).
If not provided, a random byte string is generated (you must then
read its value with the :attr:`iv` attribute).
* **nonce** (*byte string*) --
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
A value that must never be reused for any other encryption done
with this key.
For ``MODE_EAX`` there are no
restrictions on its length (recommended: **16** bytes).
For ``MODE_CTR``, its length must be in the range **[0..7]**.
If not provided for ``MODE_EAX``, a random byte string is generated (you
can read it back via the ``nonce`` attribute).
* **segment_size** (*integer*) --
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
are segmented in. It must be a multiple of 8.
If not specified, it will be assumed to be 8.
* **mac_len** : (*integer*) --
(Only ``MODE_EAX``)
Length of the authentication tag, in bytes.
It must be no longer than 8 (default).
* **initial_value** : (*integer*) --
(Only ``MODE_CTR``). The initial value for the counter within
the counter block. By default it is **0**.
:Return: a DES object, of the applicable mode.
"""
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
MODE_ECB = 1
MODE_CBC = 2
MODE_CFB = 3
MODE_OFB = 5
MODE_CTR = 6
MODE_OPENPGP = 7
MODE_EAX = 9
# Size of a data block (in bytes)
block_size = 8
# Size of a key (in bytes)
key_size = 8

View File

@ -0,0 +1,35 @@
from typing import Union, Dict, Iterable, Optional
Buffer = bytes|bytearray|memoryview
from Crypto.Cipher._mode_ecb import EcbMode
from Crypto.Cipher._mode_cbc import CbcMode
from Crypto.Cipher._mode_cfb import CfbMode
from Crypto.Cipher._mode_ofb import OfbMode
from Crypto.Cipher._mode_ctr import CtrMode
from Crypto.Cipher._mode_openpgp import OpenPgpMode
from Crypto.Cipher._mode_eax import EaxMode
DESMode = int
MODE_ECB: DESMode
MODE_CBC: DESMode
MODE_CFB: DESMode
MODE_OFB: DESMode
MODE_CTR: DESMode
MODE_OPENPGP: DESMode
MODE_EAX: DESMode
def new(key: Buffer,
mode: DESMode,
iv : Optional[Buffer] = ...,
IV : Optional[Buffer] = ...,
nonce : Optional[Buffer] = ...,
segment_size : int = ...,
mac_len : int = ...,
initial_value : Union[int, Buffer] = ...,
counter : Dict = ...) -> \
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
block_size: int
key_size: int

View File

@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
#
# Cipher/DES3.py : DES3
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
"""
Module's constants for the modes of operation supported with Triple DES:
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
"""
import sys
from Crypto.Cipher import _create_cipher
from Crypto.Util.py3compat import byte_string, bchr, bord, bstr
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
VoidPointer, SmartPointer,
c_size_t)
_raw_des3_lib = load_pycryptodome_raw_lib(
"Crypto.Cipher._raw_des3",
"""
int DES3_start_operation(const uint8_t key[],
size_t key_len,
void **pResult);
int DES3_encrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int DES3_decrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int DES3_stop_operation(void *state);
""")
def adjust_key_parity(key_in):
"""Set the parity bits in a TDES key.
:param key_in: the TDES key whose bits need to be adjusted
:type key_in: byte string
:returns: a copy of ``key_in``, with the parity bits correctly set
:rtype: byte string
:raises ValueError: if the TDES key is not 16 or 24 bytes long
:raises ValueError: if the TDES key degenerates into Single DES
"""
def parity_byte(key_byte):
parity = 1
for i in range(1, 8):
parity ^= (key_byte >> i) & 1
return (key_byte & 0xFE) | parity
if len(key_in) not in key_size:
raise ValueError("Not a valid TDES key")
key_out = b"".join([ bchr(parity_byte(bord(x))) for x in key_in ])
if key_out[:8] == key_out[8:16] or key_out[-16:-8] == key_out[-8:]:
raise ValueError("Triple DES key degenerates to single DES")
return key_out
def _create_base_cipher(dict_parameters):
"""This method instantiates and returns a handle to a low-level base cipher.
It will absorb named parameters in the process."""
try:
key_in = dict_parameters.pop("key")
except KeyError:
raise TypeError("Missing 'key' parameter")
key = adjust_key_parity(bstr(key_in))
start_operation = _raw_des3_lib.DES3_start_operation
stop_operation = _raw_des3_lib.DES3_stop_operation
cipher = VoidPointer()
result = start_operation(key,
c_size_t(len(key)),
cipher.address_of())
if result:
raise ValueError("Error %X while instantiating the TDES cipher"
% result)
return SmartPointer(cipher.get(), stop_operation)
def new(key, mode, *args, **kwargs):
"""Create a new Triple DES cipher.
:param key:
The secret key to use in the symmetric cipher.
It must be 16 or 24 byte long. The parity bits will be ignored.
:type key: bytes/bytearray/memoryview
:param mode:
The chaining mode to use for encryption or decryption.
:type mode: One of the supported ``MODE_*`` constants
:Keyword Arguments:
* **iv** (*bytes*, *bytearray*, *memoryview*) --
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
and ``MODE_OPENPGP`` modes).
The initialization vector to use for encryption or decryption.
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
For ``MODE_OPENPGP`` mode only,
it must be 8 bytes long for encryption
and 10 bytes for decryption (in the latter case, it is
actually the *encrypted* IV which was prefixed to the ciphertext).
If not provided, a random byte string is generated (you must then
read its value with the :attr:`iv` attribute).
* **nonce** (*bytes*, *bytearray*, *memoryview*) --
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
A value that must never be reused for any other encryption done
with this key.
For ``MODE_EAX`` there are no
restrictions on its length (recommended: **16** bytes).
For ``MODE_CTR``, its length must be in the range **[0..7]**.
If not provided for ``MODE_EAX``, a random byte string is generated (you
can read it back via the ``nonce`` attribute).
* **segment_size** (*integer*) --
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
are segmented in. It must be a multiple of 8.
If not specified, it will be assumed to be 8.
* **mac_len** : (*integer*) --
(Only ``MODE_EAX``)
Length of the authentication tag, in bytes.
It must be no longer than 8 (default).
* **initial_value** : (*integer*) --
(Only ``MODE_CTR``). The initial value for the counter within
the counter block. By default it is **0**.
:Return: a Triple DES object, of the applicable mode.
"""
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
MODE_ECB = 1
MODE_CBC = 2
MODE_CFB = 3
MODE_OFB = 5
MODE_CTR = 6
MODE_OPENPGP = 7
MODE_EAX = 9
# Size of a data block (in bytes)
block_size = 8
# Size of a key (in bytes)
key_size = (16, 24)

View File

@ -0,0 +1,37 @@
from typing import Union, Dict, Tuple, Optional
Buffer = bytes|bytearray|memoryview
from Crypto.Cipher._mode_ecb import EcbMode
from Crypto.Cipher._mode_cbc import CbcMode
from Crypto.Cipher._mode_cfb import CfbMode
from Crypto.Cipher._mode_ofb import OfbMode
from Crypto.Cipher._mode_ctr import CtrMode
from Crypto.Cipher._mode_openpgp import OpenPgpMode
from Crypto.Cipher._mode_eax import EaxMode
def adjust_key_parity(key_in: bytes) -> bytes: ...
DES3Mode = int
MODE_ECB: DES3Mode
MODE_CBC: DES3Mode
MODE_CFB: DES3Mode
MODE_OFB: DES3Mode
MODE_CTR: DES3Mode
MODE_OPENPGP: DES3Mode
MODE_EAX: DES3Mode
def new(key: Buffer,
mode: DES3Mode,
iv : Optional[Buffer] = ...,
IV : Optional[Buffer] = ...,
nonce : Optional[Buffer] = ...,
segment_size : int = ...,
mac_len : int = ...,
initial_value : Union[int, Buffer] = ...,
counter : Dict = ...) -> \
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
block_size: int
key_size: Tuple[int, int]

View File

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
#
# Cipher/PKCS1_OAEP.py : PKCS#1 OAEP
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
from Crypto.Signature.pss import MGF1
import Crypto.Hash.SHA1
from Crypto.Util.py3compat import _copy_bytes
import Crypto.Util.number
from Crypto.Util.number import ceil_div, bytes_to_long, long_to_bytes
from Crypto.Util.strxor import strxor
from Crypto import Random
from ._pkcs1_oaep_decode import oaep_decode
class PKCS1OAEP_Cipher:
"""Cipher object for PKCS#1 v1.5 OAEP.
Do not create directly: use :func:`new` instead."""
def __init__(self, key, hashAlgo, mgfunc, label, randfunc):
"""Initialize this PKCS#1 OAEP cipher object.
:Parameters:
key : an RSA key object
If a private half is given, both encryption and decryption are possible.
If a public half is given, only encryption is possible.
hashAlgo : hash object
The hash function to use. This can be a module under `Crypto.Hash`
or an existing hash object created from any of such modules. If not specified,
`Crypto.Hash.SHA1` is used.
mgfunc : callable
A mask generation function that accepts two parameters: a string to
use as seed, and the lenth of the mask to generate, in bytes.
If not specified, the standard MGF1 consistent with ``hashAlgo`` is used (a safe choice).
label : bytes/bytearray/memoryview
A label to apply to this particular encryption. If not specified,
an empty string is used. Specifying a label does not improve
security.
randfunc : callable
A function that returns random bytes.
:attention: Modify the mask generation function only if you know what you are doing.
Sender and receiver must use the same one.
"""
self._key = key
if hashAlgo:
self._hashObj = hashAlgo
else:
self._hashObj = Crypto.Hash.SHA1
if mgfunc:
self._mgf = mgfunc
else:
self._mgf = lambda x, y: MGF1(x, y, self._hashObj)
self._label = _copy_bytes(None, None, label)
self._randfunc = randfunc
def can_encrypt(self):
"""Legacy function to check if you can call :meth:`encrypt`.
.. deprecated:: 3.0"""
return self._key.can_encrypt()
def can_decrypt(self):
"""Legacy function to check if you can call :meth:`decrypt`.
.. deprecated:: 3.0"""
return self._key.can_decrypt()
def encrypt(self, message):
"""Encrypt a message with PKCS#1 OAEP.
:param message:
The message to encrypt, also known as plaintext. It can be of
variable length, but not longer than the RSA modulus (in bytes)
minus 2, minus twice the hash output size.
For instance, if you use RSA 2048 and SHA-256, the longest message
you can encrypt is 190 byte long.
:type message: bytes/bytearray/memoryview
:returns: The ciphertext, as large as the RSA modulus.
:rtype: bytes
:raises ValueError:
if the message is too long.
"""
# See 7.1.1 in RFC3447
modBits = Crypto.Util.number.size(self._key.n)
k = ceil_div(modBits, 8) # Convert from bits to bytes
hLen = self._hashObj.digest_size
mLen = len(message)
# Step 1b
ps_len = k - mLen - 2 * hLen - 2
if ps_len < 0:
raise ValueError("Plaintext is too long.")
# Step 2a
lHash = self._hashObj.new(self._label).digest()
# Step 2b
ps = b'\x00' * ps_len
# Step 2c
db = lHash + ps + b'\x01' + _copy_bytes(None, None, message)
# Step 2d
ros = self._randfunc(hLen)
# Step 2e
dbMask = self._mgf(ros, k-hLen-1)
# Step 2f
maskedDB = strxor(db, dbMask)
# Step 2g
seedMask = self._mgf(maskedDB, hLen)
# Step 2h
maskedSeed = strxor(ros, seedMask)
# Step 2i
em = b'\x00' + maskedSeed + maskedDB
# Step 3a (OS2IP)
em_int = bytes_to_long(em)
# Step 3b (RSAEP)
m_int = self._key._encrypt(em_int)
# Step 3c (I2OSP)
c = long_to_bytes(m_int, k)
return c
def decrypt(self, ciphertext):
"""Decrypt a message with PKCS#1 OAEP.
:param ciphertext: The encrypted message.
:type ciphertext: bytes/bytearray/memoryview
:returns: The original message (plaintext).
:rtype: bytes
:raises ValueError:
if the ciphertext has the wrong length, or if decryption
fails the integrity check (in which case, the decryption
key is probably wrong).
:raises TypeError:
if the RSA key has no private half (i.e. you are trying
to decrypt using a public key).
"""
# See 7.1.2 in RFC3447
modBits = Crypto.Util.number.size(self._key.n)
k = ceil_div(modBits, 8) # Convert from bits to bytes
hLen = self._hashObj.digest_size
# Step 1b and 1c
if len(ciphertext) != k or k < hLen+2:
raise ValueError("Ciphertext with incorrect length.")
# Step 2a (O2SIP)
ct_int = bytes_to_long(ciphertext)
# Step 2b (RSADP) and step 2c (I2OSP)
em = self._key._decrypt_to_bytes(ct_int)
# Step 3a
lHash = self._hashObj.new(self._label).digest()
# y must be 0, but we MUST NOT check it here in order not to
# allow attacks like Manger's (http://dl.acm.org/citation.cfm?id=704143)
maskedSeed = em[1:hLen+1]
maskedDB = em[hLen+1:]
# Step 3c
seedMask = self._mgf(maskedDB, hLen)
# Step 3d
seed = strxor(maskedSeed, seedMask)
# Step 3e
dbMask = self._mgf(seed, k-hLen-1)
# Step 3f
db = strxor(maskedDB, dbMask)
# Step 3b + 3g
res = oaep_decode(em, lHash, db)
if res <= 0:
raise ValueError("Incorrect decryption.")
# Step 4
return db[res:]
def new(key, hashAlgo=None, mgfunc=None, label=b'', randfunc=None):
"""Return a cipher object :class:`PKCS1OAEP_Cipher`
that can be used to perform PKCS#1 OAEP encryption or decryption.
:param key:
The key object to use to encrypt or decrypt the message.
Decryption is only possible with a private RSA key.
:type key: RSA key object
:param hashAlgo:
The hash function to use. This can be a module under `Crypto.Hash`
or an existing hash object created from any of such modules.
If not specified, `Crypto.Hash.SHA1` is used.
:type hashAlgo: hash object
:param mgfunc:
A mask generation function that accepts two parameters: a string to
use as seed, and the lenth of the mask to generate, in bytes.
If not specified, the standard MGF1 consistent with ``hashAlgo`` is used (a safe choice).
:type mgfunc: callable
:param label:
A label to apply to this particular encryption. If not specified,
an empty string is used. Specifying a label does not improve
security.
:type label: bytes/bytearray/memoryview
:param randfunc:
A function that returns random bytes.
The default is `Random.get_random_bytes`.
:type randfunc: callable
"""
if randfunc is None:
randfunc = Random.get_random_bytes
return PKCS1OAEP_Cipher(key, hashAlgo, mgfunc, label, randfunc)

View File

@ -0,0 +1,35 @@
from typing import Optional, Union, Callable, Any, overload
from typing_extensions import Protocol
from Crypto.PublicKey.RSA import RsaKey
class HashLikeClass(Protocol):
digest_size : int
def new(self, data: Optional[bytes] = ...) -> Any: ...
class HashLikeModule(Protocol):
digest_size : int
@staticmethod
def new(data: Optional[bytes] = ...) -> Any: ...
HashLike = Union[HashLikeClass, HashLikeModule]
Buffer = Union[bytes, bytearray, memoryview]
class PKCS1OAEP_Cipher:
def __init__(self,
key: RsaKey,
hashAlgo: HashLike,
mgfunc: Callable[[bytes, int], bytes],
label: Buffer,
randfunc: Callable[[int], bytes]) -> None: ...
def can_encrypt(self) -> bool: ...
def can_decrypt(self) -> bool: ...
def encrypt(self, message: Buffer) -> bytes: ...
def decrypt(self, ciphertext: Buffer) -> bytes: ...
def new(key: RsaKey,
hashAlgo: Optional[HashLike] = ...,
mgfunc: Optional[Callable[[bytes, int], bytes]] = ...,
label: Optional[Buffer] = ...,
randfunc: Optional[Callable[[int], bytes]] = ...) -> PKCS1OAEP_Cipher: ...

View File

@ -0,0 +1,189 @@
# -*- coding: utf-8 -*-
#
# Cipher/PKCS1-v1_5.py : PKCS#1 v1.5
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
__all__ = ['new', 'PKCS115_Cipher']
from Crypto import Random
from Crypto.Util.number import bytes_to_long, long_to_bytes
from Crypto.Util.py3compat import bord, is_bytes, _copy_bytes
from ._pkcs1_oaep_decode import pkcs1_decode
class PKCS115_Cipher:
"""This cipher can perform PKCS#1 v1.5 RSA encryption or decryption.
Do not instantiate directly. Use :func:`Crypto.Cipher.PKCS1_v1_5.new` instead."""
def __init__(self, key, randfunc):
"""Initialize this PKCS#1 v1.5 cipher object.
:Parameters:
key : an RSA key object
If a private half is given, both encryption and decryption are possible.
If a public half is given, only encryption is possible.
randfunc : callable
Function that returns random bytes.
"""
self._key = key
self._randfunc = randfunc
def can_encrypt(self):
"""Return True if this cipher object can be used for encryption."""
return self._key.can_encrypt()
def can_decrypt(self):
"""Return True if this cipher object can be used for decryption."""
return self._key.can_decrypt()
def encrypt(self, message):
"""Produce the PKCS#1 v1.5 encryption of a message.
This function is named ``RSAES-PKCS1-V1_5-ENCRYPT``, and it is specified in
`section 7.2.1 of RFC8017
<https://tools.ietf.org/html/rfc8017#page-28>`_.
:param message:
The message to encrypt, also known as plaintext. It can be of
variable length, but not longer than the RSA modulus (in bytes) minus 11.
:type message: bytes/bytearray/memoryview
:Returns: A byte string, the ciphertext in which the message is encrypted.
It is as long as the RSA modulus (in bytes).
:Raises ValueError:
If the RSA key length is not sufficiently long to deal with the given
message.
"""
# See 7.2.1 in RFC8017
k = self._key.size_in_bytes()
mLen = len(message)
# Step 1
if mLen > k - 11:
raise ValueError("Plaintext is too long.")
# Step 2a
ps = []
while len(ps) != k - mLen - 3:
new_byte = self._randfunc(1)
if bord(new_byte[0]) == 0x00:
continue
ps.append(new_byte)
ps = b"".join(ps)
# Step 2b
em = b'\x00\x02' + ps + b'\x00' + _copy_bytes(None, None, message)
# Step 3a (OS2IP)
em_int = bytes_to_long(em)
# Step 3b (RSAEP)
m_int = self._key._encrypt(em_int)
# Step 3c (I2OSP)
c = long_to_bytes(m_int, k)
return c
def decrypt(self, ciphertext, sentinel, expected_pt_len=0):
r"""Decrypt a PKCS#1 v1.5 ciphertext.
This is the function ``RSAES-PKCS1-V1_5-DECRYPT`` specified in
`section 7.2.2 of RFC8017
<https://tools.ietf.org/html/rfc8017#page-29>`_.
Args:
ciphertext (bytes/bytearray/memoryview):
The ciphertext that contains the message to recover.
sentinel (any type):
The object to return whenever an error is detected.
expected_pt_len (integer):
The length the plaintext is known to have, or 0 if unknown.
Returns (byte string):
It is either the original message or the ``sentinel`` (in case of an error).
.. warning::
PKCS#1 v1.5 decryption is intrinsically vulnerable to timing
attacks (see `Bleichenbacher's`__ attack).
**Use PKCS#1 OAEP instead**.
This implementation attempts to mitigate the risk
with some constant-time constructs.
However, they are not sufficient by themselves: the type of protocol you
implement and the way you handle errors make a big difference.
Specifically, you should make it very hard for the (malicious)
party that submitted the ciphertext to quickly understand if decryption
succeeded or not.
To this end, it is recommended that your protocol only encrypts
plaintexts of fixed length (``expected_pt_len``),
that ``sentinel`` is a random byte string of the same length,
and that processing continues for as long
as possible even if ``sentinel`` is returned (i.e. in case of
incorrect decryption).
.. __: https://dx.doi.org/10.1007/BFb0055716
"""
# See 7.2.2 in RFC8017
k = self._key.size_in_bytes()
# Step 1
if len(ciphertext) != k:
raise ValueError("Ciphertext with incorrect length (not %d bytes)" % k)
# Step 2a (O2SIP)
ct_int = bytes_to_long(ciphertext)
# Step 2b (RSADP) and Step 2c (I2OSP)
em = self._key._decrypt_to_bytes(ct_int)
# Step 3 (not constant time when the sentinel is not a byte string)
output = bytes(bytearray(k))
if not is_bytes(sentinel) or len(sentinel) > k:
size = pkcs1_decode(em, b'', expected_pt_len, output)
if size < 0:
return sentinel
else:
return output[size:]
# Step 3 (somewhat constant time)
size = pkcs1_decode(em, sentinel, expected_pt_len, output)
return output[size:]
def new(key, randfunc=None):
"""Create a cipher for performing PKCS#1 v1.5 encryption or decryption.
:param key:
The key to use to encrypt or decrypt the message. This is a `Crypto.PublicKey.RSA` object.
Decryption is only possible if *key* is a private RSA key.
:type key: RSA key object
:param randfunc:
Function that return random bytes.
The default is :func:`Crypto.Random.get_random_bytes`.
:type randfunc: callable
:returns: A cipher object `PKCS115_Cipher`.
"""
if randfunc is None:
randfunc = Random.get_random_bytes
return PKCS115_Cipher(key, randfunc)

View File

@ -0,0 +1,20 @@
from typing import Callable, Union, Any, Optional, TypeVar
from Crypto.PublicKey.RSA import RsaKey
Buffer = Union[bytes, bytearray, memoryview]
T = TypeVar('T')
class PKCS115_Cipher:
def __init__(self,
key: RsaKey,
randfunc: Callable[[int], bytes]) -> None: ...
def can_encrypt(self) -> bool: ...
def can_decrypt(self) -> bool: ...
def encrypt(self, message: Buffer) -> bytes: ...
def decrypt(self, ciphertext: Buffer,
sentinel: T,
expected_pt_len: Optional[int] = ...) -> Union[bytes, T]: ...
def new(key: RsaKey,
randfunc: Optional[Callable[[int], bytes]] = ...) -> PKCS115_Cipher: ...

View File

@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
#
# Cipher/Salsa20.py : Salsa20 stream cipher (http://cr.yp.to/snuffle.html)
#
# Contributed by Fabrizio Tarizzo <fabrizio@fabriziotarizzo.org>.
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
from Crypto.Util.py3compat import _copy_bytes
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
create_string_buffer,
get_raw_buffer, VoidPointer,
SmartPointer, c_size_t,
c_uint8_ptr, is_writeable_buffer)
from Crypto.Random import get_random_bytes
_raw_salsa20_lib = load_pycryptodome_raw_lib("Crypto.Cipher._Salsa20",
"""
int Salsa20_stream_init(uint8_t *key, size_t keylen,
uint8_t *nonce, size_t nonce_len,
void **pSalsaState);
int Salsa20_stream_destroy(void *salsaState);
int Salsa20_stream_encrypt(void *salsaState,
const uint8_t in[],
uint8_t out[], size_t len);
""")
class Salsa20Cipher:
"""Salsa20 cipher object. Do not create it directly. Use :py:func:`new`
instead.
:var nonce: The nonce with length 8
:vartype nonce: byte string
"""
def __init__(self, key, nonce):
"""Initialize a Salsa20 cipher object
See also `new()` at the module level."""
if len(key) not in key_size:
raise ValueError("Incorrect key length for Salsa20 (%d bytes)" % len(key))
if len(nonce) != 8:
raise ValueError("Incorrect nonce length for Salsa20 (%d bytes)" %
len(nonce))
self.nonce = _copy_bytes(None, None, nonce)
self._state = VoidPointer()
result = _raw_salsa20_lib.Salsa20_stream_init(
c_uint8_ptr(key),
c_size_t(len(key)),
c_uint8_ptr(nonce),
c_size_t(len(nonce)),
self._state.address_of())
if result:
raise ValueError("Error %d instantiating a Salsa20 cipher")
self._state = SmartPointer(self._state.get(),
_raw_salsa20_lib.Salsa20_stream_destroy)
self.block_size = 1
self.key_size = len(key)
def encrypt(self, plaintext, output=None):
"""Encrypt a piece of data.
Args:
plaintext(bytes/bytearray/memoryview): The data to encrypt, of any size.
Keyword Args:
output(bytes/bytearray/memoryview): The location where the ciphertext
is written to. If ``None``, the ciphertext is returned.
Returns:
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
Otherwise, ``None``.
"""
if output is None:
ciphertext = create_string_buffer(len(plaintext))
else:
ciphertext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(plaintext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = _raw_salsa20_lib.Salsa20_stream_encrypt(
self._state.get(),
c_uint8_ptr(plaintext),
c_uint8_ptr(ciphertext),
c_size_t(len(plaintext)))
if result:
raise ValueError("Error %d while encrypting with Salsa20" % result)
if output is None:
return get_raw_buffer(ciphertext)
else:
return None
def decrypt(self, ciphertext, output=None):
"""Decrypt a piece of data.
Args:
ciphertext(bytes/bytearray/memoryview): The data to decrypt, of any size.
Keyword Args:
output(bytes/bytearray/memoryview): The location where the plaintext
is written to. If ``None``, the plaintext is returned.
Returns:
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
Otherwise, ``None``.
"""
try:
return self.encrypt(ciphertext, output=output)
except ValueError as e:
raise ValueError(str(e).replace("enc", "dec"))
def new(key, nonce=None):
"""Create a new Salsa20 cipher
:keyword key: The secret key to use. It must be 16 or 32 bytes long.
:type key: bytes/bytearray/memoryview
:keyword nonce:
A value that must never be reused for any other encryption
done with this key. It must be 8 bytes long.
If not provided, a random byte string will be generated (you can read
it back via the ``nonce`` attribute of the returned object).
:type nonce: bytes/bytearray/memoryview
:Return: a :class:`Crypto.Cipher.Salsa20.Salsa20Cipher` object
"""
if nonce is None:
nonce = get_random_bytes(8)
return Salsa20Cipher(key, nonce)
# Size of a data block (in bytes)
block_size = 1
# Size of a key (in bytes)
key_size = (16, 32)

View File

@ -0,0 +1,26 @@
from typing import Union, Tuple, Optional, overload, Optional
Buffer = bytes|bytearray|memoryview
class Salsa20Cipher:
nonce: bytes
block_size: int
key_size: int
def __init__(self,
key: Buffer,
nonce: Buffer) -> None: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
def new(key: Buffer, nonce: Optional[Buffer] = ...) -> Salsa20Cipher: ...
block_size: int
key_size: Tuple[int, int]

Binary file not shown.

View File

@ -0,0 +1,131 @@
# ===================================================================
#
# Copyright (c) 2019, Legrandin <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
import sys
from Crypto.Cipher import _create_cipher
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
VoidPointer, SmartPointer, c_size_t,
c_uint8_ptr, c_uint)
_raw_blowfish_lib = load_pycryptodome_raw_lib(
"Crypto.Cipher._raw_eksblowfish",
"""
int EKSBlowfish_start_operation(const uint8_t key[],
size_t key_len,
const uint8_t salt[16],
size_t salt_len,
unsigned cost,
unsigned invert,
void **pResult);
int EKSBlowfish_encrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int EKSBlowfish_decrypt(const void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int EKSBlowfish_stop_operation(void *state);
"""
)
def _create_base_cipher(dict_parameters):
"""This method instantiates and returns a smart pointer to
a low-level base cipher. It will absorb named parameters in
the process."""
try:
key = dict_parameters.pop("key")
salt = dict_parameters.pop("salt")
cost = dict_parameters.pop("cost")
except KeyError as e:
raise TypeError("Missing EKSBlowfish parameter: " + str(e))
invert = dict_parameters.pop("invert", True)
if len(key) not in key_size:
raise ValueError("Incorrect EKSBlowfish key length (%d bytes)" % len(key))
start_operation = _raw_blowfish_lib.EKSBlowfish_start_operation
stop_operation = _raw_blowfish_lib.EKSBlowfish_stop_operation
void_p = VoidPointer()
result = start_operation(c_uint8_ptr(key),
c_size_t(len(key)),
c_uint8_ptr(salt),
c_size_t(len(salt)),
c_uint(cost),
c_uint(int(invert)),
void_p.address_of())
if result:
raise ValueError("Error %X while instantiating the EKSBlowfish cipher"
% result)
return SmartPointer(void_p.get(), stop_operation)
def new(key, mode, salt, cost, invert):
"""Create a new EKSBlowfish cipher
Args:
key (bytes, bytearray, memoryview):
The secret key to use in the symmetric cipher.
Its length can vary from 0 to 72 bytes.
mode (one of the supported ``MODE_*`` constants):
The chaining mode to use for encryption or decryption.
salt (bytes, bytearray, memoryview):
The salt that bcrypt uses to thwart rainbow table attacks
cost (integer):
The complexity factor in bcrypt
invert (bool):
If ``False``, in the inner loop use ``ExpandKey`` first over the salt
and then over the key, as defined in
the `original bcrypt specification <https://www.usenix.org/legacy/events/usenix99/provos/provos_html/node4.html>`_.
If ``True``, reverse the order, as in the first implementation of
`bcrypt` in OpenBSD.
:Return: an EKSBlowfish object
"""
kwargs = { 'salt':salt, 'cost':cost, 'invert':invert }
return _create_cipher(sys.modules[__name__], key, mode, **kwargs)
MODE_ECB = 1
# Size of a data block (in bytes)
block_size = 8
# Size of a key (in bytes)
key_size = range(0, 72 + 1)

View File

@ -0,0 +1,15 @@
from typing import Union, Iterable
from Crypto.Cipher._mode_ecb import EcbMode
MODE_ECB: int
Buffer = Union[bytes, bytearray, memoryview]
def new(key: Buffer,
mode: int,
salt: Buffer,
cost: int) -> EcbMode: ...
block_size: int
key_size: Iterable[int]

Binary file not shown.

View File

@ -0,0 +1,91 @@
#
# A block cipher is instantiated as a combination of:
# 1. A base cipher (such as AES)
# 2. A mode of operation (such as CBC)
#
# Both items are implemented as C modules.
#
# The API of #1 is (replace "AES" with the name of the actual cipher):
# - AES_start_operaion(key) --> base_cipher_state
# - AES_encrypt(base_cipher_state, in, out, length)
# - AES_decrypt(base_cipher_state, in, out, length)
# - AES_stop_operation(base_cipher_state)
#
# Where base_cipher_state is AES_State, a struct with BlockBase (set of
# pointers to encrypt/decrypt/stop) followed by cipher-specific data.
#
# The API of #2 is (replace "CBC" with the name of the actual mode):
# - CBC_start_operation(base_cipher_state) --> mode_state
# - CBC_encrypt(mode_state, in, out, length)
# - CBC_decrypt(mode_state, in, out, length)
# - CBC_stop_operation(mode_state)
#
# where mode_state is a a pointer to base_cipher_state plus mode-specific data.
def _create_cipher(factory, key, mode, *args, **kwargs):
kwargs["key"] = key
if args:
if mode in (8, 9, 10, 11, 12):
if len(args) > 1:
raise TypeError("Too many arguments for this mode")
kwargs["nonce"] = args[0]
elif mode in (2, 3, 5, 7):
if len(args) > 1:
raise TypeError("Too many arguments for this mode")
kwargs["IV"] = args[0]
elif mode == 6:
if len(args) > 0:
raise TypeError("Too many arguments for this mode")
elif mode == 1:
raise TypeError("IV is not meaningful for the ECB mode")
res = None
extra_modes = kwargs.pop("add_aes_modes", False)
if mode == 1:
from Crypto.Cipher._mode_ecb import _create_ecb_cipher
res = _create_ecb_cipher(factory, **kwargs)
elif mode == 2:
from Crypto.Cipher._mode_cbc import _create_cbc_cipher
res = _create_cbc_cipher(factory, **kwargs)
elif mode == 3:
from Crypto.Cipher._mode_cfb import _create_cfb_cipher
res = _create_cfb_cipher(factory, **kwargs)
elif mode == 5:
from Crypto.Cipher._mode_ofb import _create_ofb_cipher
res = _create_ofb_cipher(factory, **kwargs)
elif mode == 6:
from Crypto.Cipher._mode_ctr import _create_ctr_cipher
res = _create_ctr_cipher(factory, **kwargs)
elif mode == 7:
from Crypto.Cipher._mode_openpgp import _create_openpgp_cipher
res = _create_openpgp_cipher(factory, **kwargs)
elif mode == 9:
from Crypto.Cipher._mode_eax import _create_eax_cipher
res = _create_eax_cipher(factory, **kwargs)
elif extra_modes:
if mode == 8:
from Crypto.Cipher._mode_ccm import _create_ccm_cipher
res = _create_ccm_cipher(factory, **kwargs)
elif mode == 10:
from Crypto.Cipher._mode_siv import _create_siv_cipher
res = _create_siv_cipher(factory, **kwargs)
elif mode == 11:
from Crypto.Cipher._mode_gcm import _create_gcm_cipher
res = _create_gcm_cipher(factory, **kwargs)
elif mode == 12:
from Crypto.Cipher._mode_ocb import _create_ocb_cipher
res = _create_ocb_cipher(factory, **kwargs)
elif mode == 13:
from Crypto.Cipher._mode_kw import _create_kw_cipher
res = _create_kw_cipher(factory, **kwargs)
elif mode == 14:
from Crypto.Cipher._mode_kwp import _create_kwp_cipher
res = _create_kwp_cipher(factory, **kwargs)
if res is None:
raise ValueError("Mode not supported")
return res

Binary file not shown.

View File

@ -0,0 +1,293 @@
# ===================================================================
#
# Copyright (c) 2014, Legrandin <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
"""
Ciphertext Block Chaining (CBC) mode.
"""
__all__ = ['CbcMode']
from Crypto.Util.py3compat import _copy_bytes
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
create_string_buffer, get_raw_buffer,
SmartPointer, c_size_t, c_uint8_ptr,
is_writeable_buffer)
from Crypto.Random import get_random_bytes
raw_cbc_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_cbc", """
int CBC_start_operation(void *cipher,
const uint8_t iv[],
size_t iv_len,
void **pResult);
int CBC_encrypt(void *cbcState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int CBC_decrypt(void *cbcState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int CBC_stop_operation(void *state);
"""
)
class CbcMode(object):
"""*Cipher-Block Chaining (CBC)*.
Each of the ciphertext blocks depends on the current
and all previous plaintext blocks.
An Initialization Vector (*IV*) is required.
See `NIST SP800-38A`_ , Section 6.2 .
.. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
:undocumented: __init__
"""
def __init__(self, block_cipher, iv):
"""Create a new block cipher, configured in CBC mode.
:Parameters:
block_cipher : C pointer
A smart pointer to the low-level block cipher instance.
iv : bytes/bytearray/memoryview
The initialization vector to use for encryption or decryption.
It is as long as the cipher block.
**The IV must be unpredictable**. Ideally it is picked randomly.
Reusing the *IV* for encryptions performed with the same key
compromises confidentiality.
"""
self._state = VoidPointer()
result = raw_cbc_lib.CBC_start_operation(block_cipher.get(),
c_uint8_ptr(iv),
c_size_t(len(iv)),
self._state.address_of())
if result:
raise ValueError("Error %d while instantiating the CBC mode"
% result)
# Ensure that object disposal of this Python object will (eventually)
# free the memory allocated by the raw library for the cipher mode
self._state = SmartPointer(self._state.get(),
raw_cbc_lib.CBC_stop_operation)
# Memory allocated for the underlying block cipher is now owed
# by the cipher mode
block_cipher.release()
self.block_size = len(iv)
"""The block size of the underlying cipher, in bytes."""
self.iv = _copy_bytes(None, None, iv)
"""The Initialization Vector originally used to create the object.
The value does not change."""
self.IV = self.iv
"""Alias for `iv`"""
self._next = ["encrypt", "decrypt"]
def encrypt(self, plaintext, output=None):
"""Encrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have encrypted a message
you cannot encrypt (or decrypt) another message using the same
object.
The data to encrypt can be broken up in two or
more pieces and `encrypt` can be called multiple times.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is equivalent to:
>>> c.encrypt(a+b)
That also means that you cannot reuse an object for encrypting
or decrypting other data with the same key.
This function does not add any padding to the plaintext.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
Its lenght must be multiple of the cipher block size.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() cannot be called after decrypt()")
self._next = ["encrypt"]
if output is None:
ciphertext = create_string_buffer(len(plaintext))
else:
ciphertext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(plaintext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_cbc_lib.CBC_encrypt(self._state.get(),
c_uint8_ptr(plaintext),
c_uint8_ptr(ciphertext),
c_size_t(len(plaintext)))
if result:
if result == 3:
raise ValueError("Data must be padded to %d byte boundary in CBC mode" % self.block_size)
raise ValueError("Error %d while encrypting in CBC mode" % result)
if output is None:
return get_raw_buffer(ciphertext)
else:
return None
def decrypt(self, ciphertext, output=None):
"""Decrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have decrypted a message
you cannot decrypt (or encrypt) another message with the same
object.
The data to decrypt can be broken up in two or
more pieces and `decrypt` can be called multiple times.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is equivalent to:
>>> c.decrypt(a+b)
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
Its length must be multiple of the cipher block size.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return:
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() cannot be called after encrypt()")
self._next = ["decrypt"]
if output is None:
plaintext = create_string_buffer(len(ciphertext))
else:
plaintext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(ciphertext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_cbc_lib.CBC_decrypt(self._state.get(),
c_uint8_ptr(ciphertext),
c_uint8_ptr(plaintext),
c_size_t(len(ciphertext)))
if result:
if result == 3:
raise ValueError("Data must be padded to %d byte boundary in CBC mode" % self.block_size)
raise ValueError("Error %d while decrypting in CBC mode" % result)
if output is None:
return get_raw_buffer(plaintext)
else:
return None
def _create_cbc_cipher(factory, **kwargs):
"""Instantiate a cipher object that performs CBC encryption/decryption.
:Parameters:
factory : module
The underlying block cipher, a module from ``Crypto.Cipher``.
:Keywords:
iv : bytes/bytearray/memoryview
The IV to use for CBC.
IV : bytes/bytearray/memoryview
Alias for ``iv``.
Any other keyword will be passed to the underlying block cipher.
See the relevant documentation for details (at least ``key`` will need
to be present).
"""
cipher_state = factory._create_base_cipher(kwargs)
iv = kwargs.pop("IV", None)
IV = kwargs.pop("iv", None)
if (None, None) == (iv, IV):
iv = get_random_bytes(factory.block_size)
if iv is not None:
if IV is not None:
raise TypeError("You must either use 'iv' or 'IV', not both")
else:
iv = IV
if len(iv) != factory.block_size:
raise ValueError("Incorrect IV length (it must be %d bytes long)" %
factory.block_size)
if kwargs:
raise TypeError("Unknown parameters for CBC: %s" % str(kwargs))
return CbcMode(cipher_state, iv)

View File

@ -0,0 +1,25 @@
from typing import Union, overload
from Crypto.Util._raw_api import SmartPointer
Buffer = Union[bytes, bytearray, memoryview]
__all__ = ['CbcMode']
class CbcMode(object):
block_size: int
iv: Buffer
IV: Buffer
def __init__(self,
block_cipher: SmartPointer,
iv: Buffer) -> None: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...

View File

@ -0,0 +1,671 @@
# ===================================================================
#
# Copyright (c) 2014, Legrandin <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
"""
Counter with CBC-MAC (CCM) mode.
"""
__all__ = ['CcmMode']
import struct
from binascii import unhexlify
from Crypto.Util.py3compat import (byte_string, bord,
_copy_bytes)
from Crypto.Util._raw_api import is_writeable_buffer
from Crypto.Util.strxor import strxor
from Crypto.Util.number import long_to_bytes
from Crypto.Hash import BLAKE2s
from Crypto.Random import get_random_bytes
def enum(**enums):
return type('Enum', (), enums)
MacStatus = enum(NOT_STARTED=0, PROCESSING_AUTH_DATA=1, PROCESSING_PLAINTEXT=2)
class CCMMessageTooLongError(ValueError):
pass
class CcmMode(object):
"""Counter with CBC-MAC (CCM).
This is an Authenticated Encryption with Associated Data (`AEAD`_) mode.
It provides both confidentiality and authenticity.
The header of the message may be left in the clear, if needed, and it will
still be subject to authentication. The decryption step tells the receiver
if the message comes from a source that really knowns the secret key.
Additionally, decryption detects if any part of the message - including the
header - has been modified or corrupted.
This mode requires a nonce. The nonce shall never repeat for two
different messages encrypted with the same key, but it does not need
to be random.
Note that there is a trade-off between the size of the nonce and the
maximum size of a single message you can encrypt.
It is important to use a large nonce if the key is reused across several
messages and the nonce is chosen randomly.
It is acceptable to us a short nonce if the key is only used a few times or
if the nonce is taken from a counter.
The following table shows the trade-off when the nonce is chosen at
random. The column on the left shows how many messages it takes
for the keystream to repeat **on average**. In practice, you will want to
stop using the key way before that.
+--------------------+---------------+-------------------+
| Avg. # of messages | nonce | Max. message |
| before keystream | size | size |
| repeats | (bytes) | (bytes) |
+====================+===============+===================+
| 2^52 | 13 | 64K |
+--------------------+---------------+-------------------+
| 2^48 | 12 | 16M |
+--------------------+---------------+-------------------+
| 2^44 | 11 | 4G |
+--------------------+---------------+-------------------+
| 2^40 | 10 | 1T |
+--------------------+---------------+-------------------+
| 2^36 | 9 | 64P |
+--------------------+---------------+-------------------+
| 2^32 | 8 | 16E |
+--------------------+---------------+-------------------+
This mode is only available for ciphers that operate on 128 bits blocks
(e.g. AES but not TDES).
See `NIST SP800-38C`_ or RFC3610_.
.. _`NIST SP800-38C`: http://csrc.nist.gov/publications/nistpubs/800-38C/SP800-38C.pdf
.. _RFC3610: https://tools.ietf.org/html/rfc3610
.. _AEAD: http://blog.cryptographyengineering.com/2012/05/how-to-choose-authenticated-encryption.html
:undocumented: __init__
"""
def __init__(self, factory, key, nonce, mac_len, msg_len, assoc_len,
cipher_params):
self.block_size = factory.block_size
"""The block size of the underlying cipher, in bytes."""
self.nonce = _copy_bytes(None, None, nonce)
"""The nonce used for this cipher instance"""
self._factory = factory
self._key = _copy_bytes(None, None, key)
self._mac_len = mac_len
self._msg_len = msg_len
self._assoc_len = assoc_len
self._cipher_params = cipher_params
self._mac_tag = None # Cache for MAC tag
if self.block_size != 16:
raise ValueError("CCM mode is only available for ciphers"
" that operate on 128 bits blocks")
# MAC tag length (Tlen)
if mac_len not in (4, 6, 8, 10, 12, 14, 16):
raise ValueError("Parameter 'mac_len' must be even"
" and in the range 4..16 (not %d)" % mac_len)
# Nonce value
if not (7 <= len(nonce) <= 13):
raise ValueError("Length of parameter 'nonce' must be"
" in the range 7..13 bytes")
# Message length (if known already)
q = 15 - len(nonce) # length of Q, the encoded message length
if msg_len and len(long_to_bytes(msg_len)) > q:
raise CCMMessageTooLongError("Message too long for a %u-byte nonce" % len(nonce))
# Create MAC object (the tag will be the last block
# bytes worth of ciphertext)
self._mac = self._factory.new(key,
factory.MODE_CBC,
iv=b'\x00' * 16,
**cipher_params)
self._mac_status = MacStatus.NOT_STARTED
self._t = None
# Allowed transitions after initialization
self._next = ["update", "encrypt", "decrypt",
"digest", "verify"]
# Cumulative lengths
self._cumul_assoc_len = 0
self._cumul_msg_len = 0
# Cache for unaligned associated data/plaintext.
# This is a list with byte strings, but when the MAC starts,
# it will become a binary string no longer than the block size.
self._cache = []
# Start CTR cipher, by formatting the counter (A.3)
self._cipher = self._factory.new(key,
self._factory.MODE_CTR,
nonce=struct.pack("B", q - 1) + self.nonce,
**cipher_params)
# S_0, step 6 in 6.1 for j=0
self._s_0 = self._cipher.encrypt(b'\x00' * 16)
# Try to start the MAC
if None not in (assoc_len, msg_len):
self._start_mac()
def _start_mac(self):
assert(self._mac_status == MacStatus.NOT_STARTED)
assert(None not in (self._assoc_len, self._msg_len))
assert(isinstance(self._cache, list))
# Formatting control information and nonce (A.2.1)
q = 15 - len(self.nonce) # length of Q, the encoded message length (2..8)
flags = (self._assoc_len > 0) << 6
flags |= ((self._mac_len - 2) // 2) << 3
flags |= q - 1
b_0 = struct.pack("B", flags) + self.nonce + long_to_bytes(self._msg_len, q)
# Formatting associated data (A.2.2)
# Encoded 'a' is concatenated with the associated data 'A'
assoc_len_encoded = b''
if self._assoc_len > 0:
if self._assoc_len < (2 ** 16 - 2 ** 8):
enc_size = 2
elif self._assoc_len < (2 ** 32):
assoc_len_encoded = b'\xFF\xFE'
enc_size = 4
else:
assoc_len_encoded = b'\xFF\xFF'
enc_size = 8
assoc_len_encoded += long_to_bytes(self._assoc_len, enc_size)
# b_0 and assoc_len_encoded must be processed first
self._cache.insert(0, b_0)
self._cache.insert(1, assoc_len_encoded)
# Process all the data cached so far
first_data_to_mac = b"".join(self._cache)
self._cache = b""
self._mac_status = MacStatus.PROCESSING_AUTH_DATA
self._update(first_data_to_mac)
def _pad_cache_and_update(self):
assert(self._mac_status != MacStatus.NOT_STARTED)
assert(len(self._cache) < self.block_size)
# Associated data is concatenated with the least number
# of zero bytes (possibly none) to reach alignment to
# the 16 byte boundary (A.2.3)
len_cache = len(self._cache)
if len_cache > 0:
self._update(b'\x00' * (self.block_size - len_cache))
def update(self, assoc_data):
"""Protect associated data
If there is any associated data, the caller has to invoke
this function one or more times, before using
``decrypt`` or ``encrypt``.
By *associated data* it is meant any data (e.g. packet headers) that
will not be encrypted and will be transmitted in the clear.
However, the receiver is still able to detect any modification to it.
In CCM, the *associated data* is also called
*additional authenticated data* (AAD).
If there is no associated data, this method must not be called.
The caller may split associated data in segments of any size, and
invoke this method multiple times, each time with the next segment.
:Parameters:
assoc_data : bytes/bytearray/memoryview
A piece of associated data. There are no restrictions on its size.
"""
if "update" not in self._next:
raise TypeError("update() can only be called"
" immediately after initialization")
self._next = ["update", "encrypt", "decrypt",
"digest", "verify"]
self._cumul_assoc_len += len(assoc_data)
if self._assoc_len is not None and \
self._cumul_assoc_len > self._assoc_len:
raise ValueError("Associated data is too long")
self._update(assoc_data)
return self
def _update(self, assoc_data_pt=b""):
"""Update the MAC with associated data or plaintext
(without FSM checks)"""
# If MAC has not started yet, we just park the data into a list.
# If the data is mutable, we create a copy and store that instead.
if self._mac_status == MacStatus.NOT_STARTED:
if is_writeable_buffer(assoc_data_pt):
assoc_data_pt = _copy_bytes(None, None, assoc_data_pt)
self._cache.append(assoc_data_pt)
return
assert(len(self._cache) < self.block_size)
if len(self._cache) > 0:
filler = min(self.block_size - len(self._cache),
len(assoc_data_pt))
self._cache += _copy_bytes(None, filler, assoc_data_pt)
assoc_data_pt = _copy_bytes(filler, None, assoc_data_pt)
if len(self._cache) < self.block_size:
return
# The cache is exactly one block
self._t = self._mac.encrypt(self._cache)
self._cache = b""
update_len = len(assoc_data_pt) // self.block_size * self.block_size
self._cache = _copy_bytes(update_len, None, assoc_data_pt)
if update_len > 0:
self._t = self._mac.encrypt(assoc_data_pt[:update_len])[-16:]
def encrypt(self, plaintext, output=None):
"""Encrypt data with the key set at initialization.
A cipher object is stateful: once you have encrypted a message
you cannot encrypt (or decrypt) another message using the same
object.
This method can be called only **once** if ``msg_len`` was
not passed at initialization.
If ``msg_len`` was given, the data to encrypt can be broken
up in two or more pieces and `encrypt` can be called
multiple times.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is equivalent to:
>>> c.encrypt(a+b)
This function does not add any padding to the plaintext.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
If ``output`` is ``None``, the ciphertext as ``bytes``.
Otherwise, ``None``.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() can only be called after"
" initialization or an update()")
self._next = ["encrypt", "digest"]
# No more associated data allowed from now
if self._assoc_len is None:
assert(isinstance(self._cache, list))
self._assoc_len = sum([len(x) for x in self._cache])
if self._msg_len is not None:
self._start_mac()
else:
if self._cumul_assoc_len < self._assoc_len:
raise ValueError("Associated data is too short")
# Only once piece of plaintext accepted if message length was
# not declared in advance
if self._msg_len is None:
q = 15 - len(self.nonce)
if len(long_to_bytes(len(plaintext))) > q:
raise CCMMessageTooLongError("Message too long for a %u-byte nonce" % len(self.nonce))
self._msg_len = len(plaintext)
self._start_mac()
self._next = ["digest"]
self._cumul_msg_len += len(plaintext)
if self._cumul_msg_len > self._msg_len:
msg = "Message longer than declared for (%u bytes vs %u bytes" % \
(self._cumul_msg_len, self._msg_len)
raise CCMMessageTooLongError(msg)
if self._mac_status == MacStatus.PROCESSING_AUTH_DATA:
# Associated data is concatenated with the least number
# of zero bytes (possibly none) to reach alignment to
# the 16 byte boundary (A.2.3)
self._pad_cache_and_update()
self._mac_status = MacStatus.PROCESSING_PLAINTEXT
self._update(plaintext)
return self._cipher.encrypt(plaintext, output=output)
def decrypt(self, ciphertext, output=None):
"""Decrypt data with the key set at initialization.
A cipher object is stateful: once you have decrypted a message
you cannot decrypt (or encrypt) another message with the same
object.
This method can be called only **once** if ``msg_len`` was
not passed at initialization.
If ``msg_len`` was given, the data to decrypt can be
broken up in two or more pieces and `decrypt` can be
called multiple times.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is equivalent to:
>>> c.decrypt(a+b)
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return:
If ``output`` is ``None``, the plaintext as ``bytes``.
Otherwise, ``None``.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() can only be called"
" after initialization or an update()")
self._next = ["decrypt", "verify"]
# No more associated data allowed from now
if self._assoc_len is None:
assert(isinstance(self._cache, list))
self._assoc_len = sum([len(x) for x in self._cache])
if self._msg_len is not None:
self._start_mac()
else:
if self._cumul_assoc_len < self._assoc_len:
raise ValueError("Associated data is too short")
# Only once piece of ciphertext accepted if message length was
# not declared in advance
if self._msg_len is None:
q = 15 - len(self.nonce)
if len(long_to_bytes(len(ciphertext))) > q:
raise CCMMessageTooLongError("Message too long for a %u-byte nonce" % len(self.nonce))
self._msg_len = len(ciphertext)
self._start_mac()
self._next = ["verify"]
self._cumul_msg_len += len(ciphertext)
if self._cumul_msg_len > self._msg_len:
msg = "Message longer than declared for (%u bytes vs %u bytes" % \
(self._cumul_msg_len, self._msg_len)
raise CCMMessageTooLongError(msg)
if self._mac_status == MacStatus.PROCESSING_AUTH_DATA:
# Associated data is concatenated with the least number
# of zero bytes (possibly none) to reach alignment to
# the 16 byte boundary (A.2.3)
self._pad_cache_and_update()
self._mac_status = MacStatus.PROCESSING_PLAINTEXT
# Encrypt is equivalent to decrypt with the CTR mode
plaintext = self._cipher.encrypt(ciphertext, output=output)
if output is None:
self._update(plaintext)
else:
self._update(output)
return plaintext
def digest(self):
"""Compute the *binary* MAC tag.
The caller invokes this function at the very end.
This method returns the MAC that shall be sent to the receiver,
together with the ciphertext.
:Return: the MAC, as a byte string.
"""
if "digest" not in self._next:
raise TypeError("digest() cannot be called when decrypting"
" or validating a message")
self._next = ["digest"]
return self._digest()
def _digest(self):
if self._mac_tag:
return self._mac_tag
if self._assoc_len is None:
assert(isinstance(self._cache, list))
self._assoc_len = sum([len(x) for x in self._cache])
if self._msg_len is not None:
self._start_mac()
else:
if self._cumul_assoc_len < self._assoc_len:
raise ValueError("Associated data is too short")
if self._msg_len is None:
self._msg_len = 0
self._start_mac()
if self._cumul_msg_len != self._msg_len:
raise ValueError("Message is too short")
# Both associated data and payload are concatenated with the least
# number of zero bytes (possibly none) that align it to the
# 16 byte boundary (A.2.2 and A.2.3)
self._pad_cache_and_update()
# Step 8 in 6.1 (T xor MSB_Tlen(S_0))
self._mac_tag = strxor(self._t, self._s_0)[:self._mac_len]
return self._mac_tag
def hexdigest(self):
"""Compute the *printable* MAC tag.
This method is like `digest`.
:Return: the MAC, as a hexadecimal string.
"""
return "".join(["%02x" % bord(x) for x in self.digest()])
def verify(self, received_mac_tag):
"""Validate the *binary* MAC tag.
The caller invokes this function at the very end.
This method checks if the decrypted message is indeed valid
(that is, if the key is correct) and it has not been
tampered with while in transit.
:Parameters:
received_mac_tag : bytes/bytearray/memoryview
This is the *binary* MAC, as received from the sender.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
if "verify" not in self._next:
raise TypeError("verify() cannot be called"
" when encrypting a message")
self._next = ["verify"]
self._digest()
secret = get_random_bytes(16)
mac1 = BLAKE2s.new(digest_bits=160, key=secret, data=self._mac_tag)
mac2 = BLAKE2s.new(digest_bits=160, key=secret, data=received_mac_tag)
if mac1.digest() != mac2.digest():
raise ValueError("MAC check failed")
def hexverify(self, hex_mac_tag):
"""Validate the *printable* MAC tag.
This method is like `verify`.
:Parameters:
hex_mac_tag : string
This is the *printable* MAC, as received from the sender.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
self.verify(unhexlify(hex_mac_tag))
def encrypt_and_digest(self, plaintext, output=None):
"""Perform encrypt() and digest() in one step.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
a tuple with two items:
- the ciphertext, as ``bytes``
- the MAC tag, as ``bytes``
The first item becomes ``None`` when the ``output`` parameter
specified a location for the result.
"""
return self.encrypt(plaintext, output=output), self.digest()
def decrypt_and_verify(self, ciphertext, received_mac_tag, output=None):
"""Perform decrypt() and verify() in one step.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
received_mac_tag : bytes/bytearray/memoryview
This is the *binary* MAC, as received from the sender.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return: the plaintext as ``bytes`` or ``None`` when the ``output``
parameter specified a location for the result.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
plaintext = self.decrypt(ciphertext, output=output)
self.verify(received_mac_tag)
return plaintext
def _create_ccm_cipher(factory, **kwargs):
"""Create a new block cipher, configured in CCM mode.
:Parameters:
factory : module
A symmetric cipher module from `Crypto.Cipher` (like
`Crypto.Cipher.AES`).
:Keywords:
key : bytes/bytearray/memoryview
The secret key to use in the symmetric cipher.
nonce : bytes/bytearray/memoryview
A value that must never be reused for any other encryption.
Its length must be in the range ``[7..13]``.
11 or 12 bytes are reasonable values in general. Bear in
mind that with CCM there is a trade-off between nonce length and
maximum message size.
If not specified, a 11 byte long random string is used.
mac_len : integer
Length of the MAC, in bytes. It must be even and in
the range ``[4..16]``. The default is 16.
msg_len : integer
Length of the message to (de)cipher.
If not specified, ``encrypt`` or ``decrypt`` may only be called once.
assoc_len : integer
Length of the associated data.
If not specified, all data is internally buffered.
"""
try:
key = key = kwargs.pop("key")
except KeyError as e:
raise TypeError("Missing parameter: " + str(e))
nonce = kwargs.pop("nonce", None) # N
if nonce is None:
nonce = get_random_bytes(11)
mac_len = kwargs.pop("mac_len", factory.block_size)
msg_len = kwargs.pop("msg_len", None) # p
assoc_len = kwargs.pop("assoc_len", None) # a
cipher_params = dict(kwargs)
return CcmMode(factory, key, nonce, mac_len, msg_len,
assoc_len, cipher_params)

View File

@ -0,0 +1,52 @@
from types import ModuleType
from typing import Union, overload, Dict, Tuple, Optional
Buffer = Union[bytes, bytearray, memoryview]
__all__ = ['CcmMode']
class CCMMessageTooLongError(ValueError):
pass
class CcmMode(object):
block_size: int
nonce: bytes
def __init__(self,
factory: ModuleType,
key: Buffer,
nonce: Buffer,
mac_len: int,
msg_len: Optional[int],
assoc_len: Optional[int],
cipher_params: Dict) -> None: ...
def update(self, assoc_data: Buffer) -> CcmMode: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def verify(self, received_mac_tag: Buffer) -> None: ...
def hexverify(self, hex_mac_tag: str) -> None: ...
@overload
def encrypt_and_digest(self,
plaintext: Buffer) -> Tuple[bytes, bytes]: ...
@overload
def encrypt_and_digest(self,
plaintext: Buffer,
output: Buffer) -> Tuple[None, bytes]: ...
def decrypt_and_verify(self,
ciphertext: Buffer,
received_mac_tag: Buffer,
output: Optional[Union[bytearray, memoryview]] = ...) -> bytes: ...

View File

@ -0,0 +1,293 @@
# -*- coding: utf-8 -*-
#
# Cipher/mode_cfb.py : CFB mode
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
"""
Counter Feedback (CFB) mode.
"""
__all__ = ['CfbMode']
from Crypto.Util.py3compat import _copy_bytes
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
create_string_buffer, get_raw_buffer,
SmartPointer, c_size_t, c_uint8_ptr,
is_writeable_buffer)
from Crypto.Random import get_random_bytes
raw_cfb_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_cfb","""
int CFB_start_operation(void *cipher,
const uint8_t iv[],
size_t iv_len,
size_t segment_len, /* In bytes */
void **pResult);
int CFB_encrypt(void *cfbState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int CFB_decrypt(void *cfbState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int CFB_stop_operation(void *state);"""
)
class CfbMode(object):
"""*Cipher FeedBack (CFB)*.
This mode is similar to CFB, but it transforms
the underlying block cipher into a stream cipher.
Plaintext and ciphertext are processed in *segments*
of **s** bits. The mode is therefore sometimes
labelled **s**-bit CFB.
An Initialization Vector (*IV*) is required.
See `NIST SP800-38A`_ , Section 6.3.
.. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
:undocumented: __init__
"""
def __init__(self, block_cipher, iv, segment_size):
"""Create a new block cipher, configured in CFB mode.
:Parameters:
block_cipher : C pointer
A smart pointer to the low-level block cipher instance.
iv : bytes/bytearray/memoryview
The initialization vector to use for encryption or decryption.
It is as long as the cipher block.
**The IV must be unpredictable**. Ideally it is picked randomly.
Reusing the *IV* for encryptions performed with the same key
compromises confidentiality.
segment_size : integer
The number of bytes the plaintext and ciphertext are segmented in.
"""
self._state = VoidPointer()
result = raw_cfb_lib.CFB_start_operation(block_cipher.get(),
c_uint8_ptr(iv),
c_size_t(len(iv)),
c_size_t(segment_size),
self._state.address_of())
if result:
raise ValueError("Error %d while instantiating the CFB mode" % result)
# Ensure that object disposal of this Python object will (eventually)
# free the memory allocated by the raw library for the cipher mode
self._state = SmartPointer(self._state.get(),
raw_cfb_lib.CFB_stop_operation)
# Memory allocated for the underlying block cipher is now owed
# by the cipher mode
block_cipher.release()
self.block_size = len(iv)
"""The block size of the underlying cipher, in bytes."""
self.iv = _copy_bytes(None, None, iv)
"""The Initialization Vector originally used to create the object.
The value does not change."""
self.IV = self.iv
"""Alias for `iv`"""
self._next = ["encrypt", "decrypt"]
def encrypt(self, plaintext, output=None):
"""Encrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have encrypted a message
you cannot encrypt (or decrypt) another message using the same
object.
The data to encrypt can be broken up in two or
more pieces and `encrypt` can be called multiple times.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is equivalent to:
>>> c.encrypt(a+b)
This function does not add any padding to the plaintext.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() cannot be called after decrypt()")
self._next = ["encrypt"]
if output is None:
ciphertext = create_string_buffer(len(plaintext))
else:
ciphertext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(plaintext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_cfb_lib.CFB_encrypt(self._state.get(),
c_uint8_ptr(plaintext),
c_uint8_ptr(ciphertext),
c_size_t(len(plaintext)))
if result:
raise ValueError("Error %d while encrypting in CFB mode" % result)
if output is None:
return get_raw_buffer(ciphertext)
else:
return None
def decrypt(self, ciphertext, output=None):
"""Decrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have decrypted a message
you cannot decrypt (or encrypt) another message with the same
object.
The data to decrypt can be broken up in two or
more pieces and `decrypt` can be called multiple times.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is equivalent to:
>>> c.decrypt(a+b)
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return:
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() cannot be called after encrypt()")
self._next = ["decrypt"]
if output is None:
plaintext = create_string_buffer(len(ciphertext))
else:
plaintext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(ciphertext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_cfb_lib.CFB_decrypt(self._state.get(),
c_uint8_ptr(ciphertext),
c_uint8_ptr(plaintext),
c_size_t(len(ciphertext)))
if result:
raise ValueError("Error %d while decrypting in CFB mode" % result)
if output is None:
return get_raw_buffer(plaintext)
else:
return None
def _create_cfb_cipher(factory, **kwargs):
"""Instantiate a cipher object that performs CFB encryption/decryption.
:Parameters:
factory : module
The underlying block cipher, a module from ``Crypto.Cipher``.
:Keywords:
iv : bytes/bytearray/memoryview
The IV to use for CFB.
IV : bytes/bytearray/memoryview
Alias for ``iv``.
segment_size : integer
The number of bit the plaintext and ciphertext are segmented in.
If not present, the default is 8.
Any other keyword will be passed to the underlying block cipher.
See the relevant documentation for details (at least ``key`` will need
to be present).
"""
cipher_state = factory._create_base_cipher(kwargs)
iv = kwargs.pop("IV", None)
IV = kwargs.pop("iv", None)
if (None, None) == (iv, IV):
iv = get_random_bytes(factory.block_size)
if iv is not None:
if IV is not None:
raise TypeError("You must either use 'iv' or 'IV', not both")
else:
iv = IV
if len(iv) != factory.block_size:
raise ValueError("Incorrect IV length (it must be %d bytes long)" %
factory.block_size)
segment_size_bytes, rem = divmod(kwargs.pop("segment_size", 8), 8)
if segment_size_bytes == 0 or rem != 0:
raise ValueError("'segment_size' must be positive and multiple of 8 bits")
if kwargs:
raise TypeError("Unknown parameters for CFB: %s" % str(kwargs))
return CfbMode(cipher_state, iv, segment_size_bytes)

View File

@ -0,0 +1,26 @@
from typing import Union, overload
from Crypto.Util._raw_api import SmartPointer
Buffer = Union[bytes, bytearray, memoryview]
__all__ = ['CfbMode']
class CfbMode(object):
block_size: int
iv: Buffer
IV: Buffer
def __init__(self,
block_cipher: SmartPointer,
iv: Buffer,
segment_size: int) -> None: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...

View File

@ -0,0 +1,393 @@
# -*- coding: utf-8 -*-
#
# Cipher/mode_ctr.py : CTR mode
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
"""
Counter (CTR) mode.
"""
__all__ = ['CtrMode']
import struct
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
create_string_buffer, get_raw_buffer,
SmartPointer, c_size_t, c_uint8_ptr,
is_writeable_buffer)
from Crypto.Random import get_random_bytes
from Crypto.Util.py3compat import _copy_bytes, is_native_int
from Crypto.Util.number import long_to_bytes
raw_ctr_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_ctr", """
int CTR_start_operation(void *cipher,
uint8_t initialCounterBlock[],
size_t initialCounterBlock_len,
size_t prefix_len,
unsigned counter_len,
unsigned littleEndian,
void **pResult);
int CTR_encrypt(void *ctrState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int CTR_decrypt(void *ctrState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int CTR_stop_operation(void *ctrState);"""
)
class CtrMode(object):
"""*CounTeR (CTR)* mode.
This mode is very similar to ECB, in that
encryption of one block is done independently of all other blocks.
Unlike ECB, the block *position* contributes to the encryption
and no information leaks about symbol frequency.
Each message block is associated to a *counter* which
must be unique across all messages that get encrypted
with the same key (not just within the same message).
The counter is as big as the block size.
Counters can be generated in several ways. The most
straightword one is to choose an *initial counter block*
(which can be made public, similarly to the *IV* for the
other modes) and increment its lowest **m** bits by one
(modulo *2^m*) for each block. In most cases, **m** is
chosen to be half the block size.
See `NIST SP800-38A`_, Section 6.5 (for the mode) and
Appendix B (for how to manage the *initial counter block*).
.. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
:undocumented: __init__
"""
def __init__(self, block_cipher, initial_counter_block,
prefix_len, counter_len, little_endian):
"""Create a new block cipher, configured in CTR mode.
:Parameters:
block_cipher : C pointer
A smart pointer to the low-level block cipher instance.
initial_counter_block : bytes/bytearray/memoryview
The initial plaintext to use to generate the key stream.
It is as large as the cipher block, and it embeds
the initial value of the counter.
This value must not be reused.
It shall contain a nonce or a random component.
Reusing the *initial counter block* for encryptions
performed with the same key compromises confidentiality.
prefix_len : integer
The amount of bytes at the beginning of the counter block
that never change.
counter_len : integer
The length in bytes of the counter embedded in the counter
block.
little_endian : boolean
True if the counter in the counter block is an integer encoded
in little endian mode. If False, it is big endian.
"""
if len(initial_counter_block) == prefix_len + counter_len:
self.nonce = _copy_bytes(None, prefix_len, initial_counter_block)
"""Nonce; not available if there is a fixed suffix"""
self._state = VoidPointer()
result = raw_ctr_lib.CTR_start_operation(block_cipher.get(),
c_uint8_ptr(initial_counter_block),
c_size_t(len(initial_counter_block)),
c_size_t(prefix_len),
counter_len,
little_endian,
self._state.address_of())
if result:
raise ValueError("Error %X while instantiating the CTR mode"
% result)
# Ensure that object disposal of this Python object will (eventually)
# free the memory allocated by the raw library for the cipher mode
self._state = SmartPointer(self._state.get(),
raw_ctr_lib.CTR_stop_operation)
# Memory allocated for the underlying block cipher is now owed
# by the cipher mode
block_cipher.release()
self.block_size = len(initial_counter_block)
"""The block size of the underlying cipher, in bytes."""
self._next = ["encrypt", "decrypt"]
def encrypt(self, plaintext, output=None):
"""Encrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have encrypted a message
you cannot encrypt (or decrypt) another message using the same
object.
The data to encrypt can be broken up in two or
more pieces and `encrypt` can be called multiple times.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is equivalent to:
>>> c.encrypt(a+b)
This function does not add any padding to the plaintext.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() cannot be called after decrypt()")
self._next = ["encrypt"]
if output is None:
ciphertext = create_string_buffer(len(plaintext))
else:
ciphertext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(plaintext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_ctr_lib.CTR_encrypt(self._state.get(),
c_uint8_ptr(plaintext),
c_uint8_ptr(ciphertext),
c_size_t(len(plaintext)))
if result:
if result == 0x60002:
raise OverflowError("The counter has wrapped around in"
" CTR mode")
raise ValueError("Error %X while encrypting in CTR mode" % result)
if output is None:
return get_raw_buffer(ciphertext)
else:
return None
def decrypt(self, ciphertext, output=None):
"""Decrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have decrypted a message
you cannot decrypt (or encrypt) another message with the same
object.
The data to decrypt can be broken up in two or
more pieces and `decrypt` can be called multiple times.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is equivalent to:
>>> c.decrypt(a+b)
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return:
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() cannot be called after encrypt()")
self._next = ["decrypt"]
if output is None:
plaintext = create_string_buffer(len(ciphertext))
else:
plaintext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(ciphertext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_ctr_lib.CTR_decrypt(self._state.get(),
c_uint8_ptr(ciphertext),
c_uint8_ptr(plaintext),
c_size_t(len(ciphertext)))
if result:
if result == 0x60002:
raise OverflowError("The counter has wrapped around in"
" CTR mode")
raise ValueError("Error %X while decrypting in CTR mode" % result)
if output is None:
return get_raw_buffer(plaintext)
else:
return None
def _create_ctr_cipher(factory, **kwargs):
"""Instantiate a cipher object that performs CTR encryption/decryption.
:Parameters:
factory : module
The underlying block cipher, a module from ``Crypto.Cipher``.
:Keywords:
nonce : bytes/bytearray/memoryview
The fixed part at the beginning of the counter block - the rest is
the counter number that gets increased when processing the next block.
The nonce must be such that no two messages are encrypted under the
same key and the same nonce.
The nonce must be shorter than the block size (it can have
zero length; the counter is then as long as the block).
If this parameter is not present, a random nonce will be created with
length equal to half the block size. No random nonce shorter than
64 bits will be created though - you must really think through all
security consequences of using such a short block size.
initial_value : posive integer or bytes/bytearray/memoryview
The initial value for the counter. If not present, the cipher will
start counting from 0. The value is incremented by one for each block.
The counter number is encoded in big endian mode.
counter : object
Instance of ``Crypto.Util.Counter``, which allows full customization
of the counter block. This parameter is incompatible to both ``nonce``
and ``initial_value``.
Any other keyword will be passed to the underlying block cipher.
See the relevant documentation for details (at least ``key`` will need
to be present).
"""
cipher_state = factory._create_base_cipher(kwargs)
counter = kwargs.pop("counter", None)
nonce = kwargs.pop("nonce", None)
initial_value = kwargs.pop("initial_value", None)
if kwargs:
raise TypeError("Invalid parameters for CTR mode: %s" % str(kwargs))
if counter is not None and (nonce, initial_value) != (None, None):
raise TypeError("'counter' and 'nonce'/'initial_value'"
" are mutually exclusive")
if counter is None:
# Crypto.Util.Counter is not used
if nonce is None:
if factory.block_size < 16:
raise TypeError("Impossible to create a safe nonce for short"
" block sizes")
nonce = get_random_bytes(factory.block_size // 2)
else:
if len(nonce) >= factory.block_size:
raise ValueError("Nonce is too long")
# What is not nonce is counter
counter_len = factory.block_size - len(nonce)
if initial_value is None:
initial_value = 0
if is_native_int(initial_value):
if (1 << (counter_len * 8)) - 1 < initial_value:
raise ValueError("Initial counter value is too large")
initial_counter_block = nonce + long_to_bytes(initial_value, counter_len)
else:
if len(initial_value) != counter_len:
raise ValueError("Incorrect length for counter byte string (%d bytes, expected %d)" %
(len(initial_value), counter_len))
initial_counter_block = nonce + initial_value
return CtrMode(cipher_state,
initial_counter_block,
len(nonce), # prefix
counter_len,
False) # little_endian
# Crypto.Util.Counter is used
# 'counter' used to be a callable object, but now it is
# just a dictionary for backward compatibility.
_counter = dict(counter)
try:
counter_len = _counter.pop("counter_len")
prefix = _counter.pop("prefix")
suffix = _counter.pop("suffix")
initial_value = _counter.pop("initial_value")
little_endian = _counter.pop("little_endian")
except KeyError:
raise TypeError("Incorrect counter object"
" (use Crypto.Util.Counter.new)")
# Compute initial counter block
words = []
while initial_value > 0:
words.append(struct.pack('B', initial_value & 255))
initial_value >>= 8
words += [b'\x00'] * max(0, counter_len - len(words))
if not little_endian:
words.reverse()
initial_counter_block = prefix + b"".join(words) + suffix
if len(initial_counter_block) != factory.block_size:
raise ValueError("Size of the counter block (%d bytes) must match"
" block size (%d)" % (len(initial_counter_block),
factory.block_size))
return CtrMode(cipher_state, initial_counter_block,
len(prefix), counter_len, little_endian)

View File

@ -0,0 +1,27 @@
from typing import Union, overload
from Crypto.Util._raw_api import SmartPointer
Buffer = Union[bytes, bytearray, memoryview]
__all__ = ['CtrMode']
class CtrMode(object):
block_size: int
nonce: bytes
def __init__(self,
block_cipher: SmartPointer,
initial_counter_block: Buffer,
prefix_len: int,
counter_len: int,
little_endian: bool) -> None: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...

View File

@ -0,0 +1,408 @@
# ===================================================================
#
# Copyright (c) 2014, Legrandin <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
"""
EAX mode.
"""
__all__ = ['EaxMode']
import struct
from binascii import unhexlify
from Crypto.Util.py3compat import byte_string, bord, _copy_bytes
from Crypto.Util._raw_api import is_buffer
from Crypto.Util.strxor import strxor
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Hash import CMAC, BLAKE2s
from Crypto.Random import get_random_bytes
class EaxMode(object):
"""*EAX* mode.
This is an Authenticated Encryption with Associated Data
(`AEAD`_) mode. It provides both confidentiality and authenticity.
The header of the message may be left in the clear, if needed,
and it will still be subject to authentication.
The decryption step tells the receiver if the message comes
from a source that really knowns the secret key.
Additionally, decryption detects if any part of the message -
including the header - has been modified or corrupted.
This mode requires a *nonce*.
This mode is only available for ciphers that operate on 64 or
128 bits blocks.
There are no official standards defining EAX.
The implementation is based on `a proposal`__ that
was presented to NIST.
.. _AEAD: http://blog.cryptographyengineering.com/2012/05/how-to-choose-authenticated-encryption.html
.. __: http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/eax/eax-spec.pdf
:undocumented: __init__
"""
def __init__(self, factory, key, nonce, mac_len, cipher_params):
"""EAX cipher mode"""
self.block_size = factory.block_size
"""The block size of the underlying cipher, in bytes."""
self.nonce = _copy_bytes(None, None, nonce)
"""The nonce originally used to create the object."""
self._mac_len = mac_len
self._mac_tag = None # Cache for MAC tag
# Allowed transitions after initialization
self._next = ["update", "encrypt", "decrypt",
"digest", "verify"]
# MAC tag length
if not (2 <= self._mac_len <= self.block_size):
raise ValueError("'mac_len' must be at least 2 and not larger than %d"
% self.block_size)
# Nonce cannot be empty and must be a byte string
if len(self.nonce) == 0:
raise ValueError("Nonce cannot be empty in EAX mode")
if not is_buffer(nonce):
raise TypeError("nonce must be bytes, bytearray or memoryview")
self._omac = [
CMAC.new(key,
b'\x00' * (self.block_size - 1) + struct.pack('B', i),
ciphermod=factory,
cipher_params=cipher_params)
for i in range(0, 3)
]
# Compute MAC of nonce
self._omac[0].update(self.nonce)
self._signer = self._omac[1]
# MAC of the nonce is also the initial counter for CTR encryption
counter_int = bytes_to_long(self._omac[0].digest())
self._cipher = factory.new(key,
factory.MODE_CTR,
initial_value=counter_int,
nonce=b"",
**cipher_params)
def update(self, assoc_data):
"""Protect associated data
If there is any associated data, the caller has to invoke
this function one or more times, before using
``decrypt`` or ``encrypt``.
By *associated data* it is meant any data (e.g. packet headers) that
will not be encrypted and will be transmitted in the clear.
However, the receiver is still able to detect any modification to it.
If there is no associated data, this method must not be called.
The caller may split associated data in segments of any size, and
invoke this method multiple times, each time with the next segment.
:Parameters:
assoc_data : bytes/bytearray/memoryview
A piece of associated data. There are no restrictions on its size.
"""
if "update" not in self._next:
raise TypeError("update() can only be called"
" immediately after initialization")
self._next = ["update", "encrypt", "decrypt",
"digest", "verify"]
self._signer.update(assoc_data)
return self
def encrypt(self, plaintext, output=None):
"""Encrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have encrypted a message
you cannot encrypt (or decrypt) another message using the same
object.
The data to encrypt can be broken up in two or
more pieces and `encrypt` can be called multiple times.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is equivalent to:
>>> c.encrypt(a+b)
This function does not add any padding to the plaintext.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
If ``output`` is ``None``, the ciphertext as ``bytes``.
Otherwise, ``None``.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() can only be called after"
" initialization or an update()")
self._next = ["encrypt", "digest"]
ct = self._cipher.encrypt(plaintext, output=output)
if output is None:
self._omac[2].update(ct)
else:
self._omac[2].update(output)
return ct
def decrypt(self, ciphertext, output=None):
"""Decrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have decrypted a message
you cannot decrypt (or encrypt) another message with the same
object.
The data to decrypt can be broken up in two or
more pieces and `decrypt` can be called multiple times.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is equivalent to:
>>> c.decrypt(a+b)
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return:
If ``output`` is ``None``, the plaintext as ``bytes``.
Otherwise, ``None``.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() can only be called"
" after initialization or an update()")
self._next = ["decrypt", "verify"]
self._omac[2].update(ciphertext)
return self._cipher.decrypt(ciphertext, output=output)
def digest(self):
"""Compute the *binary* MAC tag.
The caller invokes this function at the very end.
This method returns the MAC that shall be sent to the receiver,
together with the ciphertext.
:Return: the MAC, as a byte string.
"""
if "digest" not in self._next:
raise TypeError("digest() cannot be called when decrypting"
" or validating a message")
self._next = ["digest"]
if not self._mac_tag:
tag = b'\x00' * self.block_size
for i in range(3):
tag = strxor(tag, self._omac[i].digest())
self._mac_tag = tag[:self._mac_len]
return self._mac_tag
def hexdigest(self):
"""Compute the *printable* MAC tag.
This method is like `digest`.
:Return: the MAC, as a hexadecimal string.
"""
return "".join(["%02x" % bord(x) for x in self.digest()])
def verify(self, received_mac_tag):
"""Validate the *binary* MAC tag.
The caller invokes this function at the very end.
This method checks if the decrypted message is indeed valid
(that is, if the key is correct) and it has not been
tampered with while in transit.
:Parameters:
received_mac_tag : bytes/bytearray/memoryview
This is the *binary* MAC, as received from the sender.
:Raises MacMismatchError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
if "verify" not in self._next:
raise TypeError("verify() cannot be called"
" when encrypting a message")
self._next = ["verify"]
if not self._mac_tag:
tag = b'\x00' * self.block_size
for i in range(3):
tag = strxor(tag, self._omac[i].digest())
self._mac_tag = tag[:self._mac_len]
secret = get_random_bytes(16)
mac1 = BLAKE2s.new(digest_bits=160, key=secret, data=self._mac_tag)
mac2 = BLAKE2s.new(digest_bits=160, key=secret, data=received_mac_tag)
if mac1.digest() != mac2.digest():
raise ValueError("MAC check failed")
def hexverify(self, hex_mac_tag):
"""Validate the *printable* MAC tag.
This method is like `verify`.
:Parameters:
hex_mac_tag : string
This is the *printable* MAC, as received from the sender.
:Raises MacMismatchError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
self.verify(unhexlify(hex_mac_tag))
def encrypt_and_digest(self, plaintext, output=None):
"""Perform encrypt() and digest() in one step.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
a tuple with two items:
- the ciphertext, as ``bytes``
- the MAC tag, as ``bytes``
The first item becomes ``None`` when the ``output`` parameter
specified a location for the result.
"""
return self.encrypt(plaintext, output=output), self.digest()
def decrypt_and_verify(self, ciphertext, received_mac_tag, output=None):
"""Perform decrypt() and verify() in one step.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
received_mac_tag : bytes/bytearray/memoryview
This is the *binary* MAC, as received from the sender.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return: the plaintext as ``bytes`` or ``None`` when the ``output``
parameter specified a location for the result.
:Raises MacMismatchError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
pt = self.decrypt(ciphertext, output=output)
self.verify(received_mac_tag)
return pt
def _create_eax_cipher(factory, **kwargs):
"""Create a new block cipher, configured in EAX mode.
:Parameters:
factory : module
A symmetric cipher module from `Crypto.Cipher` (like
`Crypto.Cipher.AES`).
:Keywords:
key : bytes/bytearray/memoryview
The secret key to use in the symmetric cipher.
nonce : bytes/bytearray/memoryview
A value that must never be reused for any other encryption.
There are no restrictions on its length, but it is recommended to use
at least 16 bytes.
The nonce shall never repeat for two different messages encrypted with
the same key, but it does not need to be random.
If not specified, a 16 byte long random string is used.
mac_len : integer
Length of the MAC, in bytes. It must be no larger than the cipher
block bytes (which is the default).
"""
try:
key = kwargs.pop("key")
nonce = kwargs.pop("nonce", None)
if nonce is None:
nonce = get_random_bytes(16)
mac_len = kwargs.pop("mac_len", factory.block_size)
except KeyError as e:
raise TypeError("Missing parameter: " + str(e))
return EaxMode(factory, key, nonce, mac_len, kwargs)

View File

@ -0,0 +1,45 @@
from types import ModuleType
from typing import Any, Union, Tuple, Dict, overload, Optional
Buffer = Union[bytes, bytearray, memoryview]
__all__ = ['EaxMode']
class EaxMode(object):
block_size: int
nonce: bytes
def __init__(self,
factory: ModuleType,
key: Buffer,
nonce: Buffer,
mac_len: int,
cipher_params: Dict) -> None: ...
def update(self, assoc_data: Buffer) -> EaxMode: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def verify(self, received_mac_tag: Buffer) -> None: ...
def hexverify(self, hex_mac_tag: str) -> None: ...
@overload
def encrypt_and_digest(self,
plaintext: Buffer) -> Tuple[bytes, bytes]: ...
@overload
def encrypt_and_digest(self,
plaintext: Buffer,
output: Buffer) -> Tuple[None, bytes]: ...
def decrypt_and_verify(self,
ciphertext: Buffer,
received_mac_tag: Buffer,
output: Optional[Union[bytearray, memoryview]] = ...) -> bytes: ...

View File

@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
#
# Cipher/mode_ecb.py : ECB mode
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
"""
Electronic Code Book (ECB) mode.
"""
__all__ = [ 'EcbMode' ]
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
VoidPointer, create_string_buffer,
get_raw_buffer, SmartPointer,
c_size_t, c_uint8_ptr,
is_writeable_buffer)
raw_ecb_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_ecb", """
int ECB_start_operation(void *cipher,
void **pResult);
int ECB_encrypt(void *ecbState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int ECB_decrypt(void *ecbState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int ECB_stop_operation(void *state);
"""
)
class EcbMode(object):
"""*Electronic Code Book (ECB)*.
This is the simplest encryption mode. Each of the plaintext blocks
is directly encrypted into a ciphertext block, independently of
any other block.
This mode is dangerous because it exposes frequency of symbols
in your plaintext. Other modes (e.g. *CBC*) should be used instead.
See `NIST SP800-38A`_ , Section 6.1.
.. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
:undocumented: __init__
"""
def __init__(self, block_cipher):
"""Create a new block cipher, configured in ECB mode.
:Parameters:
block_cipher : C pointer
A smart pointer to the low-level block cipher instance.
"""
self.block_size = block_cipher.block_size
self._state = VoidPointer()
result = raw_ecb_lib.ECB_start_operation(block_cipher.get(),
self._state.address_of())
if result:
raise ValueError("Error %d while instantiating the ECB mode"
% result)
# Ensure that object disposal of this Python object will (eventually)
# free the memory allocated by the raw library for the cipher
# mode
self._state = SmartPointer(self._state.get(),
raw_ecb_lib.ECB_stop_operation)
# Memory allocated for the underlying block cipher is now owned
# by the cipher mode
block_cipher.release()
def encrypt(self, plaintext, output=None):
"""Encrypt data with the key set at initialization.
The data to encrypt can be broken up in two or
more pieces and `encrypt` can be called multiple times.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is equivalent to:
>>> c.encrypt(a+b)
This function does not add any padding to the plaintext.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
The length must be multiple of the cipher block length.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
Otherwise, ``None``.
"""
if output is None:
ciphertext = create_string_buffer(len(plaintext))
else:
ciphertext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(plaintext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_ecb_lib.ECB_encrypt(self._state.get(),
c_uint8_ptr(plaintext),
c_uint8_ptr(ciphertext),
c_size_t(len(plaintext)))
if result:
if result == 3:
raise ValueError("Data must be aligned to block boundary in ECB mode")
raise ValueError("Error %d while encrypting in ECB mode" % result)
if output is None:
return get_raw_buffer(ciphertext)
else:
return None
def decrypt(self, ciphertext, output=None):
"""Decrypt data with the key set at initialization.
The data to decrypt can be broken up in two or
more pieces and `decrypt` can be called multiple times.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is equivalent to:
>>> c.decrypt(a+b)
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
The length must be multiple of the cipher block length.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return:
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
Otherwise, ``None``.
"""
if output is None:
plaintext = create_string_buffer(len(ciphertext))
else:
plaintext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(ciphertext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_ecb_lib.ECB_decrypt(self._state.get(),
c_uint8_ptr(ciphertext),
c_uint8_ptr(plaintext),
c_size_t(len(ciphertext)))
if result:
if result == 3:
raise ValueError("Data must be aligned to block boundary in ECB mode")
raise ValueError("Error %d while decrypting in ECB mode" % result)
if output is None:
return get_raw_buffer(plaintext)
else:
return None
def _create_ecb_cipher(factory, **kwargs):
"""Instantiate a cipher object that performs ECB encryption/decryption.
:Parameters:
factory : module
The underlying block cipher, a module from ``Crypto.Cipher``.
All keywords are passed to the underlying block cipher.
See the relevant documentation for details (at least ``key`` will need
to be present"""
cipher_state = factory._create_base_cipher(kwargs)
cipher_state.block_size = factory.block_size
if kwargs:
raise TypeError("Unknown parameters for ECB: %s" % str(kwargs))
return EcbMode(cipher_state)

View File

@ -0,0 +1,19 @@
from typing import Union, overload
from Crypto.Util._raw_api import SmartPointer
Buffer = Union[bytes, bytearray, memoryview]
__all__ = [ 'EcbMode' ]
class EcbMode(object):
def __init__(self, block_cipher: SmartPointer) -> None: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...

View File

@ -0,0 +1,620 @@
# ===================================================================
#
# Copyright (c) 2014, Legrandin <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
"""
Galois/Counter Mode (GCM).
"""
__all__ = ['GcmMode']
from binascii import unhexlify
from Crypto.Util.py3compat import bord, _copy_bytes
from Crypto.Util._raw_api import is_buffer
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Hash import BLAKE2s
from Crypto.Random import get_random_bytes
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
create_string_buffer, get_raw_buffer,
SmartPointer, c_size_t, c_uint8_ptr)
from Crypto.Util import _cpu_features
# C API by module implementing GHASH
_ghash_api_template = """
int ghash_%imp%(uint8_t y_out[16],
const uint8_t block_data[],
size_t len,
const uint8_t y_in[16],
const void *exp_key);
int ghash_expand_%imp%(const uint8_t h[16],
void **ghash_tables);
int ghash_destroy_%imp%(void *ghash_tables);
"""
def _build_impl(lib, postfix):
from collections import namedtuple
funcs = ( "ghash", "ghash_expand", "ghash_destroy" )
GHASH_Imp = namedtuple('_GHash_Imp', funcs)
try:
imp_funcs = [ getattr(lib, x + "_" + postfix) for x in funcs ]
except AttributeError: # Make sphinx stop complaining with its mocklib
imp_funcs = [ None ] * 3
params = dict(zip(funcs, imp_funcs))
return GHASH_Imp(**params)
def _get_ghash_portable():
api = _ghash_api_template.replace("%imp%", "portable")
lib = load_pycryptodome_raw_lib("Crypto.Hash._ghash_portable", api)
result = _build_impl(lib, "portable")
return result
_ghash_portable = _get_ghash_portable()
def _get_ghash_clmul():
"""Return None if CLMUL implementation is not available"""
if not _cpu_features.have_clmul():
return None
try:
api = _ghash_api_template.replace("%imp%", "clmul")
lib = load_pycryptodome_raw_lib("Crypto.Hash._ghash_clmul", api)
result = _build_impl(lib, "clmul")
except OSError:
result = None
return result
_ghash_clmul = _get_ghash_clmul()
class _GHASH(object):
"""GHASH function defined in NIST SP 800-38D, Algorithm 2.
If X_1, X_2, .. X_m are the blocks of input data, the function
computes:
X_1*H^{m} + X_2*H^{m-1} + ... + X_m*H
in the Galois field GF(2^256) using the reducing polynomial
(x^128 + x^7 + x^2 + x + 1).
"""
def __init__(self, subkey, ghash_c):
assert len(subkey) == 16
self.ghash_c = ghash_c
self._exp_key = VoidPointer()
result = ghash_c.ghash_expand(c_uint8_ptr(subkey),
self._exp_key.address_of())
if result:
raise ValueError("Error %d while expanding the GHASH key" % result)
self._exp_key = SmartPointer(self._exp_key.get(),
ghash_c.ghash_destroy)
# create_string_buffer always returns a string of zeroes
self._last_y = create_string_buffer(16)
def update(self, block_data):
assert len(block_data) % 16 == 0
result = self.ghash_c.ghash(self._last_y,
c_uint8_ptr(block_data),
c_size_t(len(block_data)),
self._last_y,
self._exp_key.get())
if result:
raise ValueError("Error %d while updating GHASH" % result)
return self
def digest(self):
return get_raw_buffer(self._last_y)
def enum(**enums):
return type('Enum', (), enums)
MacStatus = enum(PROCESSING_AUTH_DATA=1, PROCESSING_CIPHERTEXT=2)
class GcmMode(object):
"""Galois Counter Mode (GCM).
This is an Authenticated Encryption with Associated Data (`AEAD`_) mode.
It provides both confidentiality and authenticity.
The header of the message may be left in the clear, if needed, and it will
still be subject to authentication. The decryption step tells the receiver
if the message comes from a source that really knowns the secret key.
Additionally, decryption detects if any part of the message - including the
header - has been modified or corrupted.
This mode requires a *nonce*.
This mode is only available for ciphers that operate on 128 bits blocks
(e.g. AES but not TDES).
See `NIST SP800-38D`_.
.. _`NIST SP800-38D`: http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf
.. _AEAD: http://blog.cryptographyengineering.com/2012/05/how-to-choose-authenticated-encryption.html
:undocumented: __init__
"""
def __init__(self, factory, key, nonce, mac_len, cipher_params, ghash_c):
self.block_size = factory.block_size
if self.block_size != 16:
raise ValueError("GCM mode is only available for ciphers"
" that operate on 128 bits blocks")
if len(nonce) == 0:
raise ValueError("Nonce cannot be empty")
if not is_buffer(nonce):
raise TypeError("Nonce must be bytes, bytearray or memoryview")
# See NIST SP 800 38D, 5.2.1.1
if len(nonce) > 2**64 - 1:
raise ValueError("Nonce exceeds maximum length")
self.nonce = _copy_bytes(None, None, nonce)
"""Nonce"""
self._factory = factory
self._key = _copy_bytes(None, None, key)
self._tag = None # Cache for MAC tag
self._mac_len = mac_len
if not (4 <= mac_len <= 16):
raise ValueError("Parameter 'mac_len' must be in the range 4..16")
# Allowed transitions after initialization
self._next = ["update", "encrypt", "decrypt",
"digest", "verify"]
self._no_more_assoc_data = False
# Length of associated data
self._auth_len = 0
# Length of the ciphertext or plaintext
self._msg_len = 0
# Step 1 in SP800-38D, Algorithm 4 (encryption) - Compute H
# See also Algorithm 5 (decryption)
hash_subkey = factory.new(key,
self._factory.MODE_ECB,
**cipher_params
).encrypt(b'\x00' * 16)
# Step 2 - Compute J0
if len(self.nonce) == 12:
j0 = self.nonce + b"\x00\x00\x00\x01"
else:
fill = (16 - (len(self.nonce) % 16)) % 16 + 8
ghash_in = (self.nonce +
b'\x00' * fill +
long_to_bytes(8 * len(self.nonce), 8))
j0 = _GHASH(hash_subkey, ghash_c).update(ghash_in).digest()
# Step 3 - Prepare GCTR cipher for encryption/decryption
nonce_ctr = j0[:12]
iv_ctr = (bytes_to_long(j0) + 1) & 0xFFFFFFFF
self._cipher = factory.new(key,
self._factory.MODE_CTR,
initial_value=iv_ctr,
nonce=nonce_ctr,
**cipher_params)
# Step 5 - Bootstrat GHASH
self._signer = _GHASH(hash_subkey, ghash_c)
# Step 6 - Prepare GCTR cipher for GMAC
self._tag_cipher = factory.new(key,
self._factory.MODE_CTR,
initial_value=j0,
nonce=b"",
**cipher_params)
# Cache for data to authenticate
self._cache = b""
self._status = MacStatus.PROCESSING_AUTH_DATA
def update(self, assoc_data):
"""Protect associated data
If there is any associated data, the caller has to invoke
this function one or more times, before using
``decrypt`` or ``encrypt``.
By *associated data* it is meant any data (e.g. packet headers) that
will not be encrypted and will be transmitted in the clear.
However, the receiver is still able to detect any modification to it.
In GCM, the *associated data* is also called
*additional authenticated data* (AAD).
If there is no associated data, this method must not be called.
The caller may split associated data in segments of any size, and
invoke this method multiple times, each time with the next segment.
:Parameters:
assoc_data : bytes/bytearray/memoryview
A piece of associated data. There are no restrictions on its size.
"""
if "update" not in self._next:
raise TypeError("update() can only be called"
" immediately after initialization")
self._next = ["update", "encrypt", "decrypt",
"digest", "verify"]
self._update(assoc_data)
self._auth_len += len(assoc_data)
# See NIST SP 800 38D, 5.2.1.1
if self._auth_len > 2**64 - 1:
raise ValueError("Additional Authenticated Data exceeds maximum length")
return self
def _update(self, data):
assert(len(self._cache) < 16)
if len(self._cache) > 0:
filler = min(16 - len(self._cache), len(data))
self._cache += _copy_bytes(None, filler, data)
data = data[filler:]
if len(self._cache) < 16:
return
# The cache is exactly one block
self._signer.update(self._cache)
self._cache = b""
update_len = len(data) // 16 * 16
self._cache = _copy_bytes(update_len, None, data)
if update_len > 0:
self._signer.update(data[:update_len])
def _pad_cache_and_update(self):
assert(len(self._cache) < 16)
# The authenticated data A is concatenated to the minimum
# number of zero bytes (possibly none) such that the
# - ciphertext C is aligned to the 16 byte boundary.
# See step 5 in section 7.1
# - ciphertext C is aligned to the 16 byte boundary.
# See step 6 in section 7.2
len_cache = len(self._cache)
if len_cache > 0:
self._update(b'\x00' * (16 - len_cache))
def encrypt(self, plaintext, output=None):
"""Encrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have encrypted a message
you cannot encrypt (or decrypt) another message using the same
object.
The data to encrypt can be broken up in two or
more pieces and `encrypt` can be called multiple times.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is equivalent to:
>>> c.encrypt(a+b)
This function does not add any padding to the plaintext.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
If ``output`` is ``None``, the ciphertext as ``bytes``.
Otherwise, ``None``.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() can only be called after"
" initialization or an update()")
self._next = ["encrypt", "digest"]
ciphertext = self._cipher.encrypt(plaintext, output=output)
if self._status == MacStatus.PROCESSING_AUTH_DATA:
self._pad_cache_and_update()
self._status = MacStatus.PROCESSING_CIPHERTEXT
self._update(ciphertext if output is None else output)
self._msg_len += len(plaintext)
# See NIST SP 800 38D, 5.2.1.1
if self._msg_len > 2**39 - 256:
raise ValueError("Plaintext exceeds maximum length")
return ciphertext
def decrypt(self, ciphertext, output=None):
"""Decrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have decrypted a message
you cannot decrypt (or encrypt) another message with the same
object.
The data to decrypt can be broken up in two or
more pieces and `decrypt` can be called multiple times.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is equivalent to:
>>> c.decrypt(a+b)
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return:
If ``output`` is ``None``, the plaintext as ``bytes``.
Otherwise, ``None``.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() can only be called"
" after initialization or an update()")
self._next = ["decrypt", "verify"]
if self._status == MacStatus.PROCESSING_AUTH_DATA:
self._pad_cache_and_update()
self._status = MacStatus.PROCESSING_CIPHERTEXT
self._update(ciphertext)
self._msg_len += len(ciphertext)
return self._cipher.decrypt(ciphertext, output=output)
def digest(self):
"""Compute the *binary* MAC tag in an AEAD mode.
The caller invokes this function at the very end.
This method returns the MAC that shall be sent to the receiver,
together with the ciphertext.
:Return: the MAC, as a byte string.
"""
if "digest" not in self._next:
raise TypeError("digest() cannot be called when decrypting"
" or validating a message")
self._next = ["digest"]
return self._compute_mac()
def _compute_mac(self):
"""Compute MAC without any FSM checks."""
if self._tag:
return self._tag
# Step 5 in NIST SP 800-38D, Algorithm 4 - Compute S
self._pad_cache_and_update()
self._update(long_to_bytes(8 * self._auth_len, 8))
self._update(long_to_bytes(8 * self._msg_len, 8))
s_tag = self._signer.digest()
# Step 6 - Compute T
self._tag = self._tag_cipher.encrypt(s_tag)[:self._mac_len]
return self._tag
def hexdigest(self):
"""Compute the *printable* MAC tag.
This method is like `digest`.
:Return: the MAC, as a hexadecimal string.
"""
return "".join(["%02x" % bord(x) for x in self.digest()])
def verify(self, received_mac_tag):
"""Validate the *binary* MAC tag.
The caller invokes this function at the very end.
This method checks if the decrypted message is indeed valid
(that is, if the key is correct) and it has not been
tampered with while in transit.
:Parameters:
received_mac_tag : bytes/bytearray/memoryview
This is the *binary* MAC, as received from the sender.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
if "verify" not in self._next:
raise TypeError("verify() cannot be called"
" when encrypting a message")
self._next = ["verify"]
secret = get_random_bytes(16)
mac1 = BLAKE2s.new(digest_bits=160, key=secret,
data=self._compute_mac())
mac2 = BLAKE2s.new(digest_bits=160, key=secret,
data=received_mac_tag)
if mac1.digest() != mac2.digest():
raise ValueError("MAC check failed")
def hexverify(self, hex_mac_tag):
"""Validate the *printable* MAC tag.
This method is like `verify`.
:Parameters:
hex_mac_tag : string
This is the *printable* MAC, as received from the sender.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
self.verify(unhexlify(hex_mac_tag))
def encrypt_and_digest(self, plaintext, output=None):
"""Perform encrypt() and digest() in one step.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
a tuple with two items:
- the ciphertext, as ``bytes``
- the MAC tag, as ``bytes``
The first item becomes ``None`` when the ``output`` parameter
specified a location for the result.
"""
return self.encrypt(plaintext, output=output), self.digest()
def decrypt_and_verify(self, ciphertext, received_mac_tag, output=None):
"""Perform decrypt() and verify() in one step.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
received_mac_tag : byte string
This is the *binary* MAC, as received from the sender.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return: the plaintext as ``bytes`` or ``None`` when the ``output``
parameter specified a location for the result.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
plaintext = self.decrypt(ciphertext, output=output)
self.verify(received_mac_tag)
return plaintext
def _create_gcm_cipher(factory, **kwargs):
"""Create a new block cipher, configured in Galois Counter Mode (GCM).
:Parameters:
factory : module
A block cipher module, taken from `Crypto.Cipher`.
The cipher must have block length of 16 bytes.
GCM has been only defined for `Crypto.Cipher.AES`.
:Keywords:
key : bytes/bytearray/memoryview
The secret key to use in the symmetric cipher.
It must be 16 (e.g. *AES-128*), 24 (e.g. *AES-192*)
or 32 (e.g. *AES-256*) bytes long.
nonce : bytes/bytearray/memoryview
A value that must never be reused for any other encryption.
There are no restrictions on its length,
but it is recommended to use at least 16 bytes.
The nonce shall never repeat for two
different messages encrypted with the same key,
but it does not need to be random.
If not provided, a 16 byte nonce will be randomly created.
mac_len : integer
Length of the MAC, in bytes.
It must be no larger than 16 bytes (which is the default).
"""
try:
key = kwargs.pop("key")
except KeyError as e:
raise TypeError("Missing parameter:" + str(e))
nonce = kwargs.pop("nonce", None)
if nonce is None:
nonce = get_random_bytes(16)
mac_len = kwargs.pop("mac_len", 16)
# Not documented - only used for testing
use_clmul = kwargs.pop("use_clmul", True)
if use_clmul and _ghash_clmul:
ghash_c = _ghash_clmul
else:
ghash_c = _ghash_portable
return GcmMode(factory, key, nonce, mac_len, kwargs, ghash_c)

View File

@ -0,0 +1,45 @@
from types import ModuleType
from typing import Union, Tuple, Dict, overload, Optional
__all__ = ['GcmMode']
Buffer = Union[bytes, bytearray, memoryview]
class GcmMode(object):
block_size: int
nonce: Buffer
def __init__(self,
factory: ModuleType,
key: Buffer,
nonce: Buffer,
mac_len: int,
cipher_params: Dict) -> None: ...
def update(self, assoc_data: Buffer) -> GcmMode: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def verify(self, received_mac_tag: Buffer) -> None: ...
def hexverify(self, hex_mac_tag: str) -> None: ...
@overload
def encrypt_and_digest(self,
plaintext: Buffer) -> Tuple[bytes, bytes]: ...
@overload
def encrypt_and_digest(self,
plaintext: Buffer,
output: Buffer) -> Tuple[None, bytes]: ...
def decrypt_and_verify(self,
ciphertext: Buffer,
received_mac_tag: Buffer,
output: Optional[Union[bytearray, memoryview]] = ...) -> bytes: ...

View File

@ -0,0 +1,158 @@
import struct
from collections import deque
from types import ModuleType
from typing import Union
from Crypto.Util.strxor import strxor
def W(cipher: ModuleType,
plaintext: Union[bytes, bytearray]) -> bytes:
S = [plaintext[i:i+8] for i in range(0, len(plaintext), 8)]
n = len(S)
s = 6 * (n - 1)
A = S[0]
R = deque(S[1:])
for t in range(1, s + 1):
t_64 = struct.pack('>Q', t)
ct = cipher.encrypt(A + R.popleft())
A = strxor(ct[:8], t_64)
R.append(ct[8:])
return A + b''.join(R)
def W_inverse(cipher: ModuleType,
ciphertext: Union[bytes, bytearray]) -> bytes:
C = [ciphertext[i:i+8] for i in range(0, len(ciphertext), 8)]
n = len(C)
s = 6 * (n - 1)
A = C[0]
R = deque(C[1:])
for t in range(s, 0, -1):
t_64 = struct.pack('>Q', t)
pt = cipher.decrypt(strxor(A, t_64) + R.pop())
A = pt[:8]
R.appendleft(pt[8:])
return A + b''.join(R)
class KWMode(object):
"""Key Wrap (KW) mode.
This is a deterministic Authenticated Encryption (AE) mode
for protecting cryptographic keys. See `NIST SP800-38F`_.
It provides both confidentiality and authenticity, and it designed
so that any bit of the ciphertext depends on all bits of the plaintext.
This mode is only available for ciphers that operate on 128 bits blocks
(e.g., AES).
.. _`NIST SP800-38F`: http://csrc.nist.gov/publications/nistpubs/800-38F/SP-800-38F.pdf
:undocumented: __init__
"""
def __init__(self,
factory: ModuleType,
key: Union[bytes, bytearray]):
self.block_size = factory.block_size
if self.block_size != 16:
raise ValueError("Key Wrap mode is only available for ciphers"
" that operate on 128 bits blocks")
self._factory = factory
self._cipher = factory.new(key, factory.MODE_ECB)
self._done = False
def seal(self, plaintext: Union[bytes, bytearray]) -> bytes:
"""Encrypt and authenticate (wrap) a cryptographic key.
Args:
plaintext:
The cryptographic key to wrap.
It must be at least 16 bytes long, and its length
must be a multiple of 8.
Returns:
The wrapped key.
"""
if self._done:
raise ValueError("The cipher cannot be used more than once")
if len(plaintext) % 8:
raise ValueError("The plaintext must have length multiple of 8 bytes")
if len(plaintext) < 16:
raise ValueError("The plaintext must be at least 16 bytes long")
if len(plaintext) >= 2**32:
raise ValueError("The plaintext is too long")
res = W(self._cipher, b'\xA6\xA6\xA6\xA6\xA6\xA6\xA6\xA6' + plaintext)
self._done = True
return res
def unseal(self, ciphertext: Union[bytes, bytearray]) -> bytes:
"""Decrypt and authenticate (unwrap) a cryptographic key.
Args:
ciphertext:
The cryptographic key to unwrap.
It must be at least 24 bytes long, and its length
must be a multiple of 8.
Returns:
The original key.
Raises: ValueError
If the ciphertext or the key are not valid.
"""
if self._done:
raise ValueError("The cipher cannot be used more than once")
if len(ciphertext) % 8:
raise ValueError("The ciphertext must have length multiple of 8 bytes")
if len(ciphertext) < 24:
raise ValueError("The ciphertext must be at least 24 bytes long")
pt = W_inverse(self._cipher, ciphertext)
if pt[:8] != b'\xA6\xA6\xA6\xA6\xA6\xA6\xA6\xA6':
raise ValueError("Incorrect integrity check value")
self._done = True
return pt[8:]
def _create_kw_cipher(factory: ModuleType,
**kwargs: Union[bytes, bytearray]) -> KWMode:
"""Create a new block cipher in Key Wrap mode.
Args:
factory:
A block cipher module, taken from `Crypto.Cipher`.
The cipher must have block length of 16 bytes, such as AES.
Keywords:
key:
The secret key to use to seal or unseal.
"""
try:
key = kwargs["key"]
except KeyError as e:
raise TypeError("Missing parameter:" + str(e))
return KWMode(factory, key)

View File

@ -0,0 +1,135 @@
import struct
from types import ModuleType
from typing import Union
from ._mode_kw import W, W_inverse
class KWPMode(object):
"""Key Wrap with Padding (KWP) mode.
This is a deterministic Authenticated Encryption (AE) mode
for protecting cryptographic keys. See `NIST SP800-38F`_.
It provides both confidentiality and authenticity, and it designed
so that any bit of the ciphertext depends on all bits of the plaintext.
This mode is only available for ciphers that operate on 128 bits blocks
(e.g., AES).
.. _`NIST SP800-38F`: http://csrc.nist.gov/publications/nistpubs/800-38F/SP-800-38F.pdf
:undocumented: __init__
"""
def __init__(self,
factory: ModuleType,
key: Union[bytes, bytearray]):
self.block_size = factory.block_size
if self.block_size != 16:
raise ValueError("Key Wrap with Padding mode is only available for ciphers"
" that operate on 128 bits blocks")
self._factory = factory
self._cipher = factory.new(key, factory.MODE_ECB)
self._done = False
def seal(self, plaintext: Union[bytes, bytearray]) -> bytes:
"""Encrypt and authenticate (wrap) a cryptographic key.
Args:
plaintext:
The cryptographic key to wrap.
Returns:
The wrapped key.
"""
if self._done:
raise ValueError("The cipher cannot be used more than once")
if len(plaintext) == 0:
raise ValueError("The plaintext must be at least 1 byte")
if len(plaintext) >= 2 ** 32:
raise ValueError("The plaintext is too long")
padlen = (8 - len(plaintext)) % 8
padded = plaintext + b'\x00' * padlen
AIV = b'\xA6\x59\x59\xA6' + struct.pack('>I', len(plaintext))
if len(padded) == 8:
res = self._cipher.encrypt(AIV + padded)
else:
res = W(self._cipher, AIV + padded)
return res
def unseal(self, ciphertext: Union[bytes, bytearray]) -> bytes:
"""Decrypt and authenticate (unwrap) a cryptographic key.
Args:
ciphertext:
The cryptographic key to unwrap.
It must be at least 16 bytes long, and its length
must be a multiple of 8.
Returns:
The original key.
Raises: ValueError
If the ciphertext or the key are not valid.
"""
if self._done:
raise ValueError("The cipher cannot be used more than once")
if len(ciphertext) % 8:
raise ValueError("The ciphertext must have length multiple of 8 bytes")
if len(ciphertext) < 16:
raise ValueError("The ciphertext must be at least 24 bytes long")
if len(ciphertext) == 16:
S = self._cipher.decrypt(ciphertext)
else:
S = W_inverse(self._cipher, ciphertext)
if S[:4] != b'\xA6\x59\x59\xA6':
raise ValueError("Incorrect decryption")
Plen = struct.unpack('>I', S[4:8])[0]
padlen = len(S) - 8 - Plen
if padlen < 0 or padlen > 7:
raise ValueError("Incorrect decryption")
if S[len(S) - padlen:] != b'\x00' * padlen:
raise ValueError("Incorrect decryption")
return S[8:len(S) - padlen]
def _create_kwp_cipher(factory: ModuleType,
**kwargs: Union[bytes, bytearray]) -> KWPMode:
"""Create a new block cipher in Key Wrap with Padding mode.
Args:
factory:
A block cipher module, taken from `Crypto.Cipher`.
The cipher must have block length of 16 bytes, such as AES.
Keywords:
key:
The secret key to use to seal or unseal.
"""
try:
key = kwargs["key"]
except KeyError as e:
raise TypeError("Missing parameter:" + str(e))
return KWPMode(factory, key)

View File

@ -0,0 +1,532 @@
# ===================================================================
#
# Copyright (c) 2014, Legrandin <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
"""
Offset Codebook (OCB) mode.
OCB is Authenticated Encryption with Associated Data (AEAD) cipher mode
designed by Prof. Phillip Rogaway and specified in `RFC7253`_.
The algorithm provides both authenticity and privacy, it is very efficient,
it uses only one key and it can be used in online mode (so that encryption
or decryption can start before the end of the message is available).
This module implements the third and last variant of OCB (OCB3) and it only
works in combination with a 128-bit block symmetric cipher, like AES.
OCB is patented in US but `free licenses`_ exist for software implementations
meant for non-military purposes.
Example:
>>> from Crypto.Cipher import AES
>>> from Crypto.Random import get_random_bytes
>>>
>>> key = get_random_bytes(32)
>>> cipher = AES.new(key, AES.MODE_OCB)
>>> plaintext = b"Attack at dawn"
>>> ciphertext, mac = cipher.encrypt_and_digest(plaintext)
>>> # Deliver cipher.nonce, ciphertext and mac
...
>>> cipher = AES.new(key, AES.MODE_OCB, nonce=nonce)
>>> try:
>>> plaintext = cipher.decrypt_and_verify(ciphertext, mac)
>>> except ValueError:
>>> print "Invalid message"
>>> else:
>>> print plaintext
:undocumented: __package__
.. _RFC7253: http://www.rfc-editor.org/info/rfc7253
.. _free licenses: http://web.cs.ucdavis.edu/~rogaway/ocb/license.htm
"""
import struct
from binascii import unhexlify
from Crypto.Util.py3compat import bord, _copy_bytes, bchr
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Util.strxor import strxor
from Crypto.Hash import BLAKE2s
from Crypto.Random import get_random_bytes
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
create_string_buffer, get_raw_buffer,
SmartPointer, c_size_t, c_uint8_ptr,
is_buffer)
_raw_ocb_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_ocb", """
int OCB_start_operation(void *cipher,
const uint8_t *offset_0,
size_t offset_0_len,
void **pState);
int OCB_encrypt(void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int OCB_decrypt(void *state,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int OCB_update(void *state,
const uint8_t *in,
size_t data_len);
int OCB_digest(void *state,
uint8_t *tag,
size_t tag_len);
int OCB_stop_operation(void *state);
""")
class OcbMode(object):
"""Offset Codebook (OCB) mode.
:undocumented: __init__
"""
def __init__(self, factory, nonce, mac_len, cipher_params):
if factory.block_size != 16:
raise ValueError("OCB mode is only available for ciphers"
" that operate on 128 bits blocks")
self.block_size = 16
"""The block size of the underlying cipher, in bytes."""
self.nonce = _copy_bytes(None, None, nonce)
"""Nonce used for this session."""
if len(nonce) not in range(1, 16):
raise ValueError("Nonce must be at most 15 bytes long")
if not is_buffer(nonce):
raise TypeError("Nonce must be bytes, bytearray or memoryview")
self._mac_len = mac_len
if not 8 <= mac_len <= 16:
raise ValueError("MAC tag must be between 8 and 16 bytes long")
# Cache for MAC tag
self._mac_tag = None
# Cache for unaligned associated data
self._cache_A = b""
# Cache for unaligned ciphertext/plaintext
self._cache_P = b""
# Allowed transitions after initialization
self._next = ["update", "encrypt", "decrypt",
"digest", "verify"]
# Compute Offset_0
params_without_key = dict(cipher_params)
key = params_without_key.pop("key")
taglen_mod128 = (self._mac_len * 8) % 128
if len(self.nonce) < 15:
nonce = bchr(taglen_mod128 << 1) +\
b'\x00' * (14 - len(nonce)) +\
b'\x01' +\
self.nonce
else:
nonce = bchr((taglen_mod128 << 1) | 0x01) +\
self.nonce
bottom_bits = bord(nonce[15]) & 0x3F # 6 bits, 0..63
top_bits = bord(nonce[15]) & 0xC0 # 2 bits
ktop_cipher = factory.new(key,
factory.MODE_ECB,
**params_without_key)
ktop = ktop_cipher.encrypt(struct.pack('15sB',
nonce[:15],
top_bits))
stretch = ktop + strxor(ktop[:8], ktop[1:9]) # 192 bits
offset_0 = long_to_bytes(bytes_to_long(stretch) >>
(64 - bottom_bits), 24)[8:]
# Create low-level cipher instance
raw_cipher = factory._create_base_cipher(cipher_params)
if cipher_params:
raise TypeError("Unknown keywords: " + str(cipher_params))
self._state = VoidPointer()
result = _raw_ocb_lib.OCB_start_operation(raw_cipher.get(),
offset_0,
c_size_t(len(offset_0)),
self._state.address_of())
if result:
raise ValueError("Error %d while instantiating the OCB mode"
% result)
# Ensure that object disposal of this Python object will (eventually)
# free the memory allocated by the raw library for the cipher mode
self._state = SmartPointer(self._state.get(),
_raw_ocb_lib.OCB_stop_operation)
# Memory allocated for the underlying block cipher is now owed
# by the cipher mode
raw_cipher.release()
def _update(self, assoc_data, assoc_data_len):
result = _raw_ocb_lib.OCB_update(self._state.get(),
c_uint8_ptr(assoc_data),
c_size_t(assoc_data_len))
if result:
raise ValueError("Error %d while computing MAC in OCB mode" % result)
def update(self, assoc_data):
"""Process the associated data.
If there is any associated data, the caller has to invoke
this method one or more times, before using
``decrypt`` or ``encrypt``.
By *associated data* it is meant any data (e.g. packet headers) that
will not be encrypted and will be transmitted in the clear.
However, the receiver shall still able to detect modifications.
If there is no associated data, this method must not be called.
The caller may split associated data in segments of any size, and
invoke this method multiple times, each time with the next segment.
:Parameters:
assoc_data : bytes/bytearray/memoryview
A piece of associated data.
"""
if "update" not in self._next:
raise TypeError("update() can only be called"
" immediately after initialization")
self._next = ["encrypt", "decrypt", "digest",
"verify", "update"]
if len(self._cache_A) > 0:
filler = min(16 - len(self._cache_A), len(assoc_data))
self._cache_A += _copy_bytes(None, filler, assoc_data)
assoc_data = assoc_data[filler:]
if len(self._cache_A) < 16:
return self
# Clear the cache, and proceeding with any other aligned data
self._cache_A, seg = b"", self._cache_A
self.update(seg)
update_len = len(assoc_data) // 16 * 16
self._cache_A = _copy_bytes(update_len, None, assoc_data)
self._update(assoc_data, update_len)
return self
def _transcrypt_aligned(self, in_data, in_data_len,
trans_func, trans_desc):
out_data = create_string_buffer(in_data_len)
result = trans_func(self._state.get(),
in_data,
out_data,
c_size_t(in_data_len))
if result:
raise ValueError("Error %d while %sing in OCB mode"
% (result, trans_desc))
return get_raw_buffer(out_data)
def _transcrypt(self, in_data, trans_func, trans_desc):
# Last piece to encrypt/decrypt
if in_data is None:
out_data = self._transcrypt_aligned(self._cache_P,
len(self._cache_P),
trans_func,
trans_desc)
self._cache_P = b""
return out_data
# Try to fill up the cache, if it already contains something
prefix = b""
if len(self._cache_P) > 0:
filler = min(16 - len(self._cache_P), len(in_data))
self._cache_P += _copy_bytes(None, filler, in_data)
in_data = in_data[filler:]
if len(self._cache_P) < 16:
# We could not manage to fill the cache, so there is certainly
# no output yet.
return b""
# Clear the cache, and proceeding with any other aligned data
prefix = self._transcrypt_aligned(self._cache_P,
len(self._cache_P),
trans_func,
trans_desc)
self._cache_P = b""
# Process data in multiples of the block size
trans_len = len(in_data) // 16 * 16
result = self._transcrypt_aligned(c_uint8_ptr(in_data),
trans_len,
trans_func,
trans_desc)
if prefix:
result = prefix + result
# Left-over
self._cache_P = _copy_bytes(trans_len, None, in_data)
return result
def encrypt(self, plaintext=None):
"""Encrypt the next piece of plaintext.
After the entire plaintext has been passed (but before `digest`),
you **must** call this method one last time with no arguments to collect
the final piece of ciphertext.
If possible, use the method `encrypt_and_digest` instead.
:Parameters:
plaintext : bytes/bytearray/memoryview
The next piece of data to encrypt or ``None`` to signify
that encryption has finished and that any remaining ciphertext
has to be produced.
:Return:
the ciphertext, as a byte string.
Its length may not match the length of the *plaintext*.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() can only be called after"
" initialization or an update()")
if plaintext is None:
self._next = ["digest"]
else:
self._next = ["encrypt"]
return self._transcrypt(plaintext, _raw_ocb_lib.OCB_encrypt, "encrypt")
def decrypt(self, ciphertext=None):
"""Decrypt the next piece of ciphertext.
After the entire ciphertext has been passed (but before `verify`),
you **must** call this method one last time with no arguments to collect
the remaining piece of plaintext.
If possible, use the method `decrypt_and_verify` instead.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The next piece of data to decrypt or ``None`` to signify
that decryption has finished and that any remaining plaintext
has to be produced.
:Return:
the plaintext, as a byte string.
Its length may not match the length of the *ciphertext*.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() can only be called after"
" initialization or an update()")
if ciphertext is None:
self._next = ["verify"]
else:
self._next = ["decrypt"]
return self._transcrypt(ciphertext,
_raw_ocb_lib.OCB_decrypt,
"decrypt")
def _compute_mac_tag(self):
if self._mac_tag is not None:
return
if self._cache_A:
self._update(self._cache_A, len(self._cache_A))
self._cache_A = b""
mac_tag = create_string_buffer(16)
result = _raw_ocb_lib.OCB_digest(self._state.get(),
mac_tag,
c_size_t(len(mac_tag))
)
if result:
raise ValueError("Error %d while computing digest in OCB mode"
% result)
self._mac_tag = get_raw_buffer(mac_tag)[:self._mac_len]
def digest(self):
"""Compute the *binary* MAC tag.
Call this method after the final `encrypt` (the one with no arguments)
to obtain the MAC tag.
The MAC tag is needed by the receiver to determine authenticity
of the message.
:Return: the MAC, as a byte string.
"""
if "digest" not in self._next:
raise TypeError("digest() cannot be called now for this cipher")
assert(len(self._cache_P) == 0)
self._next = ["digest"]
if self._mac_tag is None:
self._compute_mac_tag()
return self._mac_tag
def hexdigest(self):
"""Compute the *printable* MAC tag.
This method is like `digest`.
:Return: the MAC, as a hexadecimal string.
"""
return "".join(["%02x" % bord(x) for x in self.digest()])
def verify(self, received_mac_tag):
"""Validate the *binary* MAC tag.
Call this method after the final `decrypt` (the one with no arguments)
to check if the message is authentic and valid.
:Parameters:
received_mac_tag : bytes/bytearray/memoryview
This is the *binary* MAC, as received from the sender.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
if "verify" not in self._next:
raise TypeError("verify() cannot be called now for this cipher")
assert(len(self._cache_P) == 0)
self._next = ["verify"]
if self._mac_tag is None:
self._compute_mac_tag()
secret = get_random_bytes(16)
mac1 = BLAKE2s.new(digest_bits=160, key=secret, data=self._mac_tag)
mac2 = BLAKE2s.new(digest_bits=160, key=secret, data=received_mac_tag)
if mac1.digest() != mac2.digest():
raise ValueError("MAC check failed")
def hexverify(self, hex_mac_tag):
"""Validate the *printable* MAC tag.
This method is like `verify`.
:Parameters:
hex_mac_tag : string
This is the *printable* MAC, as received from the sender.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
self.verify(unhexlify(hex_mac_tag))
def encrypt_and_digest(self, plaintext):
"""Encrypt the message and create the MAC tag in one step.
:Parameters:
plaintext : bytes/bytearray/memoryview
The entire message to encrypt.
:Return:
a tuple with two byte strings:
- the encrypted data
- the MAC
"""
return self.encrypt(plaintext) + self.encrypt(), self.digest()
def decrypt_and_verify(self, ciphertext, received_mac_tag):
"""Decrypted the message and verify its authenticity in one step.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The entire message to decrypt.
received_mac_tag : byte string
This is the *binary* MAC, as received from the sender.
:Return: the decrypted data (byte string).
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
plaintext = self.decrypt(ciphertext) + self.decrypt()
self.verify(received_mac_tag)
return plaintext
def _create_ocb_cipher(factory, **kwargs):
"""Create a new block cipher, configured in OCB mode.
:Parameters:
factory : module
A symmetric cipher module from `Crypto.Cipher`
(like `Crypto.Cipher.AES`).
:Keywords:
nonce : bytes/bytearray/memoryview
A value that must never be reused for any other encryption.
Its length can vary from 1 to 15 bytes.
If not specified, a random 15 bytes long nonce is generated.
mac_len : integer
Length of the MAC, in bytes.
It must be in the range ``[8..16]``.
The default is 16 (128 bits).
Any other keyword will be passed to the underlying block cipher.
See the relevant documentation for details (at least ``key`` will need
to be present).
"""
try:
nonce = kwargs.pop("nonce", None)
if nonce is None:
nonce = get_random_bytes(15)
mac_len = kwargs.pop("mac_len", 16)
except KeyError as e:
raise TypeError("Keyword missing: " + str(e))
return OcbMode(factory, nonce, mac_len, kwargs)

View File

@ -0,0 +1,36 @@
from types import ModuleType
from typing import Union, Any, Optional, Tuple, Dict, overload
Buffer = Union[bytes, bytearray, memoryview]
class OcbMode(object):
block_size: int
nonce: Buffer
def __init__(self,
factory: ModuleType,
nonce: Buffer,
mac_len: int,
cipher_params: Dict) -> None: ...
def update(self, assoc_data: Buffer) -> OcbMode: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def verify(self, received_mac_tag: Buffer) -> None: ...
def hexverify(self, hex_mac_tag: str) -> None: ...
def encrypt_and_digest(self,
plaintext: Buffer) -> Tuple[bytes, bytes]: ...
def decrypt_and_verify(self,
ciphertext: Buffer,
received_mac_tag: Buffer) -> bytes: ...

View File

@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
#
# Cipher/mode_ofb.py : OFB mode
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================
"""
Output Feedback (CFB) mode.
"""
__all__ = ['OfbMode']
from Crypto.Util.py3compat import _copy_bytes
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
create_string_buffer, get_raw_buffer,
SmartPointer, c_size_t, c_uint8_ptr,
is_writeable_buffer)
from Crypto.Random import get_random_bytes
raw_ofb_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_ofb", """
int OFB_start_operation(void *cipher,
const uint8_t iv[],
size_t iv_len,
void **pResult);
int OFB_encrypt(void *ofbState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int OFB_decrypt(void *ofbState,
const uint8_t *in,
uint8_t *out,
size_t data_len);
int OFB_stop_operation(void *state);
"""
)
class OfbMode(object):
"""*Output FeedBack (OFB)*.
This mode is very similar to CBC, but it
transforms the underlying block cipher into a stream cipher.
The keystream is the iterated block encryption of the
previous ciphertext block.
An Initialization Vector (*IV*) is required.
See `NIST SP800-38A`_ , Section 6.4.
.. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
:undocumented: __init__
"""
def __init__(self, block_cipher, iv):
"""Create a new block cipher, configured in OFB mode.
:Parameters:
block_cipher : C pointer
A smart pointer to the low-level block cipher instance.
iv : bytes/bytearray/memoryview
The initialization vector to use for encryption or decryption.
It is as long as the cipher block.
**The IV must be a nonce, to to be reused for any other
message**. It shall be a nonce or a random value.
Reusing the *IV* for encryptions performed with the same key
compromises confidentiality.
"""
self._state = VoidPointer()
result = raw_ofb_lib.OFB_start_operation(block_cipher.get(),
c_uint8_ptr(iv),
c_size_t(len(iv)),
self._state.address_of())
if result:
raise ValueError("Error %d while instantiating the OFB mode"
% result)
# Ensure that object disposal of this Python object will (eventually)
# free the memory allocated by the raw library for the cipher mode
self._state = SmartPointer(self._state.get(),
raw_ofb_lib.OFB_stop_operation)
# Memory allocated for the underlying block cipher is now owed
# by the cipher mode
block_cipher.release()
self.block_size = len(iv)
"""The block size of the underlying cipher, in bytes."""
self.iv = _copy_bytes(None, None, iv)
"""The Initialization Vector originally used to create the object.
The value does not change."""
self.IV = self.iv
"""Alias for `iv`"""
self._next = ["encrypt", "decrypt"]
def encrypt(self, plaintext, output=None):
"""Encrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have encrypted a message
you cannot encrypt (or decrypt) another message using the same
object.
The data to encrypt can be broken up in two or
more pieces and `encrypt` can be called multiple times.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is equivalent to:
>>> c.encrypt(a+b)
This function does not add any padding to the plaintext.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() cannot be called after decrypt()")
self._next = ["encrypt"]
if output is None:
ciphertext = create_string_buffer(len(plaintext))
else:
ciphertext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(plaintext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_ofb_lib.OFB_encrypt(self._state.get(),
c_uint8_ptr(plaintext),
c_uint8_ptr(ciphertext),
c_size_t(len(plaintext)))
if result:
raise ValueError("Error %d while encrypting in OFB mode" % result)
if output is None:
return get_raw_buffer(ciphertext)
else:
return None
def decrypt(self, ciphertext, output=None):
"""Decrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have decrypted a message
you cannot decrypt (or encrypt) another message with the same
object.
The data to decrypt can be broken up in two or
more pieces and `decrypt` can be called multiple times.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is equivalent to:
>>> c.decrypt(a+b)
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
It can be of any length.
:Keywords:
output : bytearray/memoryview
The location where the plaintext is written to.
If ``None``, the plaintext is returned.
:Return:
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
Otherwise, ``None``.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() cannot be called after encrypt()")
self._next = ["decrypt"]
if output is None:
plaintext = create_string_buffer(len(ciphertext))
else:
plaintext = output
if not is_writeable_buffer(output):
raise TypeError("output must be a bytearray or a writeable memoryview")
if len(ciphertext) != len(output):
raise ValueError("output must have the same length as the input"
" (%d bytes)" % len(plaintext))
result = raw_ofb_lib.OFB_decrypt(self._state.get(),
c_uint8_ptr(ciphertext),
c_uint8_ptr(plaintext),
c_size_t(len(ciphertext)))
if result:
raise ValueError("Error %d while decrypting in OFB mode" % result)
if output is None:
return get_raw_buffer(plaintext)
else:
return None
def _create_ofb_cipher(factory, **kwargs):
"""Instantiate a cipher object that performs OFB encryption/decryption.
:Parameters:
factory : module
The underlying block cipher, a module from ``Crypto.Cipher``.
:Keywords:
iv : bytes/bytearray/memoryview
The IV to use for OFB.
IV : bytes/bytearray/memoryview
Alias for ``iv``.
Any other keyword will be passed to the underlying block cipher.
See the relevant documentation for details (at least ``key`` will need
to be present).
"""
cipher_state = factory._create_base_cipher(kwargs)
iv = kwargs.pop("IV", None)
IV = kwargs.pop("iv", None)
if (None, None) == (iv, IV):
iv = get_random_bytes(factory.block_size)
if iv is not None:
if IV is not None:
raise TypeError("You must either use 'iv' or 'IV', not both")
else:
iv = IV
if len(iv) != factory.block_size:
raise ValueError("Incorrect IV length (it must be %d bytes long)" %
factory.block_size)
if kwargs:
raise TypeError("Unknown parameters for OFB: %s" % str(kwargs))
return OfbMode(cipher_state, iv)

View File

@ -0,0 +1,25 @@
from typing import Union, overload
from Crypto.Util._raw_api import SmartPointer
Buffer = Union[bytes, bytearray, memoryview]
__all__ = ['OfbMode']
class OfbMode(object):
block_size: int
iv: Buffer
IV: Buffer
def __init__(self,
block_cipher: SmartPointer,
iv: Buffer) -> None: ...
@overload
def encrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
@overload
def decrypt(self, plaintext: Buffer) -> bytes: ...
@overload
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...

View File

@ -0,0 +1,206 @@
# ===================================================================
#
# Copyright (c) 2014, Legrandin <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
"""
OpenPGP mode.
"""
__all__ = ['OpenPgpMode']
from Crypto.Util.py3compat import _copy_bytes
from Crypto.Random import get_random_bytes
class OpenPgpMode(object):
"""OpenPGP mode.
This mode is a variant of CFB, and it is only used in PGP and
OpenPGP_ applications. If in doubt, use another mode.
An Initialization Vector (*IV*) is required.
Unlike CFB, the *encrypted* IV (not the IV itself) is
transmitted to the receiver.
The IV is a random data block. For legacy reasons, two of its bytes are
duplicated to act as a checksum for the correctness of the key, which is now
known to be insecure and is ignored. The encrypted IV is therefore 2 bytes
longer than the clean IV.
.. _OpenPGP: http://tools.ietf.org/html/rfc4880
:undocumented: __init__
"""
def __init__(self, factory, key, iv, cipher_params):
#: The block size of the underlying cipher, in bytes.
self.block_size = factory.block_size
self._done_first_block = False # True after the first encryption
# Instantiate a temporary cipher to process the IV
IV_cipher = factory.new(
key,
factory.MODE_CFB,
IV=b'\x00' * self.block_size,
segment_size=self.block_size * 8,
**cipher_params)
iv = _copy_bytes(None, None, iv)
# The cipher will be used for...
if len(iv) == self.block_size:
# ... encryption
self._encrypted_IV = IV_cipher.encrypt(iv + iv[-2:])
elif len(iv) == self.block_size + 2:
# ... decryption
self._encrypted_IV = iv
# Last two bytes are for a deprecated "quick check" feature that
# should not be used. (https://eprint.iacr.org/2005/033)
iv = IV_cipher.decrypt(iv)[:-2]
else:
raise ValueError("Length of IV must be %d or %d bytes"
" for MODE_OPENPGP"
% (self.block_size, self.block_size + 2))
self.iv = self.IV = iv
# Instantiate the cipher for the real PGP data
self._cipher = factory.new(
key,
factory.MODE_CFB,
IV=self._encrypted_IV[-self.block_size:],
segment_size=self.block_size * 8,
**cipher_params)
def encrypt(self, plaintext):
"""Encrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have encrypted a message
you cannot encrypt (or decrypt) another message using the same
object.
The data to encrypt can be broken up in two or
more pieces and `encrypt` can be called multiple times.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is equivalent to:
>>> c.encrypt(a+b)
This function does not add any padding to the plaintext.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
:Return:
the encrypted data, as a byte string.
It is as long as *plaintext* with one exception:
when encrypting the first message chunk,
the encypted IV is prepended to the returned ciphertext.
"""
res = self._cipher.encrypt(plaintext)
if not self._done_first_block:
res = self._encrypted_IV + res
self._done_first_block = True
return res
def decrypt(self, ciphertext):
"""Decrypt data with the key and the parameters set at initialization.
A cipher object is stateful: once you have decrypted a message
you cannot decrypt (or encrypt) another message with the same
object.
The data to decrypt can be broken up in two or
more pieces and `decrypt` can be called multiple times.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is equivalent to:
>>> c.decrypt(a+b)
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
:Return: the decrypted data (byte string).
"""
return self._cipher.decrypt(ciphertext)
def _create_openpgp_cipher(factory, **kwargs):
"""Create a new block cipher, configured in OpenPGP mode.
:Parameters:
factory : module
The module.
:Keywords:
key : bytes/bytearray/memoryview
The secret key to use in the symmetric cipher.
IV : bytes/bytearray/memoryview
The initialization vector to use for encryption or decryption.
For encryption, the IV must be as long as the cipher block size.
For decryption, it must be 2 bytes longer (it is actually the
*encrypted* IV which was prefixed to the ciphertext).
"""
iv = kwargs.pop("IV", None)
IV = kwargs.pop("iv", None)
if (None, None) == (iv, IV):
iv = get_random_bytes(factory.block_size)
if iv is not None:
if IV is not None:
raise TypeError("You must either use 'iv' or 'IV', not both")
else:
iv = IV
try:
key = kwargs.pop("key")
except KeyError as e:
raise TypeError("Missing component: " + str(e))
return OpenPgpMode(factory, key, iv, kwargs)

View File

@ -0,0 +1,20 @@
from types import ModuleType
from typing import Union, Dict
Buffer = Union[bytes, bytearray, memoryview]
__all__ = ['OpenPgpMode']
class OpenPgpMode(object):
block_size: int
iv: Union[bytes, bytearray, memoryview]
IV: Union[bytes, bytearray, memoryview]
def __init__(self,
factory: ModuleType,
key: Buffer,
iv: Buffer,
cipher_params: Dict) -> None: ...
def encrypt(self, plaintext: Buffer) -> bytes: ...
def decrypt(self, plaintext: Buffer) -> bytes: ...

View File

@ -0,0 +1,392 @@
# ===================================================================
#
# Copyright (c) 2014, Legrandin <helderijs@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================
"""
Synthetic Initialization Vector (SIV) mode.
"""
__all__ = ['SivMode']
from binascii import hexlify, unhexlify
from Crypto.Util.py3compat import bord, _copy_bytes
from Crypto.Util._raw_api import is_buffer
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Protocol.KDF import _S2V
from Crypto.Hash import BLAKE2s
from Crypto.Random import get_random_bytes
class SivMode(object):
"""Synthetic Initialization Vector (SIV).
This is an Authenticated Encryption with Associated Data (`AEAD`_) mode.
It provides both confidentiality and authenticity.
The header of the message may be left in the clear, if needed, and it will
still be subject to authentication. The decryption step tells the receiver
if the message comes from a source that really knowns the secret key.
Additionally, decryption detects if any part of the message - including the
header - has been modified or corrupted.
Unlike other AEAD modes such as CCM, EAX or GCM, accidental reuse of a
nonce is not catastrophic for the confidentiality of the message. The only
effect is that an attacker can tell when the same plaintext (and same
associated data) is protected with the same key.
The length of the MAC is fixed to the block size of the underlying cipher.
The key size is twice the length of the key of the underlying cipher.
This mode is only available for AES ciphers.
+--------------------+---------------+-------------------+
| Cipher | SIV MAC size | SIV key length |
| | (bytes) | (bytes) |
+====================+===============+===================+
| AES-128 | 16 | 32 |
+--------------------+---------------+-------------------+
| AES-192 | 16 | 48 |
+--------------------+---------------+-------------------+
| AES-256 | 16 | 64 |
+--------------------+---------------+-------------------+
See `RFC5297`_ and the `original paper`__.
.. _RFC5297: https://tools.ietf.org/html/rfc5297
.. _AEAD: http://blog.cryptographyengineering.com/2012/05/how-to-choose-authenticated-encryption.html
.. __: http://www.cs.ucdavis.edu/~rogaway/papers/keywrap.pdf
:undocumented: __init__
"""
def __init__(self, factory, key, nonce, kwargs):
self.block_size = factory.block_size
"""The block size of the underlying cipher, in bytes."""
self._factory = factory
self._cipher_params = kwargs
if len(key) not in (32, 48, 64):
raise ValueError("Incorrect key length (%d bytes)" % len(key))
if nonce is not None:
if not is_buffer(nonce):
raise TypeError("When provided, the nonce must be bytes, bytearray or memoryview")
if len(nonce) == 0:
raise ValueError("When provided, the nonce must be non-empty")
self.nonce = _copy_bytes(None, None, nonce)
"""Public attribute is only available in case of non-deterministic
encryption."""
subkey_size = len(key) // 2
self._mac_tag = None # Cache for MAC tag
self._kdf = _S2V(key[:subkey_size],
ciphermod=factory,
cipher_params=self._cipher_params)
self._subkey_cipher = key[subkey_size:]
# Purely for the purpose of verifying that cipher_params are OK
factory.new(key[:subkey_size], factory.MODE_ECB, **kwargs)
# Allowed transitions after initialization
self._next = ["update", "encrypt", "decrypt",
"digest", "verify"]
def _create_ctr_cipher(self, v):
"""Create a new CTR cipher from V in SIV mode"""
v_int = bytes_to_long(v)
q = v_int & 0xFFFFFFFFFFFFFFFF7FFFFFFF7FFFFFFF
return self._factory.new(
self._subkey_cipher,
self._factory.MODE_CTR,
initial_value=q,
nonce=b"",
**self._cipher_params)
def update(self, component):
"""Protect one associated data component
For SIV, the associated data is a sequence (*vector*) of non-empty
byte strings (*components*).
This method consumes the next component. It must be called
once for each of the components that constitue the associated data.
Note that the components have clear boundaries, so that:
>>> cipher.update(b"builtin")
>>> cipher.update(b"securely")
is not equivalent to:
>>> cipher.update(b"built")
>>> cipher.update(b"insecurely")
If there is no associated data, this method must not be called.
:Parameters:
component : bytes/bytearray/memoryview
The next associated data component.
"""
if "update" not in self._next:
raise TypeError("update() can only be called"
" immediately after initialization")
self._next = ["update", "encrypt", "decrypt",
"digest", "verify"]
return self._kdf.update(component)
def encrypt(self, plaintext):
"""
For SIV, encryption and MAC authentication must take place at the same
point. This method shall not be used.
Use `encrypt_and_digest` instead.
"""
raise TypeError("encrypt() not allowed for SIV mode."
" Use encrypt_and_digest() instead.")
def decrypt(self, ciphertext):
"""
For SIV, decryption and verification must take place at the same
point. This method shall not be used.
Use `decrypt_and_verify` instead.
"""
raise TypeError("decrypt() not allowed for SIV mode."
" Use decrypt_and_verify() instead.")
def digest(self):
"""Compute the *binary* MAC tag.
The caller invokes this function at the very end.
This method returns the MAC that shall be sent to the receiver,
together with the ciphertext.
:Return: the MAC, as a byte string.
"""
if "digest" not in self._next:
raise TypeError("digest() cannot be called when decrypting"
" or validating a message")
self._next = ["digest"]
if self._mac_tag is None:
self._mac_tag = self._kdf.derive()
return self._mac_tag
def hexdigest(self):
"""Compute the *printable* MAC tag.
This method is like `digest`.
:Return: the MAC, as a hexadecimal string.
"""
return "".join(["%02x" % bord(x) for x in self.digest()])
def verify(self, received_mac_tag):
"""Validate the *binary* MAC tag.
The caller invokes this function at the very end.
This method checks if the decrypted message is indeed valid
(that is, if the key is correct) and it has not been
tampered with while in transit.
:Parameters:
received_mac_tag : bytes/bytearray/memoryview
This is the *binary* MAC, as received from the sender.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
if "verify" not in self._next:
raise TypeError("verify() cannot be called"
" when encrypting a message")
self._next = ["verify"]
if self._mac_tag is None:
self._mac_tag = self._kdf.derive()
secret = get_random_bytes(16)
mac1 = BLAKE2s.new(digest_bits=160, key=secret, data=self._mac_tag)
mac2 = BLAKE2s.new(digest_bits=160, key=secret, data=received_mac_tag)
if mac1.digest() != mac2.digest():
raise ValueError("MAC check failed")
def hexverify(self, hex_mac_tag):
"""Validate the *printable* MAC tag.
This method is like `verify`.
:Parameters:
hex_mac_tag : string
This is the *printable* MAC, as received from the sender.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
self.verify(unhexlify(hex_mac_tag))
def encrypt_and_digest(self, plaintext, output=None):
"""Perform encrypt() and digest() in one step.
:Parameters:
plaintext : bytes/bytearray/memoryview
The piece of data to encrypt.
:Keywords:
output : bytearray/memoryview
The location where the ciphertext must be written to.
If ``None``, the ciphertext is returned.
:Return:
a tuple with two items:
- the ciphertext, as ``bytes``
- the MAC tag, as ``bytes``
The first item becomes ``None`` when the ``output`` parameter
specified a location for the result.
"""
if "encrypt" not in self._next:
raise TypeError("encrypt() can only be called after"
" initialization or an update()")
self._next = ["digest"]
# Compute V (MAC)
if hasattr(self, 'nonce'):
self._kdf.update(self.nonce)
self._kdf.update(plaintext)
self._mac_tag = self._kdf.derive()
cipher = self._create_ctr_cipher(self._mac_tag)
return cipher.encrypt(plaintext, output=output), self._mac_tag
def decrypt_and_verify(self, ciphertext, mac_tag, output=None):
"""Perform decryption and verification in one step.
A cipher object is stateful: once you have decrypted a message
you cannot decrypt (or encrypt) another message with the same
object.
You cannot reuse an object for encrypting
or decrypting other data with the same key.
This function does not remove any padding from the plaintext.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
It can be of any length.
mac_tag : bytes/bytearray/memoryview
This is the *binary* MAC, as received from the sender.
:Keywords:
output : bytearray/memoryview
The location where the plaintext must be written to.
If ``None``, the plaintext is returned.
:Return: the plaintext as ``bytes`` or ``None`` when the ``output``
parameter specified a location for the result.
:Raises ValueError:
if the MAC does not match. The message has been tampered with
or the key is incorrect.
"""
if "decrypt" not in self._next:
raise TypeError("decrypt() can only be called"
" after initialization or an update()")
self._next = ["verify"]
# Take the MAC and start the cipher for decryption
self._cipher = self._create_ctr_cipher(mac_tag)
plaintext = self._cipher.decrypt(ciphertext, output=output)
if hasattr(self, 'nonce'):
self._kdf.update(self.nonce)
self._kdf.update(plaintext if output is None else output)
self.verify(mac_tag)
return plaintext
def _create_siv_cipher(factory, **kwargs):
"""Create a new block cipher, configured in
Synthetic Initializaton Vector (SIV) mode.
:Parameters:
factory : object
A symmetric cipher module from `Crypto.Cipher`
(like `Crypto.Cipher.AES`).
:Keywords:
key : bytes/bytearray/memoryview
The secret key to use in the symmetric cipher.
It must be 32, 48 or 64 bytes long.
If AES is the chosen cipher, the variants *AES-128*,
*AES-192* and or *AES-256* will be used internally.
nonce : bytes/bytearray/memoryview
For deterministic encryption, it is not present.
Otherwise, it is a value that must never be reused
for encrypting message under this key.
There are no restrictions on its length,
but it is recommended to use at least 16 bytes.
"""
try:
key = kwargs.pop("key")
except KeyError as e:
raise TypeError("Missing parameter: " + str(e))
nonce = kwargs.pop("nonce", None)
return SivMode(factory, key, nonce, kwargs)

View File

@ -0,0 +1,38 @@
from types import ModuleType
from typing import Union, Tuple, Dict, Optional, overload
Buffer = Union[bytes, bytearray, memoryview]
__all__ = ['SivMode']
class SivMode(object):
block_size: int
nonce: bytes
def __init__(self,
factory: ModuleType,
key: Buffer,
nonce: Buffer,
kwargs: Dict) -> None: ...
def update(self, component: Buffer) -> SivMode: ...
def encrypt(self, plaintext: Buffer) -> bytes: ...
def decrypt(self, plaintext: Buffer) -> bytes: ...
def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def verify(self, received_mac_tag: Buffer) -> None: ...
def hexverify(self, hex_mac_tag: str) -> None: ...
@overload
def encrypt_and_digest(self,
plaintext: Buffer) -> Tuple[bytes, bytes]: ...
@overload
def encrypt_and_digest(self,
plaintext: Buffer,
output: Buffer) -> Tuple[None, bytes]: ...
def decrypt_and_verify(self,
ciphertext: Buffer,
received_mac_tag: Buffer,
output: Optional[Union[bytearray, memoryview]] = ...) -> bytes: ...

Some files were not shown because too many files have changed in this diff Show More